From c8137a58c34c253ea4252cb3344fccdae0bdc361 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 7 Jul 2026 08:56:56 -0700 Subject: [PATCH 1/2] Lead with the outcome when no agents are active in the Live Activity With activeCount 0 the aggregate only carries recently finished work, so the presentations read as broken: "0 active agents" in the banner header, "0 active" in the compact island, a lone "0" in the expanded leading slot. Show the outcome instead: the server subtitle ("Agent work completed" / "Agent work failed") as the banner header carrying the outcome tint, "Done"/"Failed" in the count slots, and the hero row's checkmark/cross glyph in the minimal circle. Co-Authored-By: Claude Fable 5 --- apps/mobile/src/widgets/AgentActivity.test.ts | 39 +++++++++++++++++++ apps/mobile/src/widgets/AgentActivity.tsx | 26 +++++++++---- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/widgets/AgentActivity.test.ts b/apps/mobile/src/widgets/AgentActivity.test.ts index fc25b5bf539..e73e7d023aa 100644 --- a/apps/mobile/src/widgets/AgentActivity.test.ts +++ b/apps/mobile/src/widgets/AgentActivity.test.ts @@ -199,6 +199,45 @@ describe("AgentActivity widget layout", () => { ).not.toContain("widgetURL"); }); + it("leads with the outcome instead of a zero count when nothing is active", () => { + const layout = AgentActivity( + { + ...props, + subtitle: "Agent work completed", + activeCount: 0, + activities: [makeRow({ phase: "completed", status: "Done" })], + }, + environment as never, + ); + const banner = JSON.stringify(layout.banner); + expect(banner).toContain("Agent work completed"); + expect(banner).not.toContain("0 active"); + expect(banner).toContain("#6ee7b7"); // emerald-300 header tint + expect(JSON.stringify(layout.compactTrailing)).toContain("Done"); + expect(JSON.stringify(layout.compactTrailing)).not.toContain("0 active"); + expect(JSON.stringify(layout.expandedLeading)).toContain("Done"); + expect(JSON.stringify(layout.minimal)).toContain("checkmark.circle.fill"); + expect(JSON.stringify(layout.bannerSmall)).toContain("Done"); + }); + + it("reads Failed when the finished work ended in failure", () => { + const layout = AgentActivity( + { + ...props, + subtitle: "Agent work failed", + activeCount: 0, + activities: [makeRow({ phase: "failed", status: "Failed" })], + }, + environment as never, + ); + const banner = JSON.stringify(layout.banner); + expect(banner).toContain("Agent work failed"); + expect(banner).toContain("#fca5a5"); // red-300 header tint + expect(JSON.stringify(layout.compactTrailing)).toContain("Failed"); + expect(JSON.stringify(layout.expandedLeading)).toContain("Failed"); + expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill"); + }); + it("renders up to five rows in the banner", () => { const layout = AgentActivity( { diff --git a/apps/mobile/src/widgets/AgentActivity.tsx b/apps/mobile/src/widgets/AgentActivity.tsx index 92e61caaea1..08d62cc54a6 100644 --- a/apps/mobile/src/widgets/AgentActivity.tsx +++ b/apps/mobile/src/widgets/AgentActivity.tsx @@ -123,16 +123,25 @@ export function AgentActivity( ? phaseTint(failedRow.phase) : tint; + // With nothing active the aggregate only carries recently finished work, so + // "0 active agents" (and a lone "0" in the expanded island) read as broken. + // Lead with the outcome instead: the server's subtitle ("Agent work + // completed" / "Agent work failed") as the header, "Done"/"Failed" in the + // count slots. + const allDone = props.activeCount === 0; + const doneLabel = failedRow ? "Failed" : "Done"; + // Header copy: "5 active agents" + (", 1 needs attention"). The banner renders // the two parts in-line so the attention half can carry the accent color; // `summary` is the short form for tight spots (expanded center, watch card). const agentWord = props.activeCount === 1 ? "agent" : "agents"; - const agentsLabel = `${props.activeCount} active ${agentWord}`; + const agentsLabel = allDone ? props.subtitle : `${props.activeCount} active ${agentWord}`; const attentionSuffix = attentionRows.length > 0 ? `${attentionRows.length} need${attentionRows.length === 1 ? "s" : ""} attention` : ""; - const summary = attentionSuffix || `${props.activeCount} active`; + const activeLabel = allDone ? doneLabel : `${props.activeCount} active`; + const summary = attentionSuffix || activeLabel; // Any registered scheme variant routes back to this app; taps are delivered // to the widget's containing app, so the prod scheme is safe for all builds. @@ -142,8 +151,6 @@ export function AgentActivity( ? `t3code://${deepLinkRow.deepLink.slice(1)}` : null; - const activeLabel = `${props.activeCount} active`; - // A scannable status glyph per phase — reads faster than colored words and // ties the compact / expanded / banner / watch presentations together. type SFName = NonNullable["systemName"]>; @@ -245,7 +252,9 @@ export function AgentActivity( @@ -322,16 +331,17 @@ export function AgentActivity( ), // The shared/minimal form is a ~22pt circle — a single signal reads there, - // the wordmark does not. Show the blocking phase glyph, else the mark. + // the wordmark does not. Show the blocking/outcome phase glyph, else the + // mark (all-done shows the hero row's checkmark/cross). minimal: - attentionRow || failedRow + (attentionRow || failedRow || allDone) && heroRow ? renderGlyph(phaseSymbol(heroRow.phase), 13, phaseTint(heroRow.phase)) : renderLogo(11, tint), expandedLeading: ( {renderLogo(15, tint)} - {`${props.activeCount}`} + {allDone ? doneLabel : `${props.activeCount}`} ), From 5b998d6aaf1d874e6333c2160f99a1bcf5e0b8b8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 7 Jul 2026 09:06:30 -0700 Subject: [PATCH 2/2] Derive the all-done outcome from rows so mixed finishes never conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header text came from the server subtitle (keyed off the newest terminal row) while the tint, count labels, and minimal glyph keyed off any failed row — a failed run followed by a newer completed one rendered "Agent work completed" in red with "Failed" in the island. Derive the header from the same failedRow signal so a failure dominates everywhere. Co-Authored-By: Claude Fable 5 --- apps/mobile/src/widgets/AgentActivity.test.ts | 25 +++++++++++++++++++ apps/mobile/src/widgets/AgentActivity.tsx | 11 +++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/widgets/AgentActivity.test.ts b/apps/mobile/src/widgets/AgentActivity.test.ts index e73e7d023aa..dc9cd0e2211 100644 --- a/apps/mobile/src/widgets/AgentActivity.test.ts +++ b/apps/mobile/src/widgets/AgentActivity.test.ts @@ -238,6 +238,31 @@ describe("AgentActivity widget layout", () => { expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill"); }); + it("lets a failure dominate mixed finished outcomes across every presentation", () => { + const layout = AgentActivity( + { + ...props, + // The server subtitle keys off the newest terminal row (completed + // here); the layout must still read Failed everywhere so the header + // text never disagrees with the tint, count slots, or minimal glyph. + subtitle: "Agent work completed", + activeCount: 0, + activities: [ + makeRow({ phase: "completed", status: "Done" }), + makeRow({ threadId: "thread-2", phase: "failed", status: "Failed" }), + ], + }, + environment as never, + ); + const banner = JSON.stringify(layout.banner); + expect(banner).toContain("Agent work failed"); + expect(banner).not.toContain("Agent work completed"); + expect(banner).toContain("#fca5a5"); // red-300 header tint + expect(JSON.stringify(layout.compactTrailing)).toContain("Failed"); + expect(JSON.stringify(layout.expandedLeading)).toContain("Failed"); + expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill"); + }); + it("renders up to five rows in the banner", () => { const layout = AgentActivity( { diff --git a/apps/mobile/src/widgets/AgentActivity.tsx b/apps/mobile/src/widgets/AgentActivity.tsx index 08d62cc54a6..88c85648271 100644 --- a/apps/mobile/src/widgets/AgentActivity.tsx +++ b/apps/mobile/src/widgets/AgentActivity.tsx @@ -125,17 +125,20 @@ export function AgentActivity( // With nothing active the aggregate only carries recently finished work, so // "0 active agents" (and a lone "0" in the expanded island) read as broken. - // Lead with the outcome instead: the server's subtitle ("Agent work - // completed" / "Agent work failed") as the header, "Done"/"Failed" in the - // count slots. + // Lead with the outcome instead. The outcome is derived here from the rows + // rather than taken from the server subtitle (which keys off the newest + // terminal row): every presentation — header text, tint, count slots, + // minimal glyph — must agree, and a failure anywhere should dominate a + // newer success. const allDone = props.activeCount === 0; const doneLabel = failedRow ? "Failed" : "Done"; + const outcomeLabel = failedRow ? "Agent work failed" : "Agent work completed"; // Header copy: "5 active agents" + (", 1 needs attention"). The banner renders // the two parts in-line so the attention half can carry the accent color; // `summary` is the short form for tight spots (expanded center, watch card). const agentWord = props.activeCount === 1 ? "agent" : "agents"; - const agentsLabel = allDone ? props.subtitle : `${props.activeCount} active ${agentWord}`; + const agentsLabel = allDone ? outcomeLabel : `${props.activeCount} active ${agentWord}`; const attentionSuffix = attentionRows.length > 0 ? `${attentionRows.length} need${attentionRows.length === 1 ? "s" : ""} attention`