| author | |
| committer | |
| log | a5b34b25378fbbf90ce36ee101a11528ccf11c7e |
| tree | 6d273b1b36097c4315657da883fb8672c4d73449 |
| parent | 56914128c1ff9dcf6585c380b7b25845d6ca5f98 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>3 files changed, 60 insertions(+), 12 deletions(-)
apps/web/src/components/ChatView.tsx+7-2| ... | ... | @@ -6859,7 +6859,7 @@ export default function ChatView(props: ChatViewProps) { |
| 6859 | 6859 | // The stack renders items[0] front-most and tucks the rest behind hover, so |
| 6860 | 6860 | // ordering is priority: system banners, then the branch-mismatch notice, |
| 6861 | 6861 | // and the informational parked-thread banner last — it must never cover another. |
| 6862 | // Background work (subagent fleets, workflow runs, watch loops) can outlive | |
| 6862 | // Background work (subagent fleets, workflow runs, watch loops, dev servers) can outlive | |
| 6863 | 6863 | // the turn; once it settles, the composer stop button is gone, so this |
| 6864 | 6864 | // banner is the only visible stop affordance. The interrupt path also |
| 6865 | 6865 | // accepts a completed run while its provider still has background work. |
| ... | ... | @@ -6907,9 +6907,14 @@ export default function ChatView(props: ChatViewProps) { |
| 6907 | 6907 | id: `background-work:${activeThread.id}`, |
| 6908 | 6908 | variant: "default", |
| 6909 | 6909 | priority: "activity", |
| 6910 | // A dev server can run for hours after the agent is done, so only work | |
| 6911 | // that will wake the agent pulses. | |
| 6910 | 6912 | icon: ( |
| 6911 | 6913 | <span |
| 6912 | className="size-1.5 animate-status-pulse rounded-full bg-foreground" | |
| 6914 | className={cn( | |
| 6915 | "size-1.5 rounded-full bg-foreground", | |
| 6916 | presentation.waiting && "animate-status-pulse", | |
| 6917 | )} | |
| 6913 | 6918 | aria-hidden="true" |
| 6914 | 6919 | /> |
| 6915 | 6920 | ), |
packages/client-runtime/src/state/threadExecution.test.ts+28-2| ... | ... | @@ -520,6 +520,7 @@ describe("presentPendingBackgroundWork", () => { |
| 520 | 520 | expect(presentation).toEqual({ |
| 521 | 521 | title: "Waiting on subagent Luna Window Properties", |
| 522 | 522 | items: [{ taskId: "luna", kind: "subagent", label: "Luna Window Properties", childThreadId }], |
| 523 | waiting: true, | |
| 523 | 524 | }); |
| 524 | 525 | }); |
| 525 | 526 | |
| ... | ... | @@ -544,12 +545,37 @@ describe("presentPendingBackgroundWork", () => { |
| 544 | 545 | { taskId: "a", kind: "subagent", description: "Review src/math.ts" }, |
| 545 | 546 | ])?.title, |
| 546 | 547 | ).toBe("Waiting on subagent Review src/math.ts"); |
| 547 | expect(presentPendingBackgroundWork([{ taskId: "a", kind: "command" }])?.title).toBe( | |
| 548 | "Waiting on a command", | |
| 548 | expect(presentPendingBackgroundWork([{ taskId: "a", kind: "monitor" }])?.title).toBe( | |
| 549 | "Waiting on a monitor", | |
| 549 | 550 | ); |
| 550 | 551 | expect(presentPendingBackgroundWork([])).toBeNull(); |
| 551 | 552 | }); |
| 552 | 553 | |
| 554 | // A command left running, such as a dev server, does not wake the agent. | |
| 555 | it("says only commands are running, not waited on", () => { | |
| 556 | expect( | |
| 557 | presentPendingBackgroundWork([ | |
| 558 | { taskId: "dev", kind: "command", description: "Start the shared dev server" }, | |
| 559 | ]), | |
| 560 | ).toMatchObject({ title: "Running: Start the shared dev server", waiting: false }); | |
| 561 | expect(presentPendingBackgroundWork([{ taskId: "a", kind: "command" }])).toMatchObject({ | |
| 562 | title: "Running a command", | |
| 563 | waiting: false, | |
| 564 | }); | |
| 565 | expect( | |
| 566 | presentPendingBackgroundWork([ | |
| 567 | { taskId: "a", kind: "command", description: "vp run dev" }, | |
| 568 | { taskId: "b", kind: "command", description: "tailscale serve" }, | |
| 569 | ]), | |
| 570 | ).toMatchObject({ title: "Running 2 commands", waiting: false }); | |
| 571 | expect( | |
| 572 | presentPendingBackgroundWork([ | |
| 573 | { taskId: "a", kind: "command", description: "vp run dev" }, | |
| 574 | { taskId: "b", kind: "monitor", description: "Watch PR checks" }, | |
| 575 | ]), | |
| 576 | ).toMatchObject({ title: "Waiting on 1 command and 1 monitor", waiting: true }); | |
| 577 | }); | |
| 578 | ||
| 553 | 579 | it("groups work by kind, subagents first, and keeps each name", () => { |
| 554 | 580 | const presentation = presentPendingBackgroundWork([ |
| 555 | 581 | { taskId: "cmd", kind: "command", description: "npm test" }, |
packages/client-runtime/src/state/threadExecution.ts+25-8| ... | ... | @@ -16,7 +16,10 @@ import { |
| 16 | 16 | orchestrationV2RunWorkStartedAt, |
| 17 | 17 | type ThreadId, |
| 18 | 18 | } from "@t3tools/contracts"; |
| 19 | import { derivePendingBackgroundWork } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; | |
| 19 | import { | |
| 20 | backgroundWorkHoldsCompletion, | |
| 21 | derivePendingBackgroundWork, | |
| 22 | } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; | |
| 20 | 23 | import { getProviderOptionCurrentLabel, getProviderOptionDescriptors } from "@t3tools/shared/model"; |
| 21 | 24 | import { formatDuration } from "@t3tools/shared/orchestrationTiming"; |
| 22 | 25 | import * as DateTime from "effect/DateTime"; |
| ... | ... | @@ -295,9 +298,17 @@ export interface PendingBackgroundWorkItem { |
| 295 | 298 | } |
| 296 | 299 | |
| 297 | 300 | export interface PendingBackgroundWorkPresentation { |
| 298 | /** "Waiting on subagent Review src/math.ts", "Waiting on 2 subagents and 1 command". */ | |
| 301 | /** | |
| 302 | * "Waiting on subagent Review src/math.ts", "Waiting on 2 subagents and 1 command", | |
| 303 | * or "Running: Start the dev server" when only commands remain. | |
| 304 | */ | |
| 299 | 305 | readonly title: string; |
| 300 | 306 | readonly items: ReadonlyArray<PendingBackgroundWorkItem>; |
| 307 | /** | |
| 308 | * True when the work will wake the agent (subagents, monitors). False when | |
| 309 | * only commands remain, such as a dev server: the agent is done. | |
| 310 | */ | |
| 311 | readonly waiting: boolean; | |
| 301 | 312 | } |
| 302 | 313 | |
| 303 | 314 | function joinWithAnd(parts: ReadonlyArray<string>): string { |
| ... | ... | @@ -305,11 +316,12 @@ function joinWithAnd(parts: ReadonlyArray<string>): string { |
| 305 | 316 | return `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`; |
| 306 | 317 | } |
| 307 | 318 | |
| 308 | /** Names what a settled thread is still waiting on, grouped by kind, for the composer strip. */ | |
| 319 | /** Names what a settled thread still runs, grouped by kind, for the composer strip. */ | |
| 309 | 320 | export function presentPendingBackgroundWork( |
| 310 | 321 | tasks: ReadonlyArray<OrchestrationV2PendingBackgroundTask>, |
| 311 | 322 | ): PendingBackgroundWorkPresentation | null { |
| 312 | 323 | if (tasks.length === 0) return null; |
| 324 | const waiting = backgroundWorkHoldsCompletion(tasks); | |
| 313 | 325 | const items = tasks |
| 314 | 326 | .map((task): PendingBackgroundWorkItem => { |
| 315 | 327 | const description = task.description?.trim(); |
| ... | ... | @@ -335,10 +347,15 @@ export function presentPendingBackgroundWork( |
| 335 | 347 | const [only] = items; |
| 336 | 348 | if (items.length === 1 && only !== undefined) { |
| 337 | 349 | const noun = BACKGROUND_WORK_KINDS[only.kind].singular; |
| 338 | return { | |
| 339 | title: only.label === noun ? `Waiting on a ${noun}` : `Waiting on ${noun} ${only.label}`, | |
| 340 | items, | |
| 341 | }; | |
| 350 | const named = only.label !== noun; | |
| 351 | const title = waiting | |
| 352 | ? named | |
| 353 | ? `Waiting on ${noun} ${only.label}` | |
| 354 | : `Waiting on a ${noun}` | |
| 355 | : named | |
| 356 | ? `Running: ${only.label}` | |
| 357 | : `Running a ${noun}`; | |
| 358 | return { title, items, waiting }; | |
| 342 | 359 | } |
| 343 | 360 | const counts = new Map<BackgroundWorkKind, number>(); |
| 344 | 361 | for (const item of items) counts.set(item.kind, (counts.get(item.kind) ?? 0) + 1); |
| ... | ... | @@ -346,7 +363,7 @@ export function presentPendingBackgroundWork( |
| 346 | 363 | const { singular, plural } = BACKGROUND_WORK_KINDS[kind]; |
| 347 | 364 | return `${count} ${count === 1 ? singular : plural}`; |
| 348 | 365 | }); |
| 349 | return { title: `Waiting on ${joinWithAnd(groups)}`, items }; | |
| 366 | return { title: `${waiting ? "Waiting on" : "Running"} ${joinWithAnd(groups)}`, items, waiting }; | |
| 350 | 367 | } |
| 351 | 368 | |
| 352 | 369 | /** The thread a notification row opens: that of the one subagent or delegated task it reports. */ |