From 6d210a995c79ec39058601cdb9dc6f0b08b1530b Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 14 Aug 2026 18:55:51 +0000 Subject: [PATCH] fix(ui): align a delivery notice to its own comment's body edge Two consecutive comments can each carry a delivery obligation. A full-thread -width banner under a right-aligned human bubble left ownership ambiguous, so the notice now tracks the message body edge on desktop while keeping the full thread width on mobile, and the thread's hash handling recognizes the feedback-delivery anchor so an attention deep link scrolls it into view. Browser-verified at 1440x900 and 390x844, light and dark: - no horizontal scroll at either width (scrollWidth == clientWidth) - mobile actions 308x44, desktop inline 36px - deep link focuses the banner heading inside the banner at both widths - spinner animation resolves to none under prefers-reduced-motion - contrast AA: light 4.70-4.74, dark 7.66-9.68 (baseline comment timestamp 4.74) - retry button focus-visible ring matches the app-wide 3px ring Co-Authored-By: Paperclip --- ui/src/components/FeedbackDeliveryState.tsx | 33 ++++++++++++++++++--- ui/src/components/IssueChatThread.tsx | 8 +++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/ui/src/components/FeedbackDeliveryState.tsx b/ui/src/components/FeedbackDeliveryState.tsx index e64829bbbd7..6e29cabd496 100644 --- a/ui/src/components/FeedbackDeliveryState.tsx +++ b/ui/src/components/FeedbackDeliveryState.tsx @@ -43,10 +43,26 @@ export function isIssueCommentFeedbackDelivery( } /** Anchor id for deep-linking the attention queue straight at the banner. */ +export const FEEDBACK_DELIVERY_ANCHOR_PREFIX = "feedback-delivery-"; + export function feedbackDeliveryAnchorId(commentId: string) { - return `feedback-delivery-${commentId}`; + return `${FEEDBACK_DELIVERY_ANCHOR_PREFIX}${commentId}`; } +/** + * Delay before claiming focus for a deep-linked banner. The thread's own + * hash handling makes up to three scroll attempts over ~250ms; focusing inside + * that window loses the focus to the thread viewport. + */ +const DEEP_LINK_FOCUS_SETTLE_MS = 350; + +/** + * Desktop constrains the notice to the same edge as the message body it belongs + * to, so ownership is unambiguous when consecutive comments each carry one. + * Mobile keeps the full thread width inside the thread's own padding. + */ +const FEEDBACK_DELIVERY_NOTICE_WIDTH = "w-full sm:max-w-(--pct-85)"; + function shortRunId(runId: string) { return runId.slice(0, 8); } @@ -235,13 +251,18 @@ export function FeedbackDeliveryNotice({ useEffect(() => { if (typeof window === "undefined") return; if (window.location.hash !== `#${anchorId}`) return; - headingRef.current?.focus(); + const timer = setTimeout(() => headingRef.current?.focus(), DEEP_LINK_FOCUS_SETTLE_MS); + return () => clearTimeout(timer); }, [anchorId]); if (delivery.state === "recovered_after_attention") { const deliveredAt = delivery.deliveredAt ? deliveredTimeLabel(delivery.deliveredAt) : null; return ( -
+
+
{/* Announced once on transition, not on every rerender of the banner. */} Feedback wasn't delivered. Paperclip stopped retrying. diff --git a/ui/src/components/IssueChatThread.tsx b/ui/src/components/IssueChatThread.tsx index 7ac0336ed08..7de1991593b 100644 --- a/ui/src/components/IssueChatThread.tsx +++ b/ui/src/components/IssueChatThread.tsx @@ -186,6 +186,7 @@ import { type RecoveryResolveOutcome, } from "./IssueRecoveryActionCard"; import { + FEEDBACK_DELIVERY_ANCHOR_PREFIX, FeedbackDeliveryFooterStatus, FeedbackDeliveryNotice, isIssueCommentFeedbackDelivery, @@ -1682,7 +1683,7 @@ function IssueChatUserMessage({
{feedbackDelivery ? (