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."