diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx
index 8bd26e21a04dc..162ea55b2b71e 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,29 +180,45 @@ 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 verb = presentation.waiting ? "Waiting on" : "Running";
+ 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 onlyThreadId = only?.childThreadId;
const startedAtMs = only?.startedAtMs ?? null;
return {
id: `background-work:${input.threadId}`,
- icon: rows.every(isCommand) ? : ,
- title: only ? (
+ icon: kinds.every((kind) => kind === "monitor") ? (
+
+ ) : kinds.includes("subagent") ? (
+
+ ) : (
+
+ ),
+ title: presentation.subject ? (
<>
- {verb} {only.task.label}
+ {presentation.subject.verb === null ? null : `${presentation.subject.verb} `}
+
+ {presentation.subject.item.label}
+
>
) : (
presentation.title
),
- description: only ? (
- only.activity
- ) : newest ? (
- `${newest.task.label}: ${newest.activity}`
- ) : rows.every(isCommand) ? (
- {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 : (
@@ -218,7 +235,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..7e8825d3c033b 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,44 @@ describe("presentRunStop", () => {
});
});
+describe("presentPendingBackgroundWork", () => {
+ const watch = "Watching pull request #404";
+ it.each([
+ { 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",
+ },
+ {
+ 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 }) => {
+ const presentation = presentPendingBackgroundWork([
+ { taskId: "bg-1", description, kind, ...(awaited ? { awaited } : {}) },
+ ]);
+ expect(presentation?.title).toBe(title);
+ expect(presentation?.subject).toMatchObject({ verb, item: { kind, label: description } });
+ },
+ );
+
+ it("leaves an unnamed task without a subject", () => {
+ expect(presentPendingBackgroundWork([{ taskId: "bg-1", kind: "command" }])).toMatchObject({
+ title: "Running a command",
+ subject: null,
+ });
+ });
+});
+
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..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;
}
@@ -392,14 +397,21 @@ 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}`;
- return { title, items, waiting };
+ // 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}`;
+ 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);
@@ -407,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 {