fix: explain Boat admission waits and hide retries while paused

This commit is contained in:
Dotta committed 2026-10-10 22:39:58 -05:00
1 parent 2f03982a47
commit a761054e6a
4 files changed
+41 -16

No files matched your search

@@ -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(<><IssueMonitorBanner issue={issue} onCheckNow={vi.fn()} /><IssueMonitorComposerStrip issue={issue} onCheckNow={vi.fn()} /></>));
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();
+15 -3
View File
@@ -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 ? <CheckNowButton onCheckNow={onCheckNow} checkingNow={checkingNow} reviewRequested={copy.pullRequestReview} /> : null}
actions={onCheckNow && !copy.workspaceWait && !copy.computerWait ? <CheckNowButton onCheckNow={onCheckNow} checkingNow={checkingNow} reviewRequested={copy.pullRequestReview} /> : null}
>
{canCancel ? <CancelMonitorButton key={issue.id} onCancel={onCancelMonitor} /> : null}
<div className="flex flex-col gap-2">
@@ -306,12 +318,12 @@ export function IssueMonitorComposerStrip({
<div className="text-xs text-muted-foreground">{copy.stripMeta.join(" · ")}</div>
</div>
</div>
{onCheckNow && !copy.workspaceWait ? <CheckNowButton onCheckNow={onCheckNow} checkingNow={checkingNow} reviewRequested={copy.pullRequestReview} /> : null}
{onCheckNow && !copy.workspaceWait && !copy.computerWait ? <CheckNowButton onCheckNow={onCheckNow} checkingNow={checkingNow} reviewRequested={copy.pullRequestReview} /> : null}
</div>
<p className="mt-1.5 text-xs text-muted-foreground">
{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."}
</p>
+8
View File
@@ -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(
+12 -10
View File
@@ -7526,15 +7526,17 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS
? subTasksTree
: null}
<IssueMonitorBanner
key={issue.id}
onCancelMonitor={() => cancelIssueMonitor.mutateAsync()}
issue={issue}
workProducts={workProducts}
checkError={checkIssueMonitorNow.error?.message}
onCheckNow={() => checkIssueMonitorNow.mutate()}
checkingNow={checkIssueMonitorNow.isPending}
/>
{!activePauseHold ? (
<IssueMonitorBanner
key={issue.id}
onCancelMonitor={() => cancelIssueMonitor.mutateAsync()}
issue={issue}
workProducts={workProducts}
checkError={checkIssueMonitorNow.error?.message}
onCheckNow={() => checkIssueMonitorNow.mutate()}
checkingNow={checkIssueMonitorNow.isPending}
/>
) : null}
{taskChatShellEnabled ? null : (
<InlineEditor
@@ -8095,7 +8097,7 @@ export function TaskDetailSurface({ conversation, tasksTab }: { tasksTab?: TaskS
onRefreshLatestComments={refetchLatestComments}
composerRef={commentComposerRef}
composerAccessory={
hasVisibleMonitorSurface(issue) ? (
!activePauseHold && hasVisibleMonitorSurface(issue) ? (
<IssueMonitorComposerStrip
issue={issue}
workProducts={workProducts}