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.