diff --git a/ui/src/components/IssueBlockedNotice.test.tsx b/ui/src/components/IssueBlockedNotice.test.tsx index e41f1eac20a..b0a74c46e8d 100644 --- a/ui/src/components/IssueBlockedNotice.test.tsx +++ b/ui/src/components/IssueBlockedNotice.test.tsx @@ -147,7 +147,10 @@ describe("IssueBlockedNotice", () => { expect(html).toContain('data-blocker-attention-state="covered"'); expect(html).toContain('data-blocker-attention-reason="explicit_waiting"'); expect(html).toContain("Waiting on board confirmation"); - expect(html).toContain("Plan revision r2"); + expect(html).not.toContain("Plan revision r2"); + expect(html).not.toMatch(/>\s*Target\s*Owner<"); + expect(html).toContain(">Resume<"); expect(html).toContain("Resumes when the board accepts"); expect(html).toContain("border-sky-300/70"); expect(html).not.toContain("border-rose-300/70"); @@ -196,7 +199,7 @@ describe("IssueBlockedNotice", () => { expect(html).toContain("Waiting on board approval"); expect(html).toContain("A board approval is open"); - expect(html).toContain("Approval approval"); + expect(html).not.toContain("Approval approval"); expect(html).toContain("Jump to approval"); expect(html).toContain("border-sky-300/70"); }); diff --git a/ui/src/components/IssueBlockedNotice.tsx b/ui/src/components/IssueBlockedNotice.tsx index 89377af3a94..fe3dfd8fa46 100644 --- a/ui/src/components/IssueBlockedNotice.tsx +++ b/ui/src/components/IssueBlockedNotice.tsx @@ -8,7 +8,8 @@ import type { RequestConfirmationInteraction, SuggestTasksInteraction, } from "@paperclipai/shared"; -import { AlertTriangle, Hourglass } from "lucide-react"; +import { AlertTriangle, ArrowDown, ArrowUp, Hourglass } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { createIssueDetailPath } from "../lib/issueDetailBreadcrumb"; import { formatAssigneeUserLabel } from "../lib/assignees"; import { IssueLinkQuicklook } from "./IssueLinkQuicklook"; @@ -378,38 +379,6 @@ export function IssueBlockedNotice({ return <>This issue is on hold pending a human decision.; })(); - const explicitWaitingTargetLabel = (() => { - if (!pendingInteraction) { - if (pendingApproval) { - const id = pendingApproval.id; - return `Approval ${id.slice(0, 8)}`; - } - return null; - } - if (pendingInteraction.kind === "request_confirmation") { - const target = pendingInteraction.payload?.target ?? null; - if (target?.type === "issue_document" && target.key === "plan") { - const revLabel = target.revisionNumber != null ? `r${target.revisionNumber}` : "latest"; - return `Plan revision ${revLabel}`; - } - if (target?.label) return target.label; - if (target?.type === "custom" && target.key) return target.key; - return "Confirmation"; - } - if (pendingInteraction.kind === "ask_user_questions") { - const payload = pendingInteraction.payload as AskUserQuestionsPayload; - const count = payload?.questions?.length ?? 0; - if (count === 0) return "Questions"; - return `${count} ${count === 1 ? "question" : "questions"}`; - } - if (pendingInteraction.kind === "suggest_tasks") { - const count = pendingInteraction.payload?.tasks?.length ?? 0; - if (count === 0) return "Suggested tasks"; - return `${count} suggested ${count === 1 ? "task" : "tasks"}`; - } - return null; - })(); - const explicitWaitingResumeLabel = (() => { if (pendingInteraction) { const policy = pendingInteraction.continuationPolicy ?? "none"; @@ -432,37 +401,24 @@ export function IssueBlockedNotice({ return null; })(); - const explicitWaitingPills = (() => { + const explicitWaitingDetails = (() => { if (!showExplicitWaiting) return null; - const pillClass = - "inline-flex max-w-full items-center gap-1 rounded-md border border-sky-300/70 bg-white/80 px-1.5 py-0.5 text-[11px] font-medium text-sky-900 dark:border-sky-500/40 dark:bg-background/40 dark:text-sky-100"; - const labelClass = - "shrink-0 text-[10px] uppercase tracking-wide text-sky-700/80 dark:text-sky-300/80"; - const valueClass = "min-w-0 break-words"; - const pills: React.ReactNode[] = []; - if (explicitWaitingTargetLabel) { - pills.push( - - Target - {explicitWaitingTargetLabel} - , - ); - } - pills.push( - - Owner - {ownerLabel} - , + + const labelClass = "shrink-0 text-[10px] uppercase tracking-wide text-sky-700/80 dark:text-sky-300/80"; + return ( +
+
+
Owner
+
{ownerLabel}
+
+ {explicitWaitingResumeLabel ? ( +
+
Resume
+
{explicitWaitingResumeLabel}
+
+ ) : null} +
); - if (explicitWaitingResumeLabel) { - pills.push( - - Resume - {explicitWaitingResumeLabel} - , - ); - } - return
{pills}
; })(); const explicitWaitingJumpAnchor = (() => { @@ -470,23 +426,16 @@ export function IssueBlockedNotice({ let anchorId: string | null = null; let label: string | null = null; if (pendingInteraction) { - anchorId = `issue-thread-interaction-${pendingInteraction.id}`; + anchorId = `interaction-${pendingInteraction.id}`; if (pendingInteraction.kind === "request_confirmation") label = "Jump to confirmation"; else if (pendingInteraction.kind === "ask_user_questions") label = "Jump to questions"; else if (pendingInteraction.kind === "suggest_tasks") label = "Jump to suggestions"; } else if (pendingApproval) { - anchorId = `issue-approval-${pendingApproval.id}`; + anchorId = `approval-${pendingApproval.id}`; label = "Jump to approval"; } if (!anchorId || !label) return null; - return ( - - {label} ↓ - - ); + return ; })(); const ownerPill = (() => { @@ -557,7 +506,7 @@ export function IssueBlockedNotice({

{showExplicitWaiting ? ( <> - {explicitWaitingPills} + {explicitWaitingDetails} {isWaitChain && waitLeafBlockers.length > 0 ? (
Waiting at @@ -595,3 +544,49 @@ export function IssueBlockedNotice({
); } + +function ExplicitWaitingJumpAnchor({ anchorId, label }: { anchorId: string; label: string }) { + const buttonRef = useRef(null); + const [direction, setDirection] = useState<"up" | "down">("down"); + + useEffect(() => { + const button = buttonRef.current; + if (!button) return; + const update = () => { + const target = document.getElementById(anchorId); + if (!target) return; + const targetRect = target.getBoundingClientRect(); + const buttonRect = button.getBoundingClientRect(); + const targetMid = targetRect.top + targetRect.height / 2; + const buttonMid = buttonRect.top + buttonRect.height / 2; + setDirection(targetMid < buttonMid ? "up" : "down"); + }; + update(); + window.addEventListener("scroll", update, { passive: true, capture: true }); + window.addEventListener("resize", update); + return () => { + window.removeEventListener("scroll", update, { capture: true } as EventListenerOptions); + window.removeEventListener("resize", update); + }; + }, [anchorId]); + + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + const target = document.getElementById(anchorId); + if (!target) return; + target.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + + const Arrow = direction === "up" ? ArrowUp : ArrowDown; + return ( + + ); +} diff --git a/ui/storybook/stories/status-language.stories.tsx b/ui/storybook/stories/status-language.stories.tsx index 97f946d52a3..c25a66b8cfa 100644 --- a/ui/storybook/stories/status-language.stories.tsx +++ b/ui/storybook/stories/status-language.stories.tsx @@ -466,7 +466,7 @@ const blockedNoticeFixtures: BlockedNoticeFixture[] = [ }, { label: "Explicit wait (plan confirmation)", - caption: "A pending board confirmation against the latest plan revision — sky banner, target/owner/resume pills, jump anchor.", + caption: "A pending board confirmation against the latest plan revision — sky banner, owner/resume rows, jump anchor.", issueStatus: "in_review", issueIdentifier: "PAP-2708", blockers: [], @@ -505,7 +505,7 @@ const blockedNoticeFixtures: BlockedNoticeFixture[] = [ }, { label: "Explicit wait (user response)", - caption: "Pending ask_user_questions waiting on a specific user — sky banner with target = '2 questions'.", + caption: "Pending ask_user_questions waiting on a specific user — sky banner with owner/resume rows and a Jump to questions anchor.", issueStatus: "in_review", issueIdentifier: "PAP-2710", blockers: [], @@ -541,7 +541,7 @@ const blockedNoticeFixtures: BlockedNoticeFixture[] = [ }, { label: "Explicit wait (board approval)", - caption: "Pending board approval, no thread interaction — sky banner falls back to approval target/copy.", + caption: "Pending board approval, no thread interaction — sky banner falls back to board-approval headline and Jump to approval anchor.", issueStatus: "in_review", issueIdentifier: "PAP-2711", blockers: [],