From 7065d038954a61558f10275cb882d261f2094c38 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 26 Jul 2026 17:18:43 -0700 Subject: [PATCH] fix(dashboard): keep Planning Mode and Settings typable after floating-window migration FN-8606 declared the window shell as a component inside PlanningModeModal and SettingsModal render. A component declared in render is a new element type on every render, so React remounted the whole subtree on each keystroke, destroying the focused input: Planning Mode dropped everything after the first character and Settings text fields did the same. Replace ModalShell with a plain renderModalShell(children) call so the returned element types stay stable, and add a Planning regression test that types per-character across both the modal and embedded surfaces (fireEvent.change cannot observe this class of bug, which is why it shipped). Co-Authored-By: Claude Opus 5 (1M context) --- .../fix-planning-settings-composer-remount.md | 7 +++++++ .../app/components/PlanningModeModal.tsx | 15 +++++++++---- .../app/components/SettingsModal.tsx | 14 +++++++++---- .../PlanningModeModal.autosize.test.tsx | 21 +++++++++++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-planning-settings-composer-remount.md diff --git a/.changeset/fix-planning-settings-composer-remount.md b/.changeset/fix-planning-settings-composer-remount.md new file mode 100644 index 0000000000..5c42a8c581 --- /dev/null +++ b/.changeset/fix-planning-settings-composer-remount.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Planning Mode and Settings dropping typed text after the first character. +category: fix +dev: The FN-8606 floating-window migration declared `ModalShell` as a component inside `PlanningModeModal`/`SettingsModal` render, so each render produced a new element type and remounted the whole subtree, destroying the focused input. Replaced with a plain `renderModalShell(children)` call so element types stay stable. diff --git a/packages/dashboard/app/components/PlanningModeModal.tsx b/packages/dashboard/app/components/PlanningModeModal.tsx index 3431606a11..19177bd318 100644 --- a/packages/dashboard/app/components/PlanningModeModal.tsx +++ b/packages/dashboard/app/components/PlanningModeModal.tsx @@ -3540,7 +3540,14 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat ); /* FNXC:ModalTouchGeometry 2026-07-26-14:25: Preserve Planning Mode's pre-migration responsive desktop shell as the FloatingWindow seed so shared persistence does not shrink the surface. */ - const ModalShell = ({ children }: { children: ReactNode }) => isEmbedded ? ( + /* + FNXC:ModalTouchGeometry 2026-07-26-19:40: + The shell MUST be a plain render function, never a component declared inside this render. A nested component is a + brand-new element type on every render, so React unmounts and remounts the entire Planning subtree on each keystroke: + the composer textarea is recreated, loses DOM focus, and typing appears dead after the first character. Keep the + returned element types (div / FloatingWindow) stable by calling this directly. + */ + const renderModalShell = (children: ReactNode) => isEmbedded ? (
{children}
@@ -3564,8 +3571,8 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat ); - return ( - + return renderModalShell( + (
{/* FNXC:PlanningMode 2026-06-22-00:00: @@ -4250,7 +4257,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
-
+ ) ); } diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index eb45ea709b..0f02098b1e 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -4619,7 +4619,13 @@ export function SettingsModal({ FNXC:Settings 2026-06-22-00:00: Embedded settings is a main-content destination, not a dialog. It drops the fixed `.modal-overlay` backdrop and the inner card chrome (modal-overlay/modal/settings-modal classes), and instead uses `settings-embedded right-dock-embedded-view` (host) + `settings-modal--embedded` (panel) to fill the pane flush like other embedded views (Planning, Command Center). The modal path stays byte-identical. */ - const ModalShell = ({ children }: { children: ReactNode }) => isEmbedded ? ( + /* + FNXC:ModalTouchGeometry 2026-07-26-19:40: + The shell MUST stay a plain render function, never a component declared inside this render. A nested component is a + new element type on every render, so React remounts the whole Settings subtree on each keystroke and text inputs lose + focus after one character. Keep the returned element types (div / FloatingWindow) stable by calling this directly. + */ + const renderModalShell = (children: ReactNode) => isEmbedded ? (
); - return ( - + return renderModalShell( + <>
)} -
+ ); } diff --git a/packages/dashboard/app/components/__tests__/PlanningModeModal.autosize.test.tsx b/packages/dashboard/app/components/__tests__/PlanningModeModal.autosize.test.tsx index 706b1ec8dc..d0ba3b9894 100644 --- a/packages/dashboard/app/components/__tests__/PlanningModeModal.autosize.test.tsx +++ b/packages/dashboard/app/components/__tests__/PlanningModeModal.autosize.test.tsx @@ -186,6 +186,27 @@ describe("PlanningModeModal autosize", () => { expect(screen.getByText("Generating initial plan…")).toBeInTheDocument(); }); + /* + FNXC:PlanningMode 2026-07-26-19:40: + Typing into a Planning composer must keep the SAME textarea node mounted and focused. The FN-8606 floating-window + migration declared the modal/embedded shell as a component inside render, so every keystroke produced a new element + type, remounted the whole subtree, and dropped focus after one character — Planning Mode became untypable. Assert the + invariant with real per-character typing (fireEvent.change cannot see it) across both presentation surfaces. + */ + it.each(["modal", "embedded"] as const)("keeps the %s composer mounted and focused across keystrokes", async (presentation) => { + mockCreatePlanningDraft.mockReturnValue(new Promise(() => {})); + const user = userEvent.setup(); + render(); + + const textarea = screen.getByPlaceholderText(/Build a user authentication/i) as HTMLTextAreaElement; + await user.click(textarea); + await user.type(textarea, "auth flow"); + + expect(screen.getByPlaceholderText(/Build a user authentication/i)).toBe(textarea); + expect(textarea.value).toBe("auth flow"); + expect(document.activeElement).toBe(textarea); + }); + it("grows initial planning textarea and caps at max", async () => { // This assertion owns the blank composer; do not let its draft debounce replace it with a // session view while verifying the textarea's height contract.