diff --git a/ui/src/components/IssueMonitorBanner.test.tsx b/ui/src/components/IssueMonitorBanner.test.tsx index 9b259f9a554..36e749e5570 100644 --- a/ui/src/components/IssueMonitorBanner.test.tsx +++ b/ui/src/components/IssueMonitorBanner.test.tsx @@ -235,6 +235,34 @@ describe("IssueMonitorBanner / IssueMonitorComposerStrip rendering", () => { flushSync(() => root.unmount()); }); + it.each([-120_000, 120_000])("keeps pending PR links visible during an active run (offset: %s)", offset => { + const issue = { ...issueWithMonitor(new Date(NOW.getTime() + offset).toISOString()), status: "in_progress" as const }; + issue.executionState!.monitor!.serviceName = "github"; + const product = { + id: "pr-1", type: "pull_request", title: "Update runtime probe", + url: "https://github.com/example/repo/pull/42", status: "ready_for_review", + reviewState: "needs_board_review", updatedAt: NOW, + } as unknown as IssueWorkProduct; + const root = createRoot(container); + const render = (hasActiveRun: boolean, products = [product]) => flushSync(() => root.render(<> + + + )); + try { + render(true); + expect(container.textContent).toContain("Monitor waits for current run"); + expect(Array.from(container.querySelectorAll("a")).map(link => link.href)).toEqual([product.url, product.url]); + expect(container.querySelector("button")).toBeNull(); + expect(container.textContent).toContain("Review on GitHub while the current run finishes saving its work."); + expect(container.textContent).not.toMatch(/overdue|next tick|then check status/i); + render(false); + expect(container.textContent).toContain("Pull request review requested"); + expect(Array.from(container.querySelectorAll("button")).map(button => button.textContent)).toEqual(["Check status", "Check status"]); + render(true, [{ ...product, metadata: { state: "merged" } }]); + expect(container.querySelector("a")).toBeNull(); + } finally { flushSync(() => root.unmount()); } + }); + it("explains automatic workspace waiting without promising that a reply bypasses the lock", () => { const issue = { status: "todo", scheduledRetry: { status: "scheduled_retry", scheduledRetryReason: "workspace_busy", scheduledRetryAt: NOW.toISOString() }, diff --git a/ui/src/components/IssueMonitorBanner.tsx b/ui/src/components/IssueMonitorBanner.tsx index ad1af571352..52193278153 100644 --- a/ui/src/components/IssueMonitorBanner.tsx +++ b/ui/src/components/IssueMonitorBanner.tsx @@ -118,6 +118,7 @@ export function buildMonitorSurfaceCopy( const eta = formatMonitorEta(derived.nextCheckAt, now); // "in 2h 12m" | "due now" | "overdue by 18m" const absolute = formatMonitorAbsolute(derived.nextCheckAt, {}, now); // local time, e.g. "Today, 4:08 PM" + const awaitingPullRequestReview = derived.source === "monitor" && derived.serviceName?.toLowerCase() === "github" && pendingPullRequestReviews > 0; if (derived.source === "monitor" && hasActiveRun) { const hint = "The check can start after the current run finishes saving its work."; return { @@ -128,9 +129,10 @@ export function buildMonitorSurfaceCopy( tone: "info", executionWait: true, checkUnavailable: true, + pullRequestReview: awaitingPullRequestReview, }; } - if (derived.source === "monitor" && derived.serviceName?.toLowerCase() === "github" && pendingPullRequestReviews > 0) { + if (awaitingPullRequestReview) { return { bannerTitle: "Pull request review requested", stripTitle: "Pull request review requested", @@ -351,7 +353,9 @@ export function IssueMonitorComposerStrip({ {!copy.automaticChecksPaused ? (

- {copy.pullRequestReview + {copy.pullRequestReview && copy.executionWait + ? "Review on GitHub while the current run finishes saving its work." + : copy.pullRequestReview ? "Review on GitHub, then check status to have the agent verify the result." : copy.workspaceWait || copy.executionWait ? "You can keep sending instructions while the agent waits."