diff --git a/ui/src/components/IssueChatThread.test.tsx b/ui/src/components/IssueChatThread.test.tsx index f84f183b765..9ffc0f4ba4c 100644 --- a/ui/src/components/IssueChatThread.test.tsx +++ b/ui/src/components/IssueChatThread.test.tsx @@ -84,6 +84,11 @@ const { })); // The session lookup never answers on its own; tests set the account directly. +vi.mock("@/context/CompanyContext", async (importOriginal) => ({ + ...(await importOriginal()), + useCompany: () => ({ selectedCompany: null }), +})); + vi.mock("../api/auth", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, authApi: { ...actual.authApi, getSession: () => new Promise(() => {}) } }; @@ -4468,6 +4473,52 @@ describe("IssueAssigneePausedNotice", () => { act(() => root.unmount()); }); + it.each([ + ["preparing", "computer is being prepared"], + ["verifying", "setup isn't finished"], + ["paused", "setup isn't finished"], + ])("shows a setup-draft agent (%s) as still being set up, not manually paused", (lifecycleState, detail) => { + const root = createRoot(container); + const agent = { ...pausedAgent("setup"), lifecycleState, urlKey: "ceo" } as unknown as Agent; + + act(() => { + root.render( + + + , + ); + }); + + const text = container.textContent ?? ""; + expect(text).not.toContain("paused manually"); + expect(text).not.toContain("is paused"); + expect(text).toContain("CEO is still being set up"); + expect(text.toLowerCase()).toContain(detail); + // Resuming a setup draft is not the recovery path; finishing setup is. + expect(container.querySelector('[data-testid="issue-assignee-paused-resume"]')).toBeNull(); + if (lifecycleState !== "preparing") { + const finish = container.querySelector('[data-testid="issue-assignee-finish-setup"]'); + expect(finish?.getAttribute("href")).toBe("/ONB/agents/ceo"); + } else { + expect(container.querySelector('[data-testid="issue-assignee-finish-setup"]')).toBeNull(); + } + + act(() => root.unmount()); + }); + + it("explains a company-archive pause instead of calling it manual", () => { + const root = createRoot(container); + act(() => { + root.render( + , + ); + }); + expect(container.textContent).not.toContain("paused manually"); + expect(container.textContent).toContain("company is archived"); + expect(container.querySelector('[data-testid="issue-assignee-paused-resume"]')).toBeNull(); + act(() => root.unmount()); + }); + it("offers no resume action for budget pauses", () => { const root = createRoot(container); diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index d7cf97d7bc0..bfcd2e18ba3 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -228,7 +228,7 @@ import { import { buildAgentMentionHref } from "@paperclipai/shared"; import { useComposerStop } from "@/hooks/useComposerStop"; import { useComposerSubmitKey } from "@/hooks/useComposerSubmitKey"; -import { cn, formatDateTime, formatShortDate } from "../lib/utils"; +import { agentUrl, cn, formatDateTime, formatShortDate } from "../lib/utils"; import { liveBlueBadge } from "../lib/status-colors"; import { nextWorkMode, @@ -821,6 +821,41 @@ export function IssueAssigneePausedNotice({ }) { if (!agent || agent.status !== "paused" || issueStatusIsTerminalDisposition(issueStatus)) return null; + // A freshly created agent is stored as paused with pauseReason "setup" until + // its setup finishes. That is not an operator pause, so say what is actually + // happening and point at the real recovery path instead of "Resume agent". + if (agent.pauseReason === "setup") { + // Only computer preparation advances on its own (matches NewAgentSetup). + // A "verifying" or parked draft waits for the user to finish the wizard. + const stillPreparing = ["preparing", "pausing"].includes(agent.lifecycleState ?? ""); + return ( +
+ + {agent.name} is still being set up. + + } + actions={ + stillPreparing ? undefined : ( + + ) + } + > + {stillPreparing + ? "Its computer is being prepared. Runs start automatically when setup finishes." + : "Its setup isn't finished yet. Finish setup to let it start runs."} + +
+ ); + } + const pauseDetail = agent.pauseReason === "budget" ? "It was paused by a budget hard stop." @@ -828,10 +863,15 @@ export function IssueAssigneePausedNotice({ ? "It arrived paused from an organization import — imported agents stay parked until you resume them." : agent.pauseReason === "system" ? "It was paused by the system." - : "It was paused manually."; - // Budget pauses clear on their own when the budget resets; resuming by hand - // would fight the hard stop, so the action is only offered for the rest. - const canResume = Boolean(onResume) && agent.pauseReason !== "budget"; + : agent.pauseReason === "company_archived" + ? "It was paused because its company is archived." + : agent.pauseReason === "manual" + ? "It was paused manually." + : ""; + // Budget pauses clear on their own when the budget resets and archive pauses + // clear when the company is restored; resuming by hand would fight either, so + // the action is only offered for the rest. + const canResume = Boolean(onResume) && agent.pauseReason !== "budget" && agent.pauseReason !== "company_archived"; return (
@@ -858,7 +898,7 @@ export function IssueAssigneePausedNotice({ ) : undefined } > - New runs will not start until the agent is resumed. {pauseDetail} + New runs will not start until the agent is resumed.{pauseDetail ? ` ${pauseDetail}` : ""}
);