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:
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
summary: Switching projects now fully resets Planning, Chat, Missions, subtask breakdown, GitHub import, and open modals.
|
summary: Switching projects now fully resets Planning, Chat, Missions, subtask breakdown, GitHub import, and open modals.
|
||||||
category: fix
|
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).
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ function AppInner() {
|
|||||||
FNXC:FloatingWindow 2026-07-15-15:20:
|
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.
|
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>());
|
const popupNavCloseRef = useRef(new Map<string, () => void>());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -959,6 +959,18 @@ function AppInner() {
|
|||||||
openSettings: modalManager.openSettings,
|
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 {
|
const {
|
||||||
handleSelectProject,
|
handleSelectProject,
|
||||||
handleViewAllProjects,
|
handleViewAllProjects,
|
||||||
@@ -985,7 +997,7 @@ function AppInner() {
|
|||||||
openSetupWizard: modalManager.openSetupWizard,
|
openSetupWizard: modalManager.openSetupWizard,
|
||||||
closeSetupWizard: modalManager.closeSetupWizard,
|
closeSetupWizard: modalManager.closeSetupWizard,
|
||||||
closeModelOnboarding: modalManager.closeModelOnboarding,
|
closeModelOnboarding: modalManager.closeModelOnboarding,
|
||||||
closeProjectScopedModals: modalManager.closeProjectScopedModals,
|
closeProjectScopedModals: closeProjectScopedUi,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { handleDetailClose } = useDeepLink({
|
const { handleDetailClose } = useDeepLink({
|
||||||
@@ -1522,6 +1534,24 @@ function AppInner() {
|
|||||||
}
|
}
|
||||||
}, [openTasksInRightSidebar, rightDock]);
|
}, [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 = {
|
const mainContentProps: MainContentProps = {
|
||||||
showBackendConnectionErrorPage,
|
showBackendConnectionErrorPage,
|
||||||
projectsError,
|
projectsError,
|
||||||
|
|||||||
@@ -60,4 +60,21 @@ describe("usePoppedOutTasks", () => {
|
|||||||
|
|
||||||
expect(result.current.entries).toEqual([{ task: task("1"), originTaskView: "board" }]);
|
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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ export interface UsePoppedOutTasksResult {
|
|||||||
tasks: Array<Task | TaskDetail>;
|
tasks: Array<Task | TaskDetail>;
|
||||||
popOut: (task: Task | TaskDetail, originTaskView?: TaskView, initialTab?: DetailTaskTab) => void;
|
popOut: (task: Task | TaskDetail, originTaskView?: TaskView, initialTab?: DetailTaskTab) => void;
|
||||||
close: (taskId: string, originTaskView?: TaskView) => 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 {
|
export function usePoppedOutTasks(): UsePoppedOutTasksResult {
|
||||||
@@ -44,11 +51,15 @@ export function usePoppedOutTasks(): UsePoppedOutTasksResult {
|
|||||||
setEntries((current) => current.filter((entry) => entry.task.id !== taskId || entry.originTaskView !== originTaskView));
|
setEntries((current) => current.filter((entry) => entry.task.id !== taskId || entry.originTaskView !== originTaskView));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const closeAll = useCallback(() => {
|
||||||
|
setEntries([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:TaskPopupViewGating 2026-07-15-15:20:
|
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.
|
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]);
|
const tasks = useMemo(() => entries.map((entry) => entry.task), [entries]);
|
||||||
|
|
||||||
return { entries, tasks, popOut, close };
|
return { entries, tasks, popOut, close, closeAll };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user