From beae2c673e45a8653158a776fb4e521ab049605a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 20 Jul 2026 18:40:03 -0700 Subject: [PATCH] fix(planning): make refinement session-driven Open refinement areas on demand with multi-select and custom focus support, then consume synchronous AI responses so restored sessions can continue reliably. Rename the primary review action to Proceed with plan and preserve the responsive Markdown review layout. --- .../improve-planning-refinement-menu.md | 7 + .../app/components/PlanningModeModal.css | 165 ++++++++++++---- .../app/components/PlanningModeModal.tsx | 177 ++++++++++++------ .../PlanningModeModal.initial.test.tsx | 2 +- .../PlanningModeModal.planning-flow.test.tsx | 58 ++++-- .../app/planning-browser-e2e-fixture.tsx | 36 +++- .../__tests__/planning-browser-e2e.test.ts | 32 ++-- .../planning-infinite-interview.test.ts | 1 + packages/dashboard/src/planning.ts | 4 +- 9 files changed, 345 insertions(+), 137 deletions(-) create mode 100644 .changeset/improve-planning-refinement-menu.md diff --git a/.changeset/improve-planning-refinement-menu.md b/.changeset/improve-planning-refinement-menu.md new file mode 100644 index 0000000000..e8ce4c4933 --- /dev/null +++ b/.changeset/improve-planning-refinement-menu.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Improve Planning Mode refinement and replace Validate with Proceed with plan. +category: fix +dev: Combines suggested and custom focus areas and restores questions from synchronous refinement responses. diff --git a/packages/dashboard/app/components/PlanningModeModal.css b/packages/dashboard/app/components/PlanningModeModal.css index 72efb97468..90ac07c295 100644 --- a/packages/dashboard/app/components/PlanningModeModal.css +++ b/packages/dashboard/app/components/PlanningModeModal.css @@ -1113,20 +1113,6 @@ margins so paragraphs and lists do not create unintended gaps. gap: var(--space-sm); } -.planning-refine-focus { - display: flex; - flex-direction: column; - gap: var(--space-sm); - margin: 0; - padding: 0; - border: 0; -} - -.planning-refine-focus legend { - margin-bottom: var(--space-sm); - font-weight: 500; -} - .planning-confirm-answer { display: flex; flex-direction: column; @@ -1210,9 +1196,9 @@ margins so paragraphs and lists do not create unintended gaps. /* FNXC:PlanningMode 2026-07-20-17:00: -The review checkpoint renders the same canonical Markdown that Validate persists as -plan.md. Keep the document and refinement controls inside the sole scroll owner while -the sibling action row remains reachable at the bottom of bounded desktop and mobile panes. +The review checkpoint renders the same canonical Markdown that Proceed persists as +plan.md. Keep the document inside the sole scroll owner while the sibling action row and +its refinement menu remain reachable at the bottom of bounded desktop and mobile panes. */ .planning-plan-scroll { align-items: center; @@ -1224,8 +1210,7 @@ the sibling action row remains reachable at the bottom of bounded desktop and mo gap: 0; } -.planning-plan-document, -.planning-plan-scroll > .planning-refine-focus { +.planning-plan-document { width: min(100%, calc(var(--space-2xl) * 24)); } @@ -1260,19 +1245,6 @@ the sibling action row remains reachable at the bottom of bounded desktop and mo font-size: var(--font-size-base); } -.planning-plan-scroll > .planning-refine-focus { - padding: var(--space-xl); - border-radius: var(--radius-xl); - background: var(--surface); - box-shadow: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--border) 75%, transparent); -} - -.planning-plan-scroll > .planning-refine-focus legend { - padding-inline: var(--space-xs); - font-size: var(--font-size-sm); - text-wrap: balance; -} - .planning-summary-qa-disclosure { margin-top: 0; } @@ -1468,6 +1440,7 @@ the sibling action row remains reachable at the bottom of bounded desktop and mo } .planning-plan-actions { + position: relative; justify-content: flex-end; gap: var(--space-md); padding: var(--space-lg) var(--space-xl); @@ -1478,6 +1451,118 @@ the sibling action row remains reachable at the bottom of bounded desktop and mo min-width: calc(var(--space-2xl) * 4); } +.planning-refine-menu { + position: absolute; + right: var(--space-xl); + bottom: calc(100% + var(--space-sm)); + z-index: var(--z-popover); + display: flex; + flex-direction: column; + gap: var(--space-lg); + width: min(calc(var(--space-2xl) * 14), calc(100vw - var(--space-2xl))); + max-height: min(70dvh, calc(var(--space-2xl) * 16)); + padding: var(--space-xl); + overflow-y: auto; + overscroll-behavior: contain; + border-radius: var(--radius-xl); + background: var(--surface); + box-shadow: var(--shadow-lg); + outline: none; +} + +.planning-refine-menu-header { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.planning-refine-menu-header h4, +.planning-refine-menu-header p { + margin: 0; +} + +.planning-refine-menu-header h4 { + font-size: var(--font-size-base); + text-wrap: balance; +} + +.planning-refine-menu-header p, +.planning-refine-menu-custom > span { + color: var(--text-muted); + font-size: var(--font-size-sm); + text-wrap: pretty; +} + +.planning-refine-menu-options { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-sm); +} + +.planning-refine-menu-option { + display: flex; + align-items: center; + gap: var(--space-sm); + min-height: calc(var(--space-2xl) + var(--space-md)); + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-lg); + background: var(--card); + box-shadow: 0 0 0 var(--btn-border-width) color-mix(in srgb, var(--border) 80%, transparent); + cursor: pointer; + transition: + background var(--transition-fast), + box-shadow var(--transition-fast), + transform var(--transition-fast); +} + +.planning-refine-menu-option:hover { + background: var(--card-hover); + box-shadow: var(--focus-ring); +} + +.planning-refine-menu-option:active { + transform: scale(0.96); +} + +.planning-refine-menu-option:has(input:checked) { + background: color-mix(in srgb, var(--todo) 12%, var(--surface)); + box-shadow: 0 0 0 var(--btn-border-width) var(--todo); +} + +.planning-refine-menu-option input { + flex-shrink: 0; + accent-color: var(--todo); +} + +.planning-refine-menu-option span { + min-width: 0; + color: var(--text); + font-size: var(--font-size-sm); + font-weight: 500; + overflow-wrap: anywhere; +} + +.planning-refine-menu-custom { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.planning-refine-menu-custom textarea { + min-height: calc(var(--space-2xl) * 2); + resize: vertical; +} + +.planning-refine-menu-actions { + display: flex; + justify-content: flex-end; + gap: var(--space-sm); +} + +.planning-refine-menu-actions .btn { + min-width: 0; +} + .planning-summary-actions-right { display: flex; gap: var(--space-sm); @@ -1491,10 +1576,6 @@ the sibling action row remains reachable at the bottom of bounded desktop and mo padding: var(--space-xl); } - .planning-plan-scroll > .planning-refine-focus { - padding: var(--space-lg); - } - .planning-plan-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -1507,6 +1588,18 @@ the sibling action row remains reachable at the bottom of bounded desktop and mo min-width: 0; min-height: calc(var(--space-2xl) + var(--space-md)); } + + .planning-refine-menu { + right: var(--space-lg); + left: var(--space-lg); + width: auto; + max-height: min(65dvh, calc(var(--space-2xl) * 14)); + padding: var(--space-lg); + } + + .planning-refine-menu-options { + grid-template-columns: 1fr; + } } /* diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 16283d92df..1cf4c972b3 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -559,11 +559,44 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat const isSessionListMode = showSessionList || (isCompactInterview && !mobileShowDetail); // FNXC:PlanningModeMobile 2026-07-20-10:30: Empty mobile state opens the composer because no saved destination exists; once sessions exist, every compact detail surface gets this single Back-to-list escape. const canReturnToSessionList = isCompactInterview && mobileShowDetail && planningSessions.length > 0; - const [refineFocus, setRefineFocus] = useState(""); - const [customRefineFocus, setCustomRefineFocus] = useState(null); + const [isRefineMenuOpen, setIsRefineMenuOpen] = useState(false); + const [selectedRefineFocuses, setSelectedRefineFocuses] = useState([]); + const [customRefineFocus, setCustomRefineFocus] = useState(""); + const refineMenuRef = useRef(null); + const refineTriggerRef = useRef(null); const { addToast } = useToast(); const { pushNav } = useNavigationHistoryContext(); + const combinedRefineFocus = useMemo( + () => [...selectedRefineFocuses, customRefineFocus.trim()].filter(Boolean).join(", "), + [customRefineFocus, selectedRefineFocuses], + ); + + useEffect(() => { + if (!isRefineMenuOpen) return; + refineMenuRef.current?.focus(); + + const handlePointerDown = (event: PointerEvent) => { + const target = event.target as Node; + if (!refineMenuRef.current?.contains(target) && !refineTriggerRef.current?.contains(target)) { + setIsRefineMenuOpen(false); + } + }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.stopImmediatePropagation(); + setIsRefineMenuOpen(false); + refineTriggerRef.current?.focus(); + }; + + document.addEventListener("pointerdown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [isRefineMenuOpen]); + /* FNXC:Planning 2026-06-23-02:00: Resizable Planning sidebar — pointer-drag + arrow-key resize with localStorage persistence, mirroring MissionManager.handleSidebarResizeStart/handleSidebarResizeKeyDown. Width is clamped to PLANNING_SIDEBAR_MIN/MAX and applied as an inline width on the sidebar