From ecb08e809c2bbbdd4070019676d835fe2e4268bd Mon Sep 17 00:00:00 2001 From: felixlaga Date: Sat, 7 Nov 2026 17:22:25 +0900 Subject: [PATCH] fix(provider-core): background work notices no longer say every task failed when one did When several background tasks end together with different outcomes, the notice used the worst outcome's verb for all of them. Two commands where only lint failed read `Command "npm test" (exit 0) and command "npm run lint" (exit 1) failed`, and four subagents with one failure read "4 subagents failed". The summary now keeps one verb only when every task shares it. Mixed named tasks each get their own verb, and counted tasks are grouped by outcome, worst first: "1 subagent failed and 3 finished". Co-Authored-By: Claude Opus 5.5 --- .../src/server/notification.test.ts | 22 ++++++++ .../provider-core/src/server/notification.ts | 54 +++++++++++++------ 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/packages/provider-core/src/server/notification.test.ts b/packages/provider-core/src/server/notification.test.ts index 272cd007632a2..f8d0b1d113d46 100644 --- a/packages/provider-core/src/server/notification.test.ts +++ b/packages/provider-core/src/server/notification.test.ts @@ -60,6 +60,16 @@ describe("backgroundWorkNotification", () => { outcome: "cancelled", summary: 'Subagent "Agent B" and command "Sleep 60 seconds" were stopped', }); + assert.deepInclude( + backgroundWorkNotification([ + { kind: "command", label: "npm test", outcome: "completed", exitCode: 0 }, + { kind: "command", label: "npm run lint", outcome: "failed", exitCode: 1 }, + ]), + { + outcome: "failed", + summary: 'Command "npm test" finished (exit 0) and command "npm run lint" failed (exit 1)', + }, + ); }); it("counts work it cannot list", () => { @@ -73,6 +83,18 @@ describe("backgroundWorkNotification", () => { outcome: "completed", summary: "4 subagents finished", }); + assert.deepEqual( + backgroundWorkNotification( + reports.map((report, index) => + index === 0 ? { ...report, outcome: "failed" as const } : report, + ), + ), + { + source: { kind: "subagent" }, + outcome: "failed", + summary: "1 subagent failed and 3 finished", + }, + ); }); }); diff --git a/packages/provider-core/src/server/notification.ts b/packages/provider-core/src/server/notification.ts index 55b1125fd9448..9ccb425faf2d1 100644 --- a/packages/provider-core/src/server/notification.ts +++ b/packages/provider-core/src/server/notification.ts @@ -71,11 +71,26 @@ function exitSuffix(report: BackgroundWorkReport): string { : ""; } -function namedReport(report: BackgroundWorkReport, capitalize: boolean): string { +function namedReport(report: BackgroundWorkReport, capitalize: boolean, verb?: string): string { const noun = KIND_NOUN[report.kind][0]; const label = reportLabel(report.label); const named = `${capitalize ? noun : noun.toLowerCase()}${label === undefined ? "" : ` "${label}"`}`; - return `${named}${exitSuffix(report)}`; + return `${named}${verb === undefined ? "" : ` ${verb}`}${exitSuffix(report)}`; +} + +const OUTCOME_GROUPS = ["failed", "cancelled", "updated", "completed"] as const; + +function groupVerb(group: (typeof OUTCOME_GROUPS)[number], count: number): string { + switch (group) { + case "failed": + return "failed"; + case "cancelled": + return count === 1 ? "was stopped" : "were stopped"; + case "updated": + return "updated"; + case "completed": + return "finished"; + } } function joinNames(names: ReadonlyArray): string { @@ -85,7 +100,6 @@ function joinNames(names: ReadonlyArray): string { function reportsSummary( reports: readonly [BackgroundWorkReport, ...ReadonlyArray], - outcome: NotificationOutcome, ): string { const [first] = reports; if (reports.length === 1) { @@ -93,20 +107,30 @@ function reportsSummary( const noun = KIND_NOUN[first.kind][0]; return `${noun}${label === undefined ? "" : ` "${label}"`} ${outcomeVerb(first.kind, first.outcome)}${exitSuffix(first)}`; } - const verb = - outcome === "failed" - ? "failed" - : outcome === "cancelled" - ? "were stopped" - : outcome === "updated" - ? "updated" - : "finished"; + const groups = OUTCOME_GROUPS.flatMap((group) => { + const count = reports.filter( + (report) => (report.outcome === "unknown" ? "completed" : report.outcome) === group, + ).length; + return count === 0 ? [] : [{ group, count }]; + }); if (reports.length > 3 || reports.every((report) => reportLabel(report.label) === undefined)) { const kinds = new Set(reports.map((report) => report.kind)); - const noun = kinds.size === 1 ? KIND_NOUN[first.kind][1] : "background tasks"; - return `${reports.length} ${noun} ${verb}`; + const [noun, nouns] = kinds.size === 1 ? KIND_NOUN[first.kind] : KIND_NOUN.background_task; + return joinNames( + groups.map( + ({ group, count }, index) => + `${count}${index === 0 ? ` ${count === 1 ? noun.toLowerCase() : nouns}` : ""} ${groupVerb(group, count)}`, + ), + ); + } + if (groups.length > 1) { + return joinNames( + reports.map((report, index) => + namedReport(report, index === 0, outcomeVerb(report.kind, report.outcome)), + ), + ); } - return `${joinNames(reports.map((report, index) => namedReport(report, index === 0)))} ${verb}`; + return `${joinNames(reports.map((report, index) => namedReport(report, index === 0)))} ${groupVerb(groups[0]!.group, reports.length)}`; } function reportsSource( @@ -141,7 +165,7 @@ export function backgroundWorkNotification( return { source: reportsSource([first, ...rest]), outcome, - summary: reportsSummary([first, ...rest], outcome), + summary: reportsSummary([first, ...rest]), }; }