fix(dashboard): dismiss popped-out/main-panel/dock task detail and Quick Chat on project switch

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 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-23 22:53:04 -07:00
parent ec535b5f3c
commit f9f2832936
4 changed files with 62 additions and 4 deletions

View File

@@ -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).

View File

@@ -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<string, () => 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,

View File

@@ -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([]);
});
});

View File

@@ -19,6 +19,13 @@ export interface UsePoppedOutTasksResult {
tasks: Array<Task | TaskDetail>;
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 };
}