diff --git a/src/plugins/diagnostics.ts b/src/plugins/diagnostics.ts index a66e6fc6a..80030decd 100644 --- a/src/plugins/diagnostics.ts +++ b/src/plugins/diagnostics.ts @@ -49,20 +49,28 @@ export function formatPluginWarningsSummary( ): string | undefined { if (warnings.length === 0) return undefined; + // Deduped: one skill referenced by three plugins produces three warnings, + // and listing it three times makes the printed count disagree with the list. + const seenSkills = new Set(); const skillMisses: string[] = []; + let skillWarningCount = 0; for (const w of warnings) { const m = /skill "([^"]+)" referenced but not found/.exec(w); - if (m?.[1] !== undefined) skillMisses.push(m[1]); + if (m?.[1] === undefined) continue; + skillWarningCount += 1; + if (seenSkills.has(m[1])) continue; + seenSkills.add(m[1]); + skillMisses.push(m[1]); } - if (skillMisses.length > 0 && skillMisses.length === warnings.length) { + if (skillMisses.length > 0 && skillWarningCount === warnings.length) { const n = skillMisses.length; return `plugins: ${n} skill${n === 1 ? "" : "s"} missing: ${skillMisses.join(", ")}`; } if (skillMisses.length > 0) { const n = skillMisses.length; - const other = warnings.length - n; + const other = warnings.length - skillWarningCount; return `plugins: ${n} skill${n === 1 ? "" : "s"} missing (${skillMisses.join(", ")}); ${other} other warning${other === 1 ? "" : "s"}`; } diff --git a/src/tui-opentui/runner-host.ts b/src/tui-opentui/runner-host.ts index b98abd8d8..dff2cc83a 100644 --- a/src/tui-opentui/runner-host.ts +++ b/src/tui-opentui/runner-host.ts @@ -327,7 +327,7 @@ export async function mountRunnerHost(deps: RunnerHostDeps): Promise ...(deps.surfaces ?? {}), ...(host.openModels !== undefined ? { openModels: host.openModels } : {}), notify: (text) => - appendStreamRow(host.shell, { role: "system", text, meta: "command" }), + appendStreamRow(host.shell, { role: "system", text }), } const refreshModels = ( diff --git a/src/tui/runner.ts b/src/tui/runner.ts index 1a4d4ec6c..a88f418f8 100644 --- a/src/tui/runner.ts +++ b/src/tui/runner.ts @@ -150,6 +150,7 @@ import { setPromptRecognitionSource, setSentMessageHistory, setShellRunState, + surfaceStartupNotice, } from "../tui-opentui/shell.js"; import { classifyAgentSendFailure, @@ -1841,7 +1842,7 @@ export async function runTUI(initialConfig: Config): Promise { }; const systemRow = (text: string): void => { - appendStreamRow(host.shell, { role: "system", text, meta: "command" }); + appendStreamRow(host.shell, { role: "system", text }); }; /** Settle the shell after a rejected send so the run does not look live. */ @@ -2370,9 +2371,12 @@ export async function runTUI(initialConfig: Config): Promise { }); }); - // Surface fire-and-forget startup plugin diagnostics now that the shell has - // a transcript to write into (queued above, before `host` existed). - for (const notice of startupPluginNotices) systemRow(notice); + // Startup diagnostics ride the notice strip, not the transcript: an + // `appendStreamRow` here calls `clearLandingMark` and takes the whole landing + // hero with it — mark, the hints beside it, the composition — so a single + // plugin warning left a first run with no landing at all. Same reason + // CL-5618 routed MCP and hook failures here; this is the path it missed. + for (const notice of startupPluginNotices) surfaceStartupNotice(host.shell, notice); await host.waitUntilExit(); // Quitting mid-stream is an abnormal end for the in-flight cycle: nothing