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; }