Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions apps/mobile/src/widgets/AgentActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,70 @@ 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("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(
{
Expand Down
29 changes: 21 additions & 8 deletions apps/mobile/src/widgets/AgentActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,28 @@ 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 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";
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
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 = `${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`
: "";
const summary = attentionSuffix || `${props.activeCount} active`;
const activeLabel = allDone ? doneLabel : `${props.activeCount} active`;
const summary = attentionSuffix || activeLabel;
Comment thread
cursor[bot] marked this conversation as resolved.

// 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.
Expand All @@ -142,8 +154,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<ComponentProps<typeof Image>["systemName"]>;
Expand Down Expand Up @@ -245,7 +255,9 @@ export function AgentActivity(
<Text
modifiers={[
font({ weight: "semibold", size: 13 }),
foregroundStyle(primaryForeground),
// The all-done header carries the outcome tint (emerald /
// red) the way the Done/Failed status labels do.
foregroundStyle(allDone ? headerTint : primaryForeground),
lineLimit(1),
]}
>
Expand Down Expand Up @@ -322,16 +334,17 @@ export function AgentActivity(
</Text>
),
// 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: (
<HStack spacing={5} alignment="center" modifiers={[padding({ leading: 4, vertical: 4 })]}>
{renderLogo(15, tint)}
<Text modifiers={[font({ weight: "bold", size: 13 }), foregroundStyle(tint)]}>
{`${props.activeCount}`}
{allDone ? doneLabel : `${props.activeCount}`}
</Text>
</HStack>
),
Expand Down
Loading