diff --git a/ui/src/components/IssueMonitorBanner.test.tsx b/ui/src/components/IssueMonitorBanner.test.tsx index 5680dc963af..b6d3f0ba128 100644 --- a/ui/src/components/IssueMonitorBanner.test.tsx +++ b/ui/src/components/IssueMonitorBanner.test.tsx @@ -206,13 +206,16 @@ describe("IssueMonitorBanner / IssueMonitorComposerStrip rendering", () => { flushSync(() => root.unmount()); }); - it("explains automatic workspace waiting without promising that a reply bypasses the lock", () => { + it.each([ + ["workspace_busy", "Waiting for workspace"], + ["computer_admission_wait", "Connecting to computer"], + ])("explains automatic %s waiting without promising that a reply bypasses admission", (reason, title) => { const issue = { - status: "todo", scheduledRetry: { status: "scheduled_retry", scheduledRetryReason: "workspace_busy", scheduledRetryAt: NOW.toISOString() }, + status: "todo", scheduledRetry: { status: "scheduled_retry", scheduledRetryReason: reason, scheduledRetryAt: NOW.toISOString() }, } as Issue; const root = createRoot(container); flushSync(() => root.render(<>)); - expect(container.textContent).toContain("Waiting for workspace"); + expect(container.textContent).toContain(title); expect(container.textContent).toContain("You can keep sending instructions while the agent waits."); expect(container.textContent).not.toContain("wakes the agent now"); expect(container.querySelector("button")).toBeNull(); diff --git a/ui/src/components/IssueMonitorBanner.tsx b/ui/src/components/IssueMonitorBanner.tsx index 78f36b8ca11..13ecd2910b7 100644 --- a/ui/src/components/IssueMonitorBanner.tsx +++ b/ui/src/components/IssueMonitorBanner.tsx @@ -62,6 +62,7 @@ export interface MonitorSurfaceCopy { /** `warning` (amber) once overdue, `info` (blue) while still on schedule. */ tone: "info" | "warning"; workspaceWait?: boolean; + computerWait?: boolean; pullRequestReview?: boolean; } @@ -82,6 +83,17 @@ export function buildMonitorSurfaceCopy( ): MonitorSurfaceCopy | null { if (!isWaitingMonitorState(derived.state) || !derived.nextCheckAt) return null; + if (derived.source === "scheduled-retry" && scheduledRetryReason === "computer_admission_wait") { + return { + bannerTitle: "Connecting to computer", + stripTitle: "Connecting to computer", + bannerMeta: ["Work starts automatically when the computer is available."], + stripMeta: ["Work starts automatically when the computer is available."], + tone: "info", + computerWait: true, + }; + } + if (derived.source === "scheduled-retry" && scheduledRetryReason === "workspace_busy") { return { bannerTitle: "Waiting for workspace", @@ -262,7 +274,7 @@ export function IssueMonitorBanner({ icon={Clock} title={copy.bannerTitle} className={cn("relative my-3", canCancel && "pr-12")} - actions={onCheckNow && !copy.workspaceWait ? : null} + actions={onCheckNow && !copy.workspaceWait && !copy.computerWait ? : null} > {canCancel ? : null}
@@ -306,12 +318,12 @@ export function IssueMonitorComposerStrip({
{copy.stripMeta.join(" · ")}
- {onCheckNow && !copy.workspaceWait ? : null} + {onCheckNow && !copy.workspaceWait && !copy.computerWait ? : null}

{copy.pullRequestReview ? "Review on GitHub, then check status to have the agent verify the result." - : copy.workspaceWait + : copy.workspaceWait || copy.computerWait ? "You can keep sending instructions while the agent waits." : "Sending a reply wakes the agent now — before the scheduled check."}

diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx index 7249989b805..2f8f49d95cd 100644 --- a/ui/src/pages/IssueDetail.test.tsx +++ b/ui/src/pages/IssueDetail.test.tsx @@ -5647,6 +5647,12 @@ describe("IssueDetail", () => { createIssue({ status: "in_review", assigneeAgentId: "agent-1", + scheduledRetry: { + runId: "waiting-run", status: "scheduled_retry", agentId: "agent-1", + agentName: "Agent", retryOfRunId: "previous-wait", + scheduledRetryAt: new Date(Date.now() + 30_000), + scheduledRetryAttempt: 2, scheduledRetryReason: "computer_admission_wait", + }, }), ); mockIssuesApi.getTreeControlState.mockResolvedValue({ @@ -5683,6 +5689,8 @@ describe("IssueDetail", () => { expect(container.textContent).toContain("Task is paused."); expect(container.textContent).toContain("in_review"); expect(container.textContent).not.toContain("Subtree is paused."); + expect(container.textContent).not.toContain("Connecting to computer"); + expect(container.querySelector('[data-testid="issue-monitor-composer-strip"]')).toBeNull(); }); const resumeButton = Array.from(container.querySelectorAll("button")).find( diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index b7ebb693a9a..631eadd5371 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -7526,15 +7526,17 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS ? subTasksTree : null} - cancelIssueMonitor.mutateAsync()} - issue={issue} - workProducts={workProducts} - checkError={checkIssueMonitorNow.error?.message} - onCheckNow={() => checkIssueMonitorNow.mutate()} - checkingNow={checkIssueMonitorNow.isPending} - /> + {!activePauseHold ? ( + cancelIssueMonitor.mutateAsync()} + issue={issue} + workProducts={workProducts} + checkError={checkIssueMonitorNow.error?.message} + onCheckNow={() => checkIssueMonitorNow.mutate()} + checkingNow={checkIssueMonitorNow.isPending} + /> + ) : null} {taskChatShellEnabled ? null : (