From ffe319a3f7b94a44bc90ac204a3e824d1fabb6df Mon Sep 17 00:00:00 2001
From: Dotta
Date: Tue, 28 Apr 2026 22:34:52 -0500
Subject: [PATCH] PAP-2789 Simplify waiting banner Owner/Resume rows and fix
Jump anchor
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The explicit-wait IssueBlockedNotice rendered Target/Owner/Resume as a row
of pills. Drop the Target pill entirely and render the remaining Owner and
Resume entries as a plain dl/dt/dd block — the value matters more than the
chip styling for these long-form details.
The "Jump to questions/confirmation/approval" affordance referenced anchor
ids that did not exist in the DOM (`issue-thread-interaction-{id}` and
`issue-approval-{id}`); the actual anchors are `interaction-{id}` and
`approval-{id}`. Switch to those ids and use a button with
scrollIntoView({behavior:"smooth"}) so the jump actually scrolls to the
target.
Add a small ExplicitWaitingJumpAnchor component that flips the arrow ↑/↓
based on whether the target is above or below the banner, recomputing on
scroll/resize.
Co-Authored-By: Paperclip
---
ui/src/components/IssueBlockedNotice.test.tsx | 7 +-
ui/src/components/IssueBlockedNotice.tsx | 141 +++++++++---------
.../stories/status-language.stories.tsx | 6 +-
3 files changed, 76 insertions(+), 78 deletions(-)
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*);
+ expect(html).toContain(">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: [],