authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-07 09:12:52-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-07 09:12:52-07:00
log4eb1b5793340f8a197f9f995aa396c9a306dbabf
treefe9ad21545a77d30b53c062bdfb0b28fac32334f
parent4414f922fe4c83b819f9c1585a85d42f7b295ef6
signature Signed by PGP key B5690EEEBB952194

Lead with the outcome when no agents are active in the Live Activity (#3768)


2 files changed, 85 insertions(+), 8 deletions(-)

apps/mobile/src/widgets/AgentActivity.test.ts+64
......@@ -199,6 +199,70 @@ describe("AgentActivity widget layout", () => {
199199 ).not.toContain("widgetURL");
200200 });
201201
202 it("leads with the outcome instead of a zero count when nothing is active", () => {
203 const layout = AgentActivity(
204 {
205 ...props,
206 subtitle: "Agent work completed",
207 activeCount: 0,
208 activities: [makeRow({ phase: "completed", status: "Done" })],
209 },
210 environment as never,
211 );
212 const banner = JSON.stringify(layout.banner);
213 expect(banner).toContain("Agent work completed");
214 expect(banner).not.toContain("0 active");
215 expect(banner).toContain("#6ee7b7"); // emerald-300 header tint
216 expect(JSON.stringify(layout.compactTrailing)).toContain("Done");
217 expect(JSON.stringify(layout.compactTrailing)).not.toContain("0 active");
218 expect(JSON.stringify(layout.expandedLeading)).toContain("Done");
219 expect(JSON.stringify(layout.minimal)).toContain("checkmark.circle.fill");
220 expect(JSON.stringify(layout.bannerSmall)).toContain("Done");
221 });
222
223 it("reads Failed when the finished work ended in failure", () => {
224 const layout = AgentActivity(
225 {
226 ...props,
227 subtitle: "Agent work failed",
228 activeCount: 0,
229 activities: [makeRow({ phase: "failed", status: "Failed" })],
230 },
231 environment as never,
232 );
233 const banner = JSON.stringify(layout.banner);
234 expect(banner).toContain("Agent work failed");
235 expect(banner).toContain("#fca5a5"); // red-300 header tint
236 expect(JSON.stringify(layout.compactTrailing)).toContain("Failed");
237 expect(JSON.stringify(layout.expandedLeading)).toContain("Failed");
238 expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill");
239 });
240
241 it("lets a failure dominate mixed finished outcomes across every presentation", () => {
242 const layout = AgentActivity(
243 {
244 ...props,
245 // The server subtitle keys off the newest terminal row (completed
246 // here); the layout must still read Failed everywhere so the header
247 // text never disagrees with the tint, count slots, or minimal glyph.
248 subtitle: "Agent work completed",
249 activeCount: 0,
250 activities: [
251 makeRow({ phase: "completed", status: "Done" }),
252 makeRow({ threadId: "thread-2", phase: "failed", status: "Failed" }),
253 ],
254 },
255 environment as never,
256 );
257 const banner = JSON.stringify(layout.banner);
258 expect(banner).toContain("Agent work failed");
259 expect(banner).not.toContain("Agent work completed");
260 expect(banner).toContain("#fca5a5"); // red-300 header tint
261 expect(JSON.stringify(layout.compactTrailing)).toContain("Failed");
262 expect(JSON.stringify(layout.expandedLeading)).toContain("Failed");
263 expect(JSON.stringify(layout.minimal)).toContain("xmark.octagon.fill");
264 });
265
202266 it("renders up to five rows in the banner", () => {
203267 const layout = AgentActivity(
204268 {
apps/mobile/src/widgets/AgentActivity.tsx+21-8
......@@ -123,16 +123,28 @@ export function AgentActivity(
123123 ? phaseTint(failedRow.phase)
124124 : tint;
125125
126 // With nothing active the aggregate only carries recently finished work, so
127 // "0 active agents" (and a lone "0" in the expanded island) read as broken.
128 // Lead with the outcome instead. The outcome is derived here from the rows
129 // rather than taken from the server subtitle (which keys off the newest
130 // terminal row): every presentation — header text, tint, count slots,
131 // minimal glyph — must agree, and a failure anywhere should dominate a
132 // newer success.
133 const allDone = props.activeCount === 0;
134 const doneLabel = failedRow ? "Failed" : "Done";
135 const outcomeLabel = failedRow ? "Agent work failed" : "Agent work completed";
136
126137 // Header copy: "5 active agents" + (", 1 needs attention"). The banner renders
127138 // the two parts in-line so the attention half can carry the accent color;
128139 // `summary` is the short form for tight spots (expanded center, watch card).
129140 const agentWord = props.activeCount === 1 ? "agent" : "agents";
130 const agentsLabel = `${props.activeCount} active ${agentWord}`;
141 const agentsLabel = allDone ? outcomeLabel : `${props.activeCount} active ${agentWord}`;
131142 const attentionSuffix =
132143 attentionRows.length > 0
133144 ? `${attentionRows.length} need${attentionRows.length === 1 ? "s" : ""} attention`
134145 : "";
135 const summary = attentionSuffix || `${props.activeCount} active`;
146 const activeLabel = allDone ? doneLabel : `${props.activeCount} active`;
147 const summary = attentionSuffix || activeLabel;
136148
137149 // Any registered scheme variant routes back to this app; taps are delivered
138150 // to the widget's containing app, so the prod scheme is safe for all builds.
......@@ -142,8 +154,6 @@ export function AgentActivity(
142154 ? `t3code://${deepLinkRow.deepLink.slice(1)}`
143155 : null;
144156
145 const activeLabel = `${props.activeCount} active`;
146
147157 // A scannable status glyph per phase — reads faster than colored words and
148158 // ties the compact / expanded / banner / watch presentations together.
149159 type SFName = NonNullable<ComponentProps<typeof Image>["systemName"]>;
......@@ -245,7 +255,9 @@ export function AgentActivity(
245255 <Text
246256 modifiers={[
247257 font({ weight: "semibold", size: 13 }),
248 foregroundStyle(primaryForeground),
258 // The all-done header carries the outcome tint (emerald /
259 // red) the way the Done/Failed status labels do.
260 foregroundStyle(allDone ? headerTint : primaryForeground),
249261 lineLimit(1),
250262 ]}
251263 >
......@@ -322,16 +334,17 @@ export function AgentActivity(
322334 </Text>
323335 ),
324336 // The shared/minimal form is a ~22pt circle — a single signal reads there,
325 // the wordmark does not. Show the blocking phase glyph, else the mark.
337 // the wordmark does not. Show the blocking/outcome phase glyph, else the
338 // mark (all-done shows the hero row's checkmark/cross).
326339 minimal:
327 attentionRow || failedRow
340 (attentionRow || failedRow || allDone) && heroRow
328341 ? renderGlyph(phaseSymbol(heroRow.phase), 13, phaseTint(heroRow.phase))
329342 : renderLogo(11, tint),
330343 expandedLeading: (
331344 <HStack spacing={5} alignment="center" modifiers={[padding({ leading: 4, vertical: 4 })]}>
332345 {renderLogo(15, tint)}
333346 <Text modifiers={[font({ weight: "bold", size: 13 }), foregroundStyle(tint)]}>
334 {`${props.activeCount}`}
347 {allDone ? doneLabel : `${props.activeCount}`}
335348 </Text>
336349 </HStack>
337350 ),