From dff0df5fdcd5cfa90c711f931f5087113c7fad3a Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Wed, 29 Apr 2026 13:43:40 +0200 Subject: [PATCH] fix(headless): suppress notification spam, categorize messages, distinguish phase vs status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three small UX fixes for headless / autopilot logs: 1. Add `zz-notifications` to TUI_FOOTER_STATUS_KEYS — these are sticky notification dots from the interactive TUI footer; they have no meaning in headless and were spamming the log. 2. Categorize notification messages by prefix so headless output is scannable: [mcp] for MCP-client-ready, [search] for web search status, [parallel] for slice-parallel/subagent dispatch. Falls through to the existing important/non-important formatting for everything else. 3. Distinguish phase transitions from generic status updates: phase:/ milestone:/slice:/task: prefixed keys get [phase]; everything else gets [status]. Previously both used [phase], which was misleading. Patterns based on bunker commits 14ec4d97f / c15afb45f (which were the research source) but written fresh against our existing TUI_FOOTER_STATUS_KEYS structure rather than cherry-picked. The assistant-text-preview commit (cf0274c63) is a separate, larger refactor in headless.ts and is deferred to v3.1. Co-Authored-By: Claude Sonnet 4.6 --- src/headless-ui.ts | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/headless-ui.ts b/src/headless-ui.ts index 6404b2e0a..ca9e0d4e9 100644 --- a/src/headless-ui.ts +++ b/src/headless-ui.ts @@ -90,8 +90,39 @@ const TUI_FOOTER_STATUS_KEYS = new Set([ "ollama", "sf-fast", "sf-auto", + "zz-notifications", ]); +/** + * Categorize notification messages so headless logs are scannable — + * `[mcp]` for MCP-client ready lines, `[search]` for search status, + * `[parallel]` for slice-parallel/subagent dispatch. Returns null to + * fall through to the default formatting. + */ +function formatCategorizedNotification(message: string): string | null { + if (message.startsWith("MCP client ready")) return `[mcp] ${message}`; + if (message.startsWith("Web search:")) return `[search] ${message}`; + if (message.startsWith("Native Anthropic web search")) return `[search] ${message}`; + if (message.includes("dispatching") && message.includes("subagents")) + return `[parallel] ${message}`; + if (message.startsWith("Slice-parallel:")) return `[parallel] ${message}`; + if (message.startsWith("sf-reactive:")) return `[parallel] ${message}`; + return null; +} + +/** + * True when statusKey indicates a real phase/milestone/slice/task transition + * (rather than a generic status update). Drives [phase] vs [status] tagging. + */ +function isPhaseStatusKey(statusKey: string): boolean { + return ( + statusKey.startsWith("phase:") || + statusKey.startsWith("milestone:") || + statusKey.startsWith("slice:") || + statusKey.startsWith("task:") + ); +} + // --------------------------------------------------------------------------- // Tool-Arg Summarizer // --------------------------------------------------------------------------- @@ -351,6 +382,9 @@ export function formatProgress( if (method === "notify") { const msg = String(event.message ?? ""); if (!msg) return null; + // Categorise known prefixes so headless logs are scannable. + const categorised = formatCategorizedNotification(msg); + if (categorised) return categorised; // Bold important notifications const isImportant = /^(committed:|verification gate:|milestone|blocked:)/i.test(msg); @@ -368,10 +402,13 @@ export function formatProgress( // Show meaningful phase transitions. if (statusKey) { const label = parsePhaseLabel(statusKey, msg); - if (label) return `${c.cyan}${tag("phase")}${label}${c.reset}`; + if (label) { + const labelTag = isPhaseStatusKey(statusKey) ? tag("phase") : tag("status"); + return `${c.cyan}${labelTag}${label}${c.reset}`; + } } // Fallback: show message if non-empty. - if (msg) return `${c.cyan}${tag("phase")}${msg}${c.reset}`; + if (msg) return `${c.cyan}${tag("status")}${msg}${c.reset}`; return null; }