From b602268fedf0261d9c7769b3eb6c44e9a95b120b Mon Sep 17 00:00:00 2001 From: BeLazy167 Date: Sat, 3 Oct 2026 20:11:51 -0500 Subject: [PATCH 1/2] fix(client-runtime): background commands keep threads in Working --- apps/web/src/components/Sidebar.logic.test.ts | 47 +++++++++++++++++-- .../src/state/threadInbox.test.ts | 1 + .../client-runtime/src/state/threadInbox.ts | 12 +++-- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index bf2a0c0cbd6f..de170a006e40 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -2074,8 +2074,22 @@ describe("navigation after parking a thread", () => { describe("unseen completion with background work", () => { it.each([ - { kind: "command", status: "ready", topStatus: "done", receded: false, pill: "Completed" }, - { kind: "monitor", status: "waiting", topStatus: "waiting", receded: true, pill: "Waiting" }, + { + kind: "command", + status: "ready", + topStatus: "done", + receded: false, + working: true, + pill: "Completed", + }, + { + kind: "monitor", + status: "waiting", + topStatus: "waiting", + receded: true, + working: true, + pill: "Waiting", + }, ] as const)("presents a completed thread with a $kind roster", (expected) => { const thread = presentThreadShell(localEnvironmentId, { ...makeThreadFixture().source, @@ -2100,7 +2114,7 @@ describe("unseen completion with background work", () => { isSelected: false, }), ).toBe(expected.receded); - expect(isSidebarThreadWorking(thread)).toBe(expected.receded); + expect(isSidebarThreadWorking(thread)).toBe(expected.working); expect(resolveThreadStatusPill({ thread })).toMatchObject({ label: expected.pill }); }); }); @@ -2121,6 +2135,7 @@ describe("Working shelf (beta)", () => { hasPendingUserInput: false, interactionMode: "default" as const, latestRun: makeLatestRun(), + pendingBackgroundTasks: [], runtime: null, }; // Stopped with background tasks still open: V2's "waiting" sidebar status. @@ -2144,6 +2159,32 @@ describe("Working shelf (beta)", () => { ).toBe(false); }); + it("folds away every background kind, commands included", () => { + const completed = { ...runtime, status: "completed" as const }; + for (const kind of ["command", "monitor", "subagent", "background_task"] as const) { + expect( + isSidebarThreadWorking({ + ...idle, + runtime: completed, + pendingBackgroundTasks: [{ taskId: "bg-1", kind }], + }), + ).toBe(true); + } + }); + + it("keeps idle and failed threads without live work in the inbox", () => { + expect(isSidebarThreadWorking({ ...idle, runtime: { ...runtime, status: "idle" } })).toBe( + false, + ); + expect( + isSidebarThreadWorking({ + ...idle, + runtime: { ...runtime, status: "failed" as const, lastError: "boom" }, + pendingBackgroundTasks: [{ taskId: "bg-1", kind: "command" as const }], + }), + ).toBe(false); + }); + it("keeps a ready plan in the inbox while background work runs", () => { expect( isSidebarThreadWorking({ diff --git a/packages/client-runtime/src/state/threadInbox.test.ts b/packages/client-runtime/src/state/threadInbox.test.ts index 2ae5670933c7..b1bc4bc58884 100644 --- a/packages/client-runtime/src/state/threadInbox.test.ts +++ b/packages/client-runtime/src/state/threadInbox.test.ts @@ -12,6 +12,7 @@ function thread(id: string, working: boolean) { createdAt: "2026-06-01T00:00:00.000Z", unsettledAt: null, latestRun: null, + pendingBackgroundTasks: [], hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, diff --git a/packages/client-runtime/src/state/threadInbox.ts b/packages/client-runtime/src/state/threadInbox.ts index 0a7ed0918afe..75e7e65ad141 100644 --- a/packages/client-runtime/src/state/threadInbox.ts +++ b/packages/client-runtime/src/state/threadInbox.ts @@ -11,15 +11,21 @@ type WorkingThreadInput = Pick< | "hasPendingUserInput" | "interactionMode" | "latestRun" + | "pendingBackgroundTasks" | "runtime" >; /** Threads busy with work that does not need the user fold into the Working - section: a running run, or one stopped with background work that will wake - it. Approvals, questions, plan prompts, and failures stay in the inbox. */ + section: a running run, or one stopped with any background work still + live. Commands count here even though they do not hold completion: the + agent may be waiting on one, and a dev server is still visibly running. + Approvals, questions, plan prompts, and failures stay in the inbox. */ export function isThreadWorking(thread: WorkingThreadInput): boolean { if (thread.hasPendingApprovals || thread.hasPendingUserInput) return false; - if (!threadRuntimeIsActive(thread.runtime) && thread.runtime?.status !== "idle") return false; + if (thread.runtime?.status === "failed") return false; + if (!threadRuntimeIsActive(thread.runtime) && thread.pendingBackgroundTasks.length === 0) { + return false; + } // A plan prompt outranks lingering background work: the user has to act on it. const run = thread.latestRun; const runSettled = From cb010713a1158ae42f27fdf05b3c7f46c441d249 Mon Sep 17 00:00:00 2001 From: BeLazy167 Date: Sat, 3 Oct 2026 20:15:09 -0500 Subject: [PATCH 2/2] fix(client-runtime): keep idle handling unchanged --- apps/web/src/components/Sidebar.logic.test.ts | 5 +---- packages/client-runtime/src/state/threadInbox.ts | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index de170a006e40..f60016e2b7c1 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -2172,10 +2172,7 @@ describe("Working shelf (beta)", () => { } }); - it("keeps idle and failed threads without live work in the inbox", () => { - expect(isSidebarThreadWorking({ ...idle, runtime: { ...runtime, status: "idle" } })).toBe( - false, - ); + it("keeps a failed thread in the inbox while a command still runs", () => { expect( isSidebarThreadWorking({ ...idle, diff --git a/packages/client-runtime/src/state/threadInbox.ts b/packages/client-runtime/src/state/threadInbox.ts index 75e7e65ad141..df9f65ff34de 100644 --- a/packages/client-runtime/src/state/threadInbox.ts +++ b/packages/client-runtime/src/state/threadInbox.ts @@ -23,7 +23,11 @@ type WorkingThreadInput = Pick< export function isThreadWorking(thread: WorkingThreadInput): boolean { if (thread.hasPendingApprovals || thread.hasPendingUserInput) return false; if (thread.runtime?.status === "failed") return false; - if (!threadRuntimeIsActive(thread.runtime) && thread.pendingBackgroundTasks.length === 0) { + if ( + !threadRuntimeIsActive(thread.runtime) && + thread.runtime?.status !== "idle" && + thread.pendingBackgroundTasks.length === 0 + ) { return false; } // A plan prompt outranks lingering background work: the user has to act on it.