diff --git a/DESIGN.md b/DESIGN.md index 05d8865d46d..d11b7132122 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -118,9 +118,11 @@ recipient for a later draft must not change the response already starting. Runtime discovery must not replace the turn with a generic spinner or a "run started" receipt. Queued and running headers use the same spacing. -A queued turn shows `Queued`, or `Waiting for maintenance` while the server holds -admission. Reserve `Working` and `Thinking` for execution. A saved message that -starts this queued turn stays in history, including when it reopens an older +A queued turn shows the same `Working` header and shimmering `Thinking` activity +as a running turn, starting at message submission. Keep specific server holds, +such as `Waiting for maintenance` or agent setup, visible during admission. +This is presentation only; queue and execution authority stay unchanged. A saved +message that starts this queued turn stays in history, including when it reopens an older task. This placement is not a consumption receipt. Later follow-ups remain in the composer queue with their existing edit, order and discard controls. diff --git a/ui/src/components/TaskChatThread.test.tsx b/ui/src/components/TaskChatThread.test.tsx index 17d62058637..8c8e6041e26 100644 --- a/ui/src/components/TaskChatThread.test.tsx +++ b/ui/src/components/TaskChatThread.test.tsx @@ -573,7 +573,7 @@ it.each([true, false])("keeps an upload attached when a conversation draft becom expect(onAdd.mock.calls[0]?.[3]).toEqual([id]); }); -it("shows Queued during admission and clears it when the saved follow-up enters backoff", async () => { +it("shows activity during admission and clears it when the saved follow-up enters backoff", async () => { const pending = { id: "optimistic-recovery", companyId: "company", issueId: "issue", authorType: "user" as const, authorAgentId: null, authorUserId: "board", body: "Confirm the existing result.", presentation: null, metadata: null, createdAt: new Date(), updatedAt: new Date(), clientStatus: "pending" as const }; @@ -586,7 +586,9 @@ it("shows Queued during admission and clears it when the saved follow-up enters agentMap={new Map([["agent", { id: "agent", name: "Worker", adapterType: "paperclip_runner" } as Agent]])} />); show(false); expect(container.textContent).toContain("Confirm the existing result."); - expect(container.textContent).toContain("Queued"); + expect(container.textContent).toContain("Working"); + expect(container.textContent).toContain("Thinking"); + expect(container.textContent).not.toContain("Queued"); show(true); expect(container.textContent).not.toContain("Thinking"); expect(container.textContent).not.toContain("Working"); @@ -616,7 +618,9 @@ it.each((["task", "legacy"] as const).flatMap(surface => try { await act(async () => { send!.click(); }); expect(onAdd).toHaveBeenCalledTimes(1); - expect(container.textContent).toContain("Queued"); + expect(container.textContent).toContain("Working"); + expect(container.textContent).toContain("Thinking"); + expect(container.textContent).not.toContain("Queued"); const identity = container.querySelector('[data-testid="task-chat-agent-identity"]'); render(); expect(container.querySelector('[data-testid="task-chat-agent-identity"]')).toBe(identity); @@ -757,7 +761,9 @@ it.each([null, "previous-agent"].flatMap(previousAgentId => ["paperclip_runner", render(); await act(async () => acknowledge()); expect(container.querySelector('[data-testid="task-chat-agent-identity"]')).toBe(identity); - expect(container.textContent).toContain("Queued"); + expect(container.textContent).toContain("Working"); + expect(container.textContent).toContain("Thinking"); + expect(container.textContent).not.toContain("Queued"); // Choosing a recipient for the next draft does not change the sent request. await act(async () => container.querySelector('[data-testid="task-chat-composer-assignee"]')!.click()); await act(async () => document.querySelector('[data-assignee-identity="agent:previous-agent"]')!.closest("button")!.click()); @@ -1681,7 +1687,7 @@ describe("TaskChatThread runtime transcript selection", () => { const activity = container.querySelector('[data-testid="task-chat-current-activity-label"]'); expect(identity?.textContent).toBe("Runner"); expect(identity?.querySelector("svg, img")).not.toBeNull(); - expect(activity?.textContent).toBe("Queued"); + expect(activity?.textContent).toBe("Thinking"); renderThread(props); expect(container.querySelector('[data-testid="task-chat-agent-identity"]')).toBe(identity); @@ -1697,7 +1703,9 @@ describe("TaskChatThread runtime transcript selection", () => { expect(container.querySelector('[data-testid="task-chat-agent-identity"]')).toBe(identity); expect(container.querySelector('[data-testid="task-chat-runner-identity-row"]')).toBe(header); expect(container.querySelector('[data-testid="task-chat-current-activity-label"]')).toBe(activity); - expect(activity?.textContent).toBe(startup.status === "queued" ? "Queued" : "Thinking"); + expect(activity?.textContent).toBe("Thinking"); + expect(header?.textContent).toContain("Working"); + expect(container.textContent).not.toContain("Queued"); expect(container.querySelector('[data-testid="task-chat-live-run-pill"]')).toBeNull(); expect(container.textContent).not.toContain("run started"); } diff --git a/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx b/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx index 5fd1cc50f38..1c050084bbb 100644 --- a/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx +++ b/ui/src/components/task-chat/TaskChatRunnerTurn.test.tsx @@ -57,7 +57,7 @@ describe("TaskChatRunnerTurn", () => { status={status} execution={execution} preparationLabel={preparationLabel} - startedAtMs={Date.now() - 2_000} + startedAtMs={runId ? Date.now() - 2_000 : null} awaitingContinuation={awaitingContinuation} continuedAfterSteering={continuedAfterSteering} onRuntimeRequestDecision={onRuntimeRequestDecision} @@ -68,35 +68,46 @@ describe("TaskChatRunnerTurn", () => { ), ); - it("shows admission waiting instead of Thinking for a queued turn", () => { - render([], "queued"); - expect(container.querySelector('[data-testid="task-chat-turn-status-header"]')?.textContent).toContain("Queued"); - expect(container.querySelector('[data-testid="task-chat-current-activity-label"]')?.textContent).toBe("Queued"); - expect(container.textContent).not.toContain("Working"); - expect(container.textContent).not.toContain("Thinking"); - }); - - it("shows queued startup before a run ID or preparation label exists", () => { - render([], "queued", null); - expect(container.querySelector('[data-testid="task-chat-turn-status-header"]')?.textContent).toContain("Queued"); - expect(container.querySelector('[data-testid="task-chat-current-activity-label"]')?.textContent).toBe("Queued"); - expect(container.textContent).not.toContain("Thinking"); - }); + it.each([null, "run-1"].flatMap(runId => [false, true].map(projected => ({ runId, projected }))))( + "shows Working and Thinking during ordinary admission (runId=$runId, projected=$projected)", + ({ runId, projected }) => { + render([], "queued", runId, undefined, false, false, projected ? { + phase: "queued", label: "Queued", cause: null, + lastConfirmedActivityAt: null, retryAt: null, attempt: 1, maxAttempts: 3, + recoveryOwner: null, nextAction: null, permittedActions: ["inspect_run"], + predecessorRunId: null, successorRunId: null, + } : undefined); + expect(container.querySelector('[data-testid="task-chat-turn-status-header"]')?.textContent).toContain("Working"); + if (!runId) { + expect(container.querySelector('[data-testid="task-chat-turn-status-header"]')?.textContent).toBe("Working"); + } + const activity = container.querySelector('[data-testid="task-chat-current-activity-label"]'); + expect(activity?.textContent).toBe("Thinking"); + expect(activity?.classList.contains("shimmer-text")).toBe(true); + expect(container.textContent).not.toContain("Queued"); + }, + ); it("preserves a specific preparation label before a run ID exists", () => { render([], "queued", null, undefined, false, false, undefined, "Preparing workspace"); expect(container.querySelector('[data-testid="task-chat-current-activity-label"]')?.textContent).toBe("Preparing workspace"); }); - it("shows the server's maintenance wait while admission is held", () => { + it.each([ + ["task_drain", "Waiting for maintenance"], + ["agent_setup_pending", "Waiting for agent setup"], + ["agent_setup_failed", "Agent setup needs attention"], + ["server_restarting", "Waiting for server restart"], + ["previous_run_settling", "Waiting for previous run to stop"], + ])("shows the server's %s wait while admission is held", (cause, label) => { render([], "queued", "run-1", undefined, false, false, { - phase: "queued", label: "Waiting for maintenance", cause: "task_drain", + phase: "queued", label, cause, lastConfirmedActivityAt: null, retryAt: null, attempt: 1, maxAttempts: 3, recoveryOwner: null, nextAction: "Starts automatically when maintenance finishes.", permittedActions: ["inspect_run"], predecessorRunId: null, successorRunId: null, }); - expect(container.querySelector('[data-testid="task-chat-turn-status-header"]')?.textContent).toContain("Waiting for maintenance"); - expect(container.querySelector('[data-testid="task-chat-current-activity-label"]')?.textContent).toBe("Waiting for maintenance"); + expect(container.querySelector('[data-testid="task-chat-turn-status-header"]')?.textContent).toContain(label); + expect(container.querySelector('[data-testid="task-chat-current-activity-label"]')?.textContent).toBe(label); expect(container.textContent).not.toContain("Thinking"); }); diff --git a/ui/src/components/task-chat/TaskChatRunnerTurn.tsx b/ui/src/components/task-chat/TaskChatRunnerTurn.tsx index 3ad93e30966..dad7616ece8 100644 --- a/ui/src/components/task-chat/TaskChatRunnerTurn.tsx +++ b/ui/src/components/task-chat/TaskChatRunnerTurn.tsx @@ -94,7 +94,7 @@ function RunnerTurnStatus({ const label = terminal ? (failed ? "Stopped" : "Worked") : waitingLabel ?? "Working"; const suffix = terminal ? elapsed ? ` ${failed ? "after" : "for"} ${elapsed}` : "" - : waitingLabel ? elapsed ? ` · ${elapsed}` : "" : ` for ${elapsed ?? "0s"}`; + : elapsed ? `${waitingLabel ? " · " : " for "}${elapsed}` : ""; return (