From e4fb3f994e8c8425264aa3df857ddbf5366c1f57 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 25 Jul 2026 10:02:02 -0700 Subject: [PATCH] fix(dashboard): one Planning add-comment trigger, shown when the selection is done The plan review pane rendered both the document-adjacent and the action-rail "Add comment to selection" triggers, so operators saw duplicate buttons. Delete the document variant and its --document/--mobile CSS pair; the rail button is now the single control at every breakpoint. Selection capture also ran on every mid-drag selectionchange, which mounted and unmounted the trigger as the user dragged. Gate quote writes between pointerdown and pointerup inside the plan document so the control appears once, on release. Co-Authored-By: Claude Opus 5 (1M context) --- .../planning-single-add-comment-trigger.md | 7 ++ .../app/components/PlanningModeModal.css | 32 +++---- .../app/components/PlanningModeModal.tsx | 96 ++++++++++++------- .../__tests__/PlanningModeModal.css.test.ts | 22 +++-- .../PlanningModeModal.planning-flow.test.tsx | 54 +++++++++-- ...PlanningModeModal.ui-interactions.test.tsx | 24 +++-- .../__tests__/planning-browser-e2e.test.ts | 53 +++++----- 7 files changed, 188 insertions(+), 100 deletions(-) create mode 100644 .changeset/planning-single-add-comment-trigger.md diff --git a/.changeset/planning-single-add-comment-trigger.md b/.changeset/planning-single-add-comment-trigger.md new file mode 100644 index 0000000000..73fd33472a --- /dev/null +++ b/.changeset/planning-single-add-comment-trigger.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Planning Mode shows one "Add comment to selection" button, and only once the selection is finished. +category: fix +dev: Removes the `planning-add-comment--document` trigger and the `--mobile` modifier (single `.planning-add-comment` rail button at every breakpoint); `planSelectionDragActiveRef` suppresses quote writes between pointerdown and pointerup inside the plan document so mid-drag `selectionchange` no longer mounts/unmounts the trigger. diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index 0d605519cd..574769b637 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -1798,15 +1798,17 @@ Contextual comments remain transient review controls: render the capture/editor selection, and render the tray only after a valid comment exists so an empty plan review keeps its established Refine and Proceed action hierarchy. */ +/* +FNXC:PlanningComments 2026-07-25-10:20: +Single Add-comment trigger, living in the plan action rail at every breakpoint. The former +--document / --mobile variant pair is gone: both rendered in the DOM and read as duplicate buttons. +*/ .planning-add-comment { display: inline-flex; align-items: center; + justify-content: center; gap: var(--space-sm); - margin-top: var(--space-lg); -} - -.planning-add-comment--mobile { - display: none; + margin-top: 0; } .planning-comment-editor, @@ -1910,17 +1912,11 @@ reach past Proceed — match the tablet in-flow stack on phone too. } /* - FNXC:PlanningComments 2026-07-24-05:35: - Tablet and phone hide the document-adjacent trigger (it sits at the end of a long plan and - requires scrolling). The action-rail variant becomes the sole control and spans the full - footer width above Refine/Proceed so a selection always exposes Add comment without leaving - the plan action baseline. + FNXC:PlanningComments 2026-07-25-10:20: + Tablet and phone give the sole action-rail trigger the full footer width above Refine/Proceed so a + selection always exposes Add comment without leaving the plan action baseline. */ - .planning-add-comment--document { - display: none; - } - - .planning-plan-actions .btn.planning-add-comment--mobile { + .planning-plan-actions .btn.planning-add-comment { display: flex; grid-column: 1 / -1; margin-top: 0; @@ -1934,7 +1930,7 @@ reach past Proceed — match the tablet in-flow stack on phone too. tablet and phone share the mobile icon scale (default lucide size can dwarf the label in the full-width rail row). */ - .planning-plan-actions .btn.planning-add-comment--mobile svg { + .planning-plan-actions .btn.planning-add-comment svg { width: var(--space-lg); height: var(--space-lg); flex-shrink: 0; @@ -2089,7 +2085,7 @@ plan actions, and a token-sized bottom inset keep all three controls inline with gap: var(--space-md); } - .planning-plan-actions .btn.planning-add-comment--mobile { + .planning-plan-actions .btn.planning-add-comment { display: flex; grid-column: 1 / -1; width: 100%; @@ -2098,7 +2094,7 @@ plan actions, and a token-sized bottom inset keep all three controls inline with gap: var(--space-sm); } - .planning-plan-actions .btn.planning-add-comment--mobile svg { + .planning-plan-actions .btn.planning-add-comment svg { width: var(--space-lg); height: var(--space-lg); flex-shrink: 0; diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 11335906f4..9d934b565e 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -766,10 +766,17 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat const commentInputRef = useRef(null); const commentEditorRef = useRef(null); const addCommentTriggerRef = useRef(null); - const mobileAddCommentTriggerRef = useRef(null); const restoreCommentTriggerFocusRef = useRef(false); const isCommentEditorOpenRef = useRef(false); const pendingOpenCommentQuoteRef = useRef(null); + /* + FNXC:PlanningComments 2026-07-25-10:20: + While a pointer drag is extending a plan selection, selectionchange fires on every mouse move. + Mounting/unmounting the Add-comment control on each of those intermediate ranges made the button + strobe under the cursor on desktop. This ref suppresses quote writes for the duration of the drag + so the control appears exactly once, when the selection is done (pointerup/pointercancel). + */ + const planSelectionDragActiveRef = useRef(false); const setCommentEditorOpen = useCallback((open: boolean) => { /* @@ -818,12 +825,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat const focusAddCommentTrigger = useCallback(() => { /* - FNXC:PlanningComments 2026-07-24-05:35: - Tablet and phone both expose the action-rail trigger; only wide desktop uses the document - variant. Match the 1024px CSS gate so focus restore lands on the visible control. + FNXC:PlanningComments 2026-07-25-10:20: + One trigger at every breakpoint. The document-adjacent duplicate that sat at the end of the plan + text was removed (FN operator report: two "Add comment to selection" buttons), so focus restore + always targets the plan action rail control. */ - const usesRailTrigger = window.matchMedia?.("(max-width: 1024px)").matches ?? false; - (usesRailTrigger ? mobileAddCommentTriggerRef : addCommentTriggerRef).current?.focus(); + addCommentTriggerRef.current?.focus(); }, []); /* @@ -834,7 +841,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat if (isCommentEditorOpen || !restoreCommentTriggerFocusRef.current) return; restoreCommentTriggerFocusRef.current = false; queueMicrotask(() => { - if (addCommentTriggerRef.current || mobileAddCommentTriggerRef.current) { + if (addCommentTriggerRef.current) { focusAddCommentTrigger(); } }); @@ -2999,6 +3006,12 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat as the open gesture collapsed the native range. */ if (isCommentEditorOpenRef.current || pendingOpenCommentQuoteRef.current) return; + /* + FNXC:PlanningComments 2026-07-25-10:20: + Mid-drag ranges are not a finished selection. Skip them entirely; the pointerup handler runs one + final capture so the control shows once the selection is done instead of flickering per movement. + */ + if (planSelectionDragActiveRef.current) return; const selection = window.getSelection(); const root = planDocumentRef.current; @@ -3018,12 +3031,38 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat setSelectedPlanQuote(quote || null); }, []); + /* + FNXC:PlanningComments 2026-07-25-10:20: + Selection-in-progress gate. A drag that starts inside the plan document hides any stale control and + freezes quote updates until the pointer is released; the release (or cancel) performs the single + capture. Drags that start outside the plan are left alone — their collapse still clears the quote + through the normal selectionchange path. + */ useEffect(() => { + const handlePointerDown = (event: PointerEvent) => { + if (isCommentEditorOpenRef.current || pendingOpenCommentQuoteRef.current) return; + const root = planDocumentRef.current; + if (!root || !root.contains(event.target as Node)) return; + planSelectionDragActiveRef.current = true; + setSelectedPlanQuote(null); + }; + const handlePointerRelease = () => { + if (!planSelectionDragActiveRef.current) return; + planSelectionDragActiveRef.current = false; + capturePlanSelection(); + }; + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("pointerup", handlePointerRelease); + document.addEventListener("pointercancel", handlePointerRelease); document.addEventListener("selectionchange", capturePlanSelection); document.addEventListener("mouseup", capturePlanSelection); document.addEventListener("touchend", capturePlanSelection); document.addEventListener("keyup", capturePlanSelection); return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("pointerup", handlePointerRelease); + document.removeEventListener("pointercancel", handlePointerRelease); document.removeEventListener("selectionchange", capturePlanSelection); document.removeEventListener("mouseup", capturePlanSelection); document.removeEventListener("touchend", capturePlanSelection); @@ -3353,20 +3392,13 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat testId="planning-plan-markdown" /> - {selectedPlanQuote && !isCommentEditorOpen && ( - - )} + {/* + FNXC:PlanningComments 2026-07-25-10:20: + The document-adjacent Add-comment trigger was REMOVED. It duplicated the plan action rail + control (operators saw two "Add comment to selection" buttons) and, sitting at the end of a + long plan, it was the harder of the two to reach. The rail trigger below is now the single + control at every breakpoint. Do not reintroduce a second trigger inside the plan document. + */} {isCommentEditorOpen && openCommentQuote && (
{/* - FNXC:PlanningComments 2026-07-31-00:00: - FN-8533 keeps the selection-adjacent control on wide desktop, but compact shells need a - counterpart that cannot be lost under the document fold. - - FNXC:PlanningComments 2026-07-24-05:50: - On tablet and phone (≤1024) the rail control stays in the plan action footer as a - full-width row above Refine/Proceed so a selection never requires scrolling past the - action baseline. Document-level selectionchange still dismisses it when the selection - collapses. CSS shows exactly one of the two variants; only established 768px/1024px - breakpoint literals are allowed here, while all other dimensions remain design-token based. + FNXC:PlanningComments 2026-07-25-10:20: + The plan action rail holds the ONLY Add-comment trigger, at every breakpoint. It cannot be + lost under the document fold, and a selection never requires scrolling past the action + baseline. Document-level selectionchange still dismisses it when the selection collapses. FNXC:PlanningComments 2026-07-24-05:55: - Tablet must keep the two-column grid (not flex nowrap) so Add comment stays a full-width - first row with the same MessageSquarePlus 16px glyph as phone. + Tablet and phone keep the two-column grid (not flex nowrap) so Add comment stays a full-width + first row above Refine/Proceed with the same MessageSquarePlus 16px glyph. */} {selectedPlanQuote && !isCommentEditorOpen && (