fix(ui): label stopped Swarm outcomes accurately (#149508)

Describe the existing combined failure, timeout, and cancellation count as failed or stopped. Keep authoritative totals and protocol unchanged, cover active and terminal summaries, and clarify the combined count in the Swarm docs.
This commit is contained in:
Peter Steinberger 2026-09-16 03:56:01 -07:00 • committed by GitHub
parent 7bcb0a2e8e
commit ca9c4d54d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 28 deletions

View file

@ -382,9 +382,10 @@ Keep the parent session open in Chat while a swarm is active. The Control UI and
native Android, iOS, and macOS chat surfaces show a compact Swarm progress widget
between the transcript and composer.
In the Control UI, cards show queued, running, completed, and failed counts with
visible status markers. Click or tap **Child details**, or activate it with the
keyboard, to expand available child names, status icons, and run durations. The
In the Control UI, cards show queued, running, completed, and **failed or stopped**
counts with visible status markers. The combined count includes failures, timeouts,
and cancelled children; the summary does not report these outcomes separately.
Click or tap **Child details**, or activate it with the keyboard, to expand available child names, status icons, and run durations. The
view shows up to four active groups plus the latest completed group, with an
explicit count when more groups are active. Each card displays at most 64 markers
and 64 child details. Its counts include every accepted group member.
@ -392,16 +393,16 @@ and 64 child details. Its counts include every accepted group member.
The latest completed group's counts remain visible after the children finish,
including when the parent fails before writing its final response. Groups whose
children all succeed use a compact completion row. Activate the row to expand
child details and the final-response reminder. Running, queued, and failed groups
keep their visible status markers and counts. These are
child outcomes, not confirmation that the parent produced a synthesis. Counts
child details and the final-response reminder. Groups with running, queued,
failed, or stopped children keep their visible status markers and counts. These
are child outcomes, not confirmation that the parent produced a synthesis. Counts
come from retained collector records, so reloading the page or cleaning up a
child session does not reduce the reported total. They expire with the existing
collector retention policy. This is not a permanent execution archive.
Native Android, iOS, and macOS chat surfaces still show active-only phase-grouped
grids, capped at 256 markers per phase with an overflow count. Accessible labels
identify each child's status. All clients present killed and timed-out children
identify each child's status. Native clients present killed and timed-out children
as failed. Native groups leave the widget when none of their children are queued
or running. The native widget disappears when no active groups remain.

View file

@ -2836,8 +2836,8 @@ export const en: TranslationMap & {
groupTitle: "Parallel tasks",
defaultPhase: "Unphased",
progress: "{complete} of {total}",
active: "{running} running · {queued} queued · {failed} failed",
finished: "{done} completed · {failed} failed",
active: "{running} running · {queued} queued · {failed} failed or stopped",
finished: "{done} completed · {failed} failed or stopped",
completed: "{done} completed",
childOutcome: "Child runs finished. Check the conversation for the final response.",
childOutcomeProcessing: "Child runs finished. The parent is processing their results.",

View file

@ -279,27 +279,53 @@ describe("chat Swarm progress", () => {
expect(container.querySelector("[data-test-id=chat-swarm]")).toBeNull();
});
it("keeps completed and failed child outcomes visible after the group finishes", () => {
const running = session({ key: "running", status: "running" });
const completed = session({ key: "completed", status: "done", hasActiveRun: true });
const failed = session({ key: "failed", status: "failed", hasActiveRun: true });
const container = renderProgress([running, completed, failed]);
it.each([
{ name: "failed", statuses: ["failed"], completed: 1 },
{ name: "stopped", statuses: Array.from({ length: 9 }, () => "killed" as const), completed: 0 },
{ name: "mixed", statuses: ["failed", "killed", "timeout"], completed: 1 },
] as const)(
"labels $name outcomes as a combined count through completion",
({ statuses, completed }) => {
const terminal = [
...Array.from({ length: completed }, (_, index) =>
session({ key: `completed-${index}`, status: "done", hasActiveRun: true }),
),
...statuses.map((status, index) =>
session({ key: `unsuccessful-${index}`, status, hasActiveRun: true }),
),
];
const container = renderProgress([
session({ key: "running", status: "running" }),
...terminal,
]);
const activeCounts = `1 running · 0 queued · ${statuses.length} failed or stopped`;
expect(container.querySelectorAll(".chat-swarm__task-icon--running")).toHaveLength(1);
expect(container.querySelectorAll(".chat-swarm__task-icon--done")).toHaveLength(1);
expect(container.querySelectorAll(".chat-swarm__task-icon--failed")).toHaveLength(1);
expect(container.querySelector(".chat-swarm__counts")?.textContent).toBe(activeCounts);
expect(container.querySelector(".chat-swarm__markers")?.getAttribute("aria-label")).toBe(
activeCounts,
);
expect(container.querySelectorAll(".chat-swarm__task-icon--running")).toHaveLength(1);
expect(container.querySelectorAll(".chat-swarm__task-icon--done")).toHaveLength(completed);
expect(container.querySelectorAll(".chat-swarm__task-icon--failed")).toHaveLength(
statuses.length,
);
render(
renderChatSwarmProgress({
sessionKey: parentSessionKey,
sessions: withSummary([completed, failed]),
}),
container,
);
expect(container.querySelector("[data-test-id=chat-swarm]")).not.toBeNull();
expect(container.textContent).toContain("1 completed · 1 failed");
expect(container.textContent).toContain("Check the conversation for the final response");
});
render(
renderChatSwarmProgress({
sessionKey: parentSessionKey,
sessions: withSummary(terminal),
}),
container,
);
const finishedCounts = `${completed} completed · ${statuses.length} failed or stopped`;
expect(container.querySelector("[data-test-id=chat-swarm]")).not.toBeNull();
expect(container.querySelector(".chat-swarm__counts")?.textContent).toBe(finishedCounts);
expect(container.querySelector(".chat-swarm__markers")?.getAttribute("aria-label")).toBe(
finishedCounts,
);
expect(container.textContent).toContain("Check the conversation for the final response");
},
);
it("says the parent is processing while child runs are finished but the parent is still active", () => {
const completed = session({ key: "completed", status: "done", hasActiveRun: true });