mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
fix: explain Boat admission waits and hide retries while paused
This commit is contained in:
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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user