From a2e226991bda30bc8f9ff81df910409a07466da9 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:38:20 +0100 Subject: [PATCH 1/5] fix(web): background monitors read as their own sentence A monitor at the end of the timeline read "Waiting on Watching pull request #404" with a terminal icon and a monospace label. Monitors now show their own sentence in the sans font with an eye icon, and commands keep "Waiting on" with a monospace label. The task text uses the foreground colour; the verb, timer and Stop stay muted. The shared presenter now drops the verb for a named monitor whether or not it is awaited, so mobile no longer shows "Running: Watching ...". Co-authored-by: Claude Opus 5.5 --- .../components/chat/ThreadActivityBanners.tsx | 36 +++++++++++++++---- .../src/state/threadExecution.test.ts | 22 ++++++++++++ .../src/state/threadExecution.ts | 18 ++++++---- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx index 8bd26e21a04dc..fc7fcf5054e6f 100644 --- a/apps/web/src/components/chat/ThreadActivityBanners.tsx +++ b/apps/web/src/components/chat/ThreadActivityBanners.tsx @@ -15,6 +15,7 @@ import * as DateTime from "effect/DateTime"; import { CircleDashedIcon, CircleStopIcon, + EyeIcon, GitBranchIcon, MessageCircleIcon, ShieldQuestionIcon, @@ -179,17 +180,34 @@ export function backgroundWorkNowItem(input: { const newest = rows .filter((row) => row.activity) .toSorted((left, right) => right.updatedAtMs - left.updatedAtMs)[0]; - const isCommand = (row: (typeof rows)[number]) => row.task.kind !== "subagent"; + const isShellWork = (row: (typeof rows)[number]) => row.task.kind !== "subagent"; + // Monitors describe themselves in prose; commands are code. + const isMonitor = (row: (typeof rows)[number]) => row.task.kind === "monitor"; + const isCode = (row: (typeof rows)[number]) => isShellWork(row) && !isMonitor(row); const verb = presentation.waiting ? "Waiting on" : "Running"; const onlyThreadId = only?.childThreadId; const startedAtMs = only?.startedAtMs ?? null; return { id: `background-work:${input.threadId}`, - icon: rows.every(isCommand) ? : , + icon: rows.every(isMonitor) ? ( + + ) : rows.every(isShellWork) ? ( + + ) : ( + + ), title: only ? ( - <> - {verb} {only.task.label} - + // The presenter titles a task that reads as its own sentence with just its label. + presentation.title === only.task.label ? ( + {only.task.label} + ) : ( + <> + {verb}{" "} + + {only.task.label} + + + ) ) : ( presentation.title ), @@ -197,7 +215,7 @@ export function backgroundWorkNowItem(input: { only.activity ) : newest ? ( `${newest.task.label}: ${newest.activity}` - ) : rows.every(isCommand) ? ( + ) : rows.every(isCode) ? ( {rows.map((row) => row.task.label).join(", ")} ) : ( rows.map((row) => row.task.label).join(", ") @@ -218,7 +236,11 @@ export function backgroundWorkNowItem(input: { Open )} - void input.onStop()}> + void input.onStop()} + > {input.stopping ? "Stopping..." : "Stop"} diff --git a/packages/client-runtime/src/state/threadExecution.test.ts b/packages/client-runtime/src/state/threadExecution.test.ts index 3f8ef2a7f9d31..b412975d17992 100644 --- a/packages/client-runtime/src/state/threadExecution.test.ts +++ b/packages/client-runtime/src/state/threadExecution.test.ts @@ -25,6 +25,7 @@ import { deriveRunlessWorkStartedAt, deriveThreadActivityRun, deriveThreadRuntime, + presentPendingBackgroundWork, presentRunStop, threadRuntimeHasInterruptibleRun, } from "./threadExecution.ts"; @@ -328,6 +329,27 @@ describe("presentRunStop", () => { }); }); +describe("presentPendingBackgroundWork", () => { + const watch = "Watching pull request #404"; + it.each([ + { kind: "monitor", awaited: true, description: watch, title: watch }, + { kind: "monitor", awaited: undefined, description: watch, title: watch }, + { + kind: "command", + awaited: true, + description: "pnpm test", + title: "Waiting on command pnpm test", + }, + { kind: "command", awaited: undefined, description: "pnpm test", title: "Running: pnpm test" }, + ] as const)("titles a $kind with awaited $awaited", ({ kind, awaited, description, title }) => { + expect( + presentPendingBackgroundWork([ + { taskId: "bg-1", description, kind, ...(awaited ? { awaited } : {}) }, + ])?.title, + ).toBe(title); + }); +}); + describe("deriveRunlessWorkStartedAt", () => { const later = DateTime.makeUnsafe("2026-07-28T10:05:00.000Z"); const rootTurn = ( diff --git a/packages/client-runtime/src/state/threadExecution.ts b/packages/client-runtime/src/state/threadExecution.ts index 52d07babfbb61..4662b310cb33a 100644 --- a/packages/client-runtime/src/state/threadExecution.ts +++ b/packages/client-runtime/src/state/threadExecution.ts @@ -392,13 +392,17 @@ export function presentPendingBackgroundWork( if (items.length === 1 && only !== undefined) { const noun = BACKGROUND_WORK_KINDS[only.kind].singular; const named = only.label !== noun; - const title = waiting - ? named - ? `${only.kind === "monitor" ? "" : `Waiting on ${noun} `}${only.label}` - : `Waiting on a ${noun}` - : named - ? `Running: ${only.label}` - : `Running a ${noun}`; + // A named monitor describes itself in a sentence, so it gets no verb. + const title = + named && only.kind === "monitor" + ? only.label + : waiting + ? named + ? `Waiting on ${noun} ${only.label}` + : `Waiting on a ${noun}` + : named + ? `Running: ${only.label}` + : `Running a ${noun}`; return { title, items, waiting }; } const counts = new Map(); From ec7764a47d6959e8b26209e1ec39b18c878e8350 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:40:02 +0100 Subject: [PATCH 2/5] refactor(web): simplify background work row predicates Co-authored-by: Claude Opus 5.5 --- .../components/chat/ThreadActivityBanners.tsx | 17 ++++++++--------- .../src/state/threadExecution.test.ts | 1 - 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx index fc7fcf5054e6f..448cefee272f7 100644 --- a/apps/web/src/components/chat/ThreadActivityBanners.tsx +++ b/apps/web/src/components/chat/ThreadActivityBanners.tsx @@ -180,21 +180,20 @@ export function backgroundWorkNowItem(input: { const newest = rows .filter((row) => row.activity) .toSorted((left, right) => right.updatedAtMs - left.updatedAtMs)[0]; - const isShellWork = (row: (typeof rows)[number]) => row.task.kind !== "subagent"; + const kinds = rows.map((row) => row.task.kind); // Monitors describe themselves in prose; commands are code. - const isMonitor = (row: (typeof rows)[number]) => row.task.kind === "monitor"; - const isCode = (row: (typeof rows)[number]) => isShellWork(row) && !isMonitor(row); + const isCode = (kind: (typeof kinds)[number]) => kind !== "subagent" && kind !== "monitor"; const verb = presentation.waiting ? "Waiting on" : "Running"; const onlyThreadId = only?.childThreadId; const startedAtMs = only?.startedAtMs ?? null; return { id: `background-work:${input.threadId}`, - icon: rows.every(isMonitor) ? ( + icon: kinds.every((kind) => kind === "monitor") ? ( - ) : rows.every(isShellWork) ? ( - - ) : ( + ) : kinds.includes("subagent") ? ( + ) : ( + ), title: only ? ( // The presenter titles a task that reads as its own sentence with just its label. @@ -203,7 +202,7 @@ export function backgroundWorkNowItem(input: { ) : ( <> {verb}{" "} - + {only.task.label} @@ -215,7 +214,7 @@ export function backgroundWorkNowItem(input: { only.activity ) : newest ? ( `${newest.task.label}: ${newest.activity}` - ) : rows.every(isCode) ? ( + ) : kinds.every(isCode) ? ( {rows.map((row) => row.task.label).join(", ")} ) : ( rows.map((row) => row.task.label).join(", ") diff --git a/packages/client-runtime/src/state/threadExecution.test.ts b/packages/client-runtime/src/state/threadExecution.test.ts index b412975d17992..d26acd90835c3 100644 --- a/packages/client-runtime/src/state/threadExecution.test.ts +++ b/packages/client-runtime/src/state/threadExecution.test.ts @@ -340,7 +340,6 @@ describe("presentPendingBackgroundWork", () => { description: "pnpm test", title: "Waiting on command pnpm test", }, - { kind: "command", awaited: undefined, description: "pnpm test", title: "Running: pnpm test" }, ] as const)("titles a $kind with awaited $awaited", ({ kind, awaited, description, title }) => { expect( presentPendingBackgroundWork([ From dbd300d31ea137f21a07e69a477dafafd76d78b2 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:45:12 +0100 Subject: [PATCH 3/5] fix(web): style background work from the presenter's subject The line no longer compares title strings to decide on a verb, an unnamed task keeps the presenter's title, and a command keeps monospace in a mixed list. Co-authored-by: Claude Opus 5.5 --- .../components/chat/ThreadActivityBanners.tsx | 44 +++++++++---------- .../src/state/threadExecution.test.ts | 25 ++++++++--- .../src/state/threadExecution.ts | 17 ++++++- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx index 448cefee272f7..162ea55b2b71e 100644 --- a/apps/web/src/components/chat/ThreadActivityBanners.tsx +++ b/apps/web/src/components/chat/ThreadActivityBanners.tsx @@ -183,7 +183,6 @@ export function backgroundWorkNowItem(input: { const kinds = rows.map((row) => row.task.kind); // Monitors describe themselves in prose; commands are code. const isCode = (kind: (typeof kinds)[number]) => kind !== "subagent" && kind !== "monitor"; - const verb = presentation.waiting ? "Waiting on" : "Running"; const onlyThreadId = only?.childThreadId; const startedAtMs = only?.startedAtMs ?? null; return { @@ -195,30 +194,31 @@ export function backgroundWorkNowItem(input: { ) : ( ), - title: only ? ( - // The presenter titles a task that reads as its own sentence with just its label. - presentation.title === only.task.label ? ( - {only.task.label} - ) : ( - <> - {verb}{" "} - - {only.task.label} - - - ) + title: presentation.subject ? ( + <> + {presentation.subject.verb === null ? null : `${presentation.subject.verb} `} + + {presentation.subject.item.label} + + ) : ( presentation.title ), - description: only ? ( - only.activity - ) : newest ? ( - `${newest.task.label}: ${newest.activity}` - ) : kinds.every(isCode) ? ( - {rows.map((row) => row.task.label).join(", ")} - ) : ( - rows.map((row) => row.task.label).join(", ") - ), + description: only + ? only.activity + : newest + ? `${newest.task.label}: ${newest.activity}` + : rows.map((row, index) => ( + + {index > 0 ? ", " : null} + {row.task.label} + + )), actions: ( <> {startedAtMs === null ? null : ( diff --git a/packages/client-runtime/src/state/threadExecution.test.ts b/packages/client-runtime/src/state/threadExecution.test.ts index d26acd90835c3..05dc7813d8a43 100644 --- a/packages/client-runtime/src/state/threadExecution.test.ts +++ b/packages/client-runtime/src/state/threadExecution.test.ts @@ -332,20 +332,31 @@ describe("presentRunStop", () => { describe("presentPendingBackgroundWork", () => { const watch = "Watching pull request #404"; it.each([ - { kind: "monitor", awaited: true, description: watch, title: watch }, - { kind: "monitor", awaited: undefined, description: watch, title: watch }, + { kind: "monitor", awaited: true, description: watch, title: watch, verb: null }, + { kind: "monitor", awaited: undefined, description: watch, title: watch, verb: null }, { kind: "command", awaited: true, description: "pnpm test", title: "Waiting on command pnpm test", + verb: "Waiting on", }, - ] as const)("titles a $kind with awaited $awaited", ({ kind, awaited, description, title }) => { - expect( - presentPendingBackgroundWork([ + ] as const)( + "titles a $kind with awaited $awaited", + ({ kind, awaited, description, title, verb }) => { + const presentation = presentPendingBackgroundWork([ { taskId: "bg-1", description, kind, ...(awaited ? { awaited } : {}) }, - ])?.title, - ).toBe(title); + ]); + expect(presentation?.title).toBe(title); + expect(presentation?.subject?.verb).toBe(verb); + }, + ); + + it("leaves an unnamed task without a subject", () => { + expect(presentPendingBackgroundWork([{ taskId: "bg-1", kind: "command" }])).toMatchObject({ + title: "Running a command", + subject: null, + }); }); }); diff --git a/packages/client-runtime/src/state/threadExecution.ts b/packages/client-runtime/src/state/threadExecution.ts index 4662b310cb33a..05a3451215adb 100644 --- a/packages/client-runtime/src/state/threadExecution.ts +++ b/packages/client-runtime/src/state/threadExecution.ts @@ -352,6 +352,11 @@ export interface PendingBackgroundWorkItem { export interface PendingBackgroundWorkPresentation { readonly title: string; + /** The one named task, for a line that styles its label apart from the verb. */ + readonly subject: { + readonly verb: "Waiting on" | "Running" | null; + readonly item: PendingBackgroundWorkItem; + } | null; readonly items: ReadonlyArray; readonly waiting: boolean; } @@ -403,7 +408,10 @@ export function presentPendingBackgroundWork( : named ? `Running: ${only.label}` : `Running a ${noun}`; - return { title, items, waiting }; + const subject: PendingBackgroundWorkPresentation["subject"] = named + ? { verb: only.kind === "monitor" ? null : waiting ? "Waiting on" : "Running", item: only } + : null; + return { title, subject, items, waiting }; } const counts = new Map(); for (const item of items) counts.set(item.kind, (counts.get(item.kind) ?? 0) + 1); @@ -411,7 +419,12 @@ export function presentPendingBackgroundWork( const { singular, plural } = BACKGROUND_WORK_KINDS[kind]; return `${count} ${count === 1 ? singular : plural}`; }); - return { title: `${waiting ? "Waiting on" : "Running"} ${joinWithAnd(groups)}`, items, waiting }; + return { + title: `${waiting ? "Waiting on" : "Running"} ${joinWithAnd(groups)}`, + subject: null, + items, + waiting, + }; } export interface ProviderGoalPresentation { From 674611aea636e9b5370adb476957e504d5920dec Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:48:43 +0100 Subject: [PATCH 4/5] test(client-runtime): cover the running verb and the subject's task Co-authored-by: Claude Opus 5.5 --- .../client-runtime/src/state/threadExecution.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/threadExecution.test.ts b/packages/client-runtime/src/state/threadExecution.test.ts index 05dc7813d8a43..7e8825d3c033b 100644 --- a/packages/client-runtime/src/state/threadExecution.test.ts +++ b/packages/client-runtime/src/state/threadExecution.test.ts @@ -341,6 +341,13 @@ describe("presentPendingBackgroundWork", () => { title: "Waiting on command pnpm test", verb: "Waiting on", }, + { + kind: "command", + awaited: undefined, + description: "pnpm test", + title: "Running: pnpm test", + verb: "Running", + }, ] as const)( "titles a $kind with awaited $awaited", ({ kind, awaited, description, title, verb }) => { @@ -348,7 +355,7 @@ describe("presentPendingBackgroundWork", () => { { taskId: "bg-1", description, kind, ...(awaited ? { awaited } : {}) }, ]); expect(presentation?.title).toBe(title); - expect(presentation?.subject?.verb).toBe(verb); + expect(presentation?.subject).toMatchObject({ verb, item: { kind, label: description } }); }, ); From 25eeb77f1a02a701fddadc5e1eff5f18bfbb1a32 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:06:26 +0100 Subject: [PATCH 5/5] chore: credit the models that built this PR Co-authored-by: GPT 6.1 Sol