From f9f283293624d790653d474a5b009f23ba69ef40 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 23 Jul 2026 22:53:04 -0700 Subject: [PATCH] fix(dashboard): dismiss popped-out/main-panel/dock task detail and Quick Chat on project switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task detail does not always render through modalManager.detailTask: with Open tasks as popups it opens as FloatingWindows, with the right-sidebar setting it opens in the right dock, and changes-tab opens use the main-panel task-detail view. None of those were closed by closeProjectScopedModals, so switching projects left the previous project's task windows over the new project — and the Quick Chat floating window (App state) stayed open the same way. useProjectActions now receives a composite App-level reset (via a stable ref, since the dock and main-panel pieces are constructed later in App) that closes project-scoped modals, all popped-out task windows (new usePoppedOutTasks.closeAll), main-panel task detail when present, the right-dock task, and Quick Chat. Co-Authored-By: Claude Fable 5 --- .changeset/project-switch-modal-reset.md | 2 +- packages/dashboard/app/App.tsx | 34 +++++++++++++++++-- .../hooks/__tests__/usePoppedOutTasks.test.ts | 17 ++++++++++ .../dashboard/app/hooks/usePoppedOutTasks.ts | 13 ++++++- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/.changeset/project-switch-modal-reset.md b/.changeset/project-switch-modal-reset.md index b9fcd8c3f4..5e09a16db2 100644 --- a/.changeset/project-switch-modal-reset.md +++ b/.changeset/project-switch-modal-reset.md @@ -4,4 +4,4 @@ summary: Switching projects now fully resets Planning, Chat, Missions, subtask breakdown, GitHub import, and open modals. category: fix -dev: New `closeProjectScopedModals()` on the modal manager, invoked by project select/view-all/setup-complete; PlanningModeModal, ChatView, MissionManager, SubtaskBreakdownModal, and GitHubImportModal are keyed by project id so running streams, session lists, and per-project persisted drafts/active sessions no longer leak or mis-file across projects (subtask/mission drafts save on unmount under their own project key). +dev: New `closeProjectScopedModals()` on the modal manager plus an App-level composite reset invoked by project select/view-all/setup-complete that also dismisses popped-out task FloatingWindows, main-panel task detail, the right-dock task, and the Quick Chat window; PlanningModeModal, ChatView, MissionManager, SubtaskBreakdownModal, and GitHubImportModal are keyed by project id so running streams, session lists, and per-project persisted drafts/active sessions no longer leak or mis-file across projects (subtask/mission drafts save on unmount under their own project key). diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 90ab54d603..f9deaba0f5 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -568,7 +568,7 @@ function AppInner() { FNXC:FloatingWindow 2026-07-15-15:20: FN-8016 identifies a popped-out task detail by task id plus origin view. The same task can therefore coexist in separate view-scoped FloatingWindows while re-opening it on one view refreshes only that entry. */ - const { entries: poppedOutTaskEntries, popOut: popOutTaskDetail, close: closePoppedOutTask } = usePoppedOutTasks(); + const { entries: poppedOutTaskEntries, popOut: popOutTaskDetail, close: closePoppedOutTask, closeAll: closeAllPoppedOutTasks } = usePoppedOutTasks(); const popupNavCloseRef = useRef(new Map void>()); /* @@ -959,6 +959,18 @@ function AppInner() { openSettings: modalManager.openSettings, }); + /* + FNXC:ProjectSwitchModalReset 2026-07-23-00:00: + Project-scoped task-detail surfaces are not all owned by modalManager: depending on + settings, tasks open as popped-out FloatingWindows, in the main panel (task-detail view), + or in the right dock, and Quick Chat is an App-level floating window. A project swap must + dismiss ALL of them, so useProjectActions receives this composite via a ref — the pieces + it closes (main-panel detail, right dock) are constructed later in this component, and + the ref keeps the callback stable while always invoking the latest wiring. + */ + const closeProjectScopedUiRef = useRef<() => void>(() => {}); + const closeProjectScopedUi = useCallback(() => closeProjectScopedUiRef.current(), []); + const { handleSelectProject, handleViewAllProjects, @@ -985,7 +997,7 @@ function AppInner() { openSetupWizard: modalManager.openSetupWizard, closeSetupWizard: modalManager.closeSetupWizard, closeModelOnboarding: modalManager.closeModelOnboarding, - closeProjectScopedModals: modalManager.closeProjectScopedModals, + closeProjectScopedModals: closeProjectScopedUi, }); const { handleDetailClose } = useDeepLink({ @@ -1522,6 +1534,24 @@ function AppInner() { } }, [openTasksInRightSidebar, rightDock]); + /* + FNXC:ProjectSwitchModalReset 2026-07-23-00:00: + The composite project-switch reset. Beyond modalManager's project-scoped modals, dismiss + every task-detail surface the open-task routing can target (popped-out FloatingWindows, + main-panel task-detail view, right-dock task) and close the Quick Chat floating window — + all of these showed the previous project's content over the new project. Assigned each + render so the ref-stable callback handed to useProjectActions always sees current state. + */ + closeProjectScopedUiRef.current = () => { + modalManager.closeProjectScopedModals(); + closeAllPoppedOutTasks(); + if (mainPanelDetailTask) { + closeTaskDetailMainPanel(); + } + rightDock.closeDockTask(); + setQuickChatOpen(false); + }; + const mainContentProps: MainContentProps = { showBackendConnectionErrorPage, projectsError, diff --git a/packages/dashboard/app/hooks/__tests__/usePoppedOutTasks.test.ts b/packages/dashboard/app/hooks/__tests__/usePoppedOutTasks.test.ts index dc56110bdd..d7162edfd7 100644 --- a/packages/dashboard/app/hooks/__tests__/usePoppedOutTasks.test.ts +++ b/packages/dashboard/app/hooks/__tests__/usePoppedOutTasks.test.ts @@ -60,4 +60,21 @@ describe("usePoppedOutTasks", () => { expect(result.current.entries).toEqual([{ task: task("1"), originTaskView: "board" }]); }); + + /* + FNXC:ProjectSwitchModalReset 2026-07-23-00:00: + A project swap dismisses every popped-out task window regardless of origin view — they + are task-detail surfaces for the previous project. + */ + it("closeAll dismisses every popped-out task across origin views", () => { + const { result } = renderHook(() => usePoppedOutTasks()); + + act(() => { + result.current.popOut(task("1"), "board"); + result.current.popOut(task("2"), "planning"); + result.current.closeAll(); + }); + + expect(result.current.entries).toEqual([]); + }); }); diff --git a/packages/dashboard/app/hooks/usePoppedOutTasks.ts b/packages/dashboard/app/hooks/usePoppedOutTasks.ts index 4f6f0ed393..54bc0547be 100644 --- a/packages/dashboard/app/hooks/usePoppedOutTasks.ts +++ b/packages/dashboard/app/hooks/usePoppedOutTasks.ts @@ -19,6 +19,13 @@ export interface UsePoppedOutTasksResult { tasks: Array; popOut: (task: Task | TaskDetail, originTaskView?: TaskView, initialTab?: DetailTaskTab) => void; close: (taskId: string, originTaskView?: TaskView) => void; + /* + FNXC:ProjectSwitchModalReset 2026-07-23-00:00: + Popped-out task windows are task-detail surfaces for the active project. A project swap + must dismiss them all — they bypass modalManager.detailTask, so closeProjectScopedModals + alone left the previous project's task popups floating over the new project. + */ + closeAll: () => void; } export function usePoppedOutTasks(): UsePoppedOutTasksResult { @@ -44,11 +51,15 @@ export function usePoppedOutTasks(): UsePoppedOutTasksResult { setEntries((current) => current.filter((entry) => entry.task.id !== taskId || entry.originTaskView !== originTaskView)); }, []); + const closeAll = useCallback(() => { + setEntries([]); + }, []); + /* FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 scopes popup identity to task id plus opening view. Every new pop-out has an origin; undefined origins are retained only for legacy snapshots and remain globally visible for compatibility. Closing receives the same identity so a task open on two views stays independent. */ const tasks = useMemo(() => entries.map((entry) => entry.task), [entries]); - return { entries, tasks, popOut, close }; + return { entries, tasks, popOut, close, closeAll }; }