Skip to content

Commit 4eb1b57

Browse files
Lead with the outcome when no agents are active in the Live Activity (#3768)
1 parent 4414f92 commit 4eb1b57

2 files changed

Lines changed: 85 additions & 8 deletions

File tree

apps/mobile/src/widgets/AgentActivity.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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
),

0 commit comments

Comments
 (0)