;
blockerAttention?: IssueBlockerAttention | null;
successfulRunHandoff?: SuccessfulRunHandoffState | null;
scheduledRetry?: IssueScheduledRetry | null;
@@ -198,6 +443,35 @@ export function IssueBlockedNotice({
);
};
+ // Blue "Waiting on live work" variant: the blocker chain is a healthy plan
+ // executing in order and something in it is live. `covered` is
+ // the only state that goes blue — stalled / needs_attention / none keep the
+ // amber notice byte-for-byte. The successful-run handoff notice is about this
+ // task's own finished run, so it always keeps its amber priority styling.
+ const liveIds = liveIssueIds ?? EMPTY_LIVE_IDS;
+ const chainBlockers = allBlockers ?? blockers;
+ const hasLiveWaitingBlocker = [...chainBlockers, ...terminalBlockers].some((blocker) => (
+ liveIds.has(blocker.id)
+ ));
+ const waitingOnLiveWork =
+ !showSuccessfulRunHandoff
+ && blockerAttention?.state === "covered"
+ && chainBlockers.length > 0
+ && hasLiveWaitingBlocker;
+
+ if (waitingOnLiveWork) {
+ return (
+
+ );
+ }
+
return (
{
});
});
+ it("renders the blue 'Waiting on live work' variant when the blocker chain is covered", () => {
+ const root = createRoot(container);
+
+ flushAct(() => {
+ root.render(
+
+ {}}
+ enableLiveTranscriptPolling={false}
+ />
+ ,
+ );
+ });
+
+ const notice = container.querySelector('[data-testid="issue-blocked-notice-live"]');
+ expect(notice).not.toBeNull();
+ expect(notice?.getAttribute("data-blocker-attention-state")).toBe("covered");
+ expect(container.textContent).toContain("Waiting on live work");
+ expect(container.textContent).toContain("resumes automatically when the chain is done");
+ // Progress counts: 1 done, 1 running out of 3.
+ expect(container.textContent).toContain("1 of 3 done · 1 running");
+ // Amber "Ultimately waiting on" / "blocked by the linked task" copy is gone.
+ expect(container.textContent).not.toContain("Ultimately waiting on");
+ expect(container.textContent).not.toContain("Work on this task is blocked by");
+ // All three blockers render as chips.
+ expect(container.querySelector('[data-issue-path-id="PAP-2001"]')).not.toBeNull();
+ expect(container.querySelector('[data-issue-path-id="PAP-2002"]')).not.toBeNull();
+ expect(container.querySelector('[data-issue-path-id="PAP-2003"]')).not.toBeNull();
+
+ flushAct(() => {
+ root.unmount();
+ });
+ });
+
+ it("shows a 'Now running' row replacing 'Ultimately waiting on' when the terminal leaf is live", () => {
+ const root = createRoot(container);
+
+ flushAct(() => {
+ root.render(
+
+ {}}
+ enableLiveTranscriptPolling={false}
+ />
+ ,
+ );
+ });
+
+ const nowRunning = container.querySelector('[data-testid="issue-blocked-notice-now-running"]');
+ expect(nowRunning).not.toBeNull();
+ expect(nowRunning?.textContent).toContain("Now running");
+ expect(nowRunning?.textContent).toContain("PAP-3002");
+ expect(container.textContent).not.toContain("Ultimately waiting on");
+
+ flushAct(() => {
+ root.unmount();
+ });
+ });
+
+ it("keeps the parked-work row amber inside the blue variant", () => {
+ const root = createRoot(container);
+
+ flushAct(() => {
+ root.render(
+
+ {}}
+ enableLiveTranscriptPolling={false}
+ />
+ ,
+ );
+ });
+
+ expect(container.querySelector('[data-testid="issue-blocked-notice-live"]')).not.toBeNull();
+ const parkedRow = container.querySelector('[data-testid="issue-blocked-notice-parked-row"]');
+ expect(parkedRow).not.toBeNull();
+ expect(parkedRow?.textContent).toContain("Blocked by parked work");
+ // Parked label keeps its amber tone even inside the blue box.
+ expect(parkedRow?.querySelector(".text-amber-800")).not.toBeNull();
+
+ flushAct(() => {
+ root.unmount();
+ });
+ });
+
+ it("keeps the amber notice byte-for-byte for stalled / needs_attention / none states", () => {
+ for (const state of ["stalled", "needs_attention", "none"] as const) {
+ const root = createRoot(container);
+
+ flushAct(() => {
+ root.render(
+
+ {}}
+ enableLiveTranscriptPolling={false}
+ />
+ ,
+ );
+ });
+
+ // Blue variant never appears for non-covered states.
+ expect(container.querySelector('[data-testid="issue-blocked-notice-live"]')).toBeNull();
+ // The amber container (with the canonical attention data attribute) does.
+ const amber = container.querySelector(`[data-blocker-attention-state="${state}"]`);
+ expect(amber).not.toBeNull();
+ expect(amber?.className).toContain("border-amber-300/70");
+ if (state === "stalled") {
+ expect(container.textContent).toContain("Stalled in review");
+ }
+
+ flushAct(() => {
+ root.unmount();
+ });
+ }
+ });
+
it("shows paused responsible agent context above the composer", () => {
const root = createRoot(container);
const pausedAgent = {
diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx
index 04f2199df3..11b5f0e4f9 100644
--- a/ui/src/components/IssueChatThread.tsx
+++ b/ui/src/components/IssueChatThread.tsx
@@ -422,6 +422,8 @@ interface IssueChatThreadProps {
activeRun?: ActiveRunForIssue | null;
issueId?: string | null;
blockedBy?: IssueRelationIssueSummary[];
+ /** Company-wide set of issue ids with a live (queued/running) run. */
+ liveIssueIds?: ReadonlySet;
blockerAttention?: IssueBlockerAttention | null;
successfulRunHandoff?: SuccessfulRunHandoffState | null;
scheduledRetry?: IssueScheduledRetry | null;
@@ -4164,6 +4166,7 @@ export function IssueChatThread({
activeRun = null,
issueId = null,
blockedBy = [],
+ liveIssueIds,
blockerAttention = null,
successfulRunHandoff = null,
scheduledRetry = null,
@@ -4931,6 +4934,8 @@ export function IssueChatThread({
issueId={issueId}
issueStatus={issueStatus}
blockers={unresolvedBlockers}
+ allBlockers={blockedBy}
+ liveIssueIds={liveIssueIds}
blockerAttention={blockerAttention}
successfulRunHandoff={recoveryAction ? null : successfulRunHandoff}
scheduledRetry={scheduledRetry}
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx
index b4a078a71d..9c53f284f1 100644
--- a/ui/src/pages/IssueDetail.tsx
+++ b/ui/src/pages/IssueDetail.tsx
@@ -887,6 +887,7 @@ type IssueDetailChatTabProps = {
issueWorkMode: IssueWorkMode;
executionRunId: string | null;
blockedBy: Issue["blockedBy"];
+ liveIssueIds: ReadonlySet;
blockerAttention: Issue["blockerAttention"] | null;
successfulRunHandoff: Issue["successfulRunHandoff"] | null;
scheduledRetry: Issue["scheduledRetry"] | null;
@@ -970,6 +971,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
issueStatus,
executionRunId,
blockedBy,
+ liveIssueIds,
blockerAttention,
successfulRunHandoff,
scheduledRetry,
@@ -1193,6 +1195,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
activeRun={resolvedActiveRun}
issueId={issueId}
blockedBy={blockedBy ?? []}
+ liveIssueIds={liveIssueIds}
blockerAttention={blockerAttention}
successfulRunHandoff={successfulRunHandoff}
scheduledRetry={scheduledRetry}
@@ -4664,6 +4667,7 @@ export function IssueDetail() {
issueWorkMode={issue.workMode ?? "standard"}
executionRunId={issue.executionRunId ?? null}
blockedBy={issue.blockedBy ?? []}
+ liveIssueIds={liveIssueIds}
blockerAttention={issue.blockerAttention ?? null}
successfulRunHandoff={issue.successfulRunHandoff ?? null}
scheduledRetry={issue.scheduledRetry ?? null}