Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 });
});
});
Expand All @@ -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.
Expand All @@ -2144,6 +2159,29 @@ 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 a failed thread in the inbox while a command still runs", () => {
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({
Expand Down
1 change: 1 addition & 0 deletions packages/client-runtime/src/state/threadInbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 13 additions & 3 deletions packages/client-runtime/src/state/threadInbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@ 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.runtime?.status !== "idle" &&
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 =
Expand Down
Loading