diff --git a/.changeset/fn-7252-file-browser-worktree-reload.md b/.changeset/fn-7252-file-browser-worktree-reload.md new file mode 100644 index 0000000000..9d3649ec96 --- /dev/null +++ b/.changeset/fn-7252-file-browser-worktree-reload.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Reload the selected file when switching Files modal worktrees. +category: fix +dev: Keeps Files modal selected-path state while changing workspace/worktree. diff --git a/packages/dashboard/app/components/FileBrowserModal.tsx b/packages/dashboard/app/components/FileBrowserModal.tsx index b99be00992..547e29ba22 100644 --- a/packages/dashboard/app/components/FileBrowserModal.tsx +++ b/packages/dashboard/app/components/FileBrowserModal.tsx @@ -228,12 +228,27 @@ export function FileBrowserModal({ setContent(originalContent); }, [originalContent, setContent]); + /* + FNXC:FileBrowser 2026-06-29-00:00: + Worktree switches must keep the last selected file path so users can recover from a load error caused by viewing the right path in the wrong workspace. The raw browser hook still resets each new workspace to root, so the modal re-points the sidebar to the selected file's parent directory after that reset while mobile keeps the editor pane active. + */ + const previousWorkspaceRef = useRef(currentWorkspace); + useEffect(() => { + const workspaceChanged = previousWorkspaceRef.current !== currentWorkspace; + if (workspaceChanged && selectedFile) { + setPath(getParentDirectory(selectedFile)); + if (isMobile) { + setMobileView("editor"); + } + } + previousWorkspaceRef.current = currentWorkspace; + }, [currentWorkspace, isMobile, selectedFile, setPath]); + const handleWorkspaceSelect = useCallback((workspace: string) => { setCurrentWorkspace(workspace); - setSelectedFile(null); - setMobileView("list"); + setMobileView(selectedFile ? "editor" : "list"); onWorkspaceChange?.(workspace); - }, [onWorkspaceChange]); + }, [onWorkspaceChange, selectedFile]); const persistSidebarWidth = useCallback((width: number) => { try { diff --git a/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx b/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx index c833990b06..6f8f632316 100644 --- a/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/FileBrowserModal.test.tsx @@ -230,6 +230,159 @@ describe("FileBrowserModal", () => { expect(mockOnWorkspaceChange).toHaveBeenCalledWith("FN-002"); }); + it("keeps an errored nested selected file open on desktop after switching workspaces", async () => { + const user = userEvent.setup(); + const nestedFile = "packages/dashboard/app/App.tsx"; + mockUseWorkspaceFileEditor.mockReturnValue({ + ...defaultEditorState, + error: "Could not load App.tsx", + }); + + render( + , + ); + + await waitFor(() => expect(screen.getByLabelText(`Editor for ${nestedFile}`)).toBeInTheDocument()); + expect(screen.getByText("Could not load App.tsx")).toBeInTheDocument(); + mockSetPath.mockClear(); + + await user.click(screen.getByRole("button", { name: /kb/i })); + await user.click(screen.getByRole("button", { name: /FN-002 Task Two/i })); + + await waitFor(() => { + expect(mockUseWorkspaceFileEditor).toHaveBeenLastCalledWith("FN-002", nestedFile, true, undefined); + }); + expect(screen.getByLabelText(`Editor for ${nestedFile}`)).toBeInTheDocument(); + expect(screen.getByText("Could not load App.tsx")).toBeInTheDocument(); + expect(document.querySelector(".file-browser-content.mobile")).toBeNull(); + expect(mockSetPath).toHaveBeenCalledWith("packages/dashboard/app"); + expect(mockOnWorkspaceChange).toHaveBeenCalledWith("FN-002"); + }); + + it("replaces an errored selected file with newly loaded content after switching workspaces", async () => { + const user = userEvent.setup(); + const nestedFile = "packages/dashboard/app/App.tsx"; + mockUseWorkspaceFileEditor.mockImplementation((workspace, filePath) => { + if (workspace === "FN-002" && filePath === nestedFile) { + return { + ...defaultEditorState, + content: "console.log('loaded from task worktree');", + originalContent: "console.log('loaded from task worktree');", + error: null, + }; + } + + if (filePath === nestedFile) { + return { + ...defaultEditorState, + content: "", + originalContent: "", + error: "Could not load App.tsx", + }; + } + + return defaultEditorState; + }); + + render( + , + ); + + await waitFor(() => expect(screen.getByText("Could not load App.tsx")).toBeInTheDocument()); + mockSetPath.mockClear(); + + await user.click(screen.getByRole("button", { name: /kb/i })); + await user.click(screen.getByRole("button", { name: /FN-002 Task Two/i })); + + await waitFor(() => { + expect(mockUseWorkspaceFileEditor).toHaveBeenLastCalledWith("FN-002", nestedFile, true, undefined); + }); + expect(screen.queryByText("Could not load App.tsx")).not.toBeInTheDocument(); + expect(document.querySelector(".cm-content")?.textContent).toContain("console.log('loaded from task worktree');"); + expect(mockSetPath).toHaveBeenCalledWith("packages/dashboard/app"); + }); + + it("keeps mobile on the errored nested selected file after switching workspaces", async () => { + Object.defineProperty(window, "innerWidth", { + writable: true, + configurable: true, + value: 375, + }); + const user = userEvent.setup(); + const nestedFile = "packages/dashboard/app/App.tsx"; + mockUseWorkspaceFileEditor.mockReturnValue({ + ...defaultEditorState, + error: "Could not load App.tsx", + }); + + render( + , + ); + + fireEvent(window, new Event("resize")); + await waitFor(() => expect(screen.getByLabelText("Back to file list")).toBeInTheDocument()); + expect(screen.getByLabelText(`Editor for ${nestedFile}`)).toBeInTheDocument(); + mockSetPath.mockClear(); + + await user.click(screen.getByRole("button", { name: /kb/i })); + await user.click(screen.getByRole("button", { name: /FN-002 Task Two/i })); + + await waitFor(() => { + expect(mockUseWorkspaceFileEditor).toHaveBeenLastCalledWith("FN-002", nestedFile, true, undefined); + }); + expect(screen.getByLabelText("Back to file list")).toBeInTheDocument(); + expect(screen.getByLabelText(`Editor for ${nestedFile}`)).toBeInTheDocument(); + expect(document.querySelector(".file-browser-content.mobile.active")).not.toBeNull(); + expect(document.querySelector(".file-browser-sidebar.mobile.active")).toBeNull(); + expect(mockSetPath).toHaveBeenCalledWith("packages/dashboard/app"); + expect(mockOnWorkspaceChange).toHaveBeenCalledWith("FN-002"); + }); + + it("keeps the placeholder after switching workspaces with no selected file", async () => { + const user = userEvent.setup(); + + render( + , + ); + + expect(screen.getByText("Select a file to edit")).toBeInTheDocument(); + mockUseWorkspaceFileEditor.mockClear(); + + await user.click(screen.getByRole("button", { name: /kb/i })); + await user.click(screen.getByRole("button", { name: /FN-002 Task Two/i })); + + await waitFor(() => { + expect(mockUseWorkspaceFileEditor).toHaveBeenLastCalledWith("FN-002", null, true, undefined); + }); + expect(screen.getByText("Select a file to edit")).toBeInTheDocument(); + expect(screen.queryByLabelText(/Editor for /)).not.toBeInTheDocument(); + expect(mockSetPath).not.toHaveBeenCalledWith(expect.stringContaining("packages/dashboard/app")); + expect(mockOnWorkspaceChange).toHaveBeenCalledWith("FN-002"); + }); + it("shows back button in mobile editor view", async () => { Object.defineProperty(window, "innerWidth", { writable: true, diff --git a/packages/dashboard/app/hooks/useWorkspaceFileBrowser.ts b/packages/dashboard/app/hooks/useWorkspaceFileBrowser.ts index 58419c165f..062beadfa6 100644 --- a/packages/dashboard/app/hooks/useWorkspaceFileBrowser.ts +++ b/packages/dashboard/app/hooks/useWorkspaceFileBrowser.ts @@ -39,6 +39,10 @@ export function useWorkspaceFileBrowser( setError(null); }, []); + /* + FNXC:FileBrowser 2026-06-29-19:35: + Workspace file pickers must start each workspace at root so SettingsModal directory/file pickers do not inherit editor selection state. FileBrowserModal restores its selected file path at the modal layer when it needs editor persistence across worktree switches. + */ useEffect(() => { setCurrentPath("."); setError(null);