c08d68c0fe
* refactor: re-key dockview panel state by environmentId instead of sessionId - Remove terminal from SESSION_SCOPED_COMPONENTS (uses useEnvironmentSessionId internally) - Convert ChangesContent and file-editor-panel to useEnvironmentSessionId() - Re-key sessionCommits store from bySessionId to byEnvironmentId - Re-key useCumulativeDiff cache by environmentId - Update WS handler to resolve envKey before invalidating cumulative diff cache - Update comments across portal manager, session switch, and layout files * fix: revert useEnvironmentSessionId in file-editor-panel file-editor is session-scoped and ephemeral — portal is destroyed on session switch, so useEnvironmentSessionId() has no practical effect. * fix: address CodeRabbit review comments - Pass appStore to setupPortalCleanup so terminal tab close still calls stopUserShell via activeSessionId fallback (entry.sessionId is now undefined for global panels) - Add request version guard to useCumulativeDiff to discard stale responses after environment switches * fix: scope activeSessionId fallback to terminal only in portal cleanup vscode is session-scoped and always has entry.sessionId — the activeSessionId fallback should only apply to terminal (now global). * fix: migrate stranded env-keyed data on registerSessionEnvironment When registerSessionEnvironment(sessionId, environmentId) is called, any data previously stored under the fallback sessionId key is now migrated to the proper environmentId key. This prevents data from "vanishing" when selectors start reading by environmentId after the mapping is registered. Applies to: sessionCommits, gitStatus, shell outputs/statuses, userShells. * fix: extract migrateEnvKeyedData helper to fix lint max-lines * fix: address CodeRabbit review comments - Always delete orphaned sessionId key in migrateEnvKeyedData even when environmentId key already exists (prevents stale accumulation) - Reset loading state when switching to an envKey with warm cache to avoid brief spinner flash * fix: always reset loading on envKey switch in useCumulativeDiff Prevents stuck loading state on rapid A→B→A env switches where the stale in-flight response is discarded by version guard but loading is never reset. * fix: move sync effect before fetch effect in useCumulativeDiff React 18 batches effects in declaration order. With the sync effect after the fetch effect, setLoading(false) from the sync overrode setLoading(true) from the fetch on cold-cache env switches. Moving the sync effect first ensures the fetch effect's setLoading(true) wins the batch. * test: add migrateEnvKeyedData coverage for registerSessionEnvironment Tests 4 cases: normal migration, don't-clobber existing data, no-op when sessionId equals environmentId, and partial sub-key migration. * fix: close stale-diff gaps in useCumulativeDiff sync effect - Bump requestVersionRef on envKey change so in-flight fetches for the previous envKey are discarded - Clear loadingState[envKey] so the fetch effect isn't blocked on re-entry (A→B→A scenario)
116 lines
4.5 KiB
TypeScript
116 lines
4.5 KiB
TypeScript
import type { DockviewReadyEvent } from "dockview-react";
|
|
import type { StoreApi } from "zustand";
|
|
import type { AppState } from "@/lib/state/store";
|
|
import { useDockviewStore } from "@/lib/state/dockview-store";
|
|
import { getRootSplitview } from "@/lib/state/dockview-layout-builders";
|
|
import { setSessionLayout } from "@/lib/local-storage";
|
|
import { panelPortalManager } from "@/lib/layout/panel-portal-manager";
|
|
import { stopVscode } from "@/lib/api/domains/vscode-api";
|
|
import { stopUserShell } from "@/lib/api/domains/user-shell-api";
|
|
|
|
const LAYOUT_STORAGE_KEY = "dockview-layout-v1";
|
|
|
|
function trackPinnedWidths(api: DockviewReadyEvent["api"]): void {
|
|
if (useDockviewStore.getState().isRestoringLayout) return;
|
|
if (api.hasMaximizedGroup() || useDockviewStore.getState().preMaximizeLayout !== null) return;
|
|
const sv = getRootSplitview(api);
|
|
if (!sv || sv.length < 2) return;
|
|
try {
|
|
const sidebarW = sv.getViewSize(0);
|
|
if (sidebarW > 50) {
|
|
const current = useDockviewStore.getState().pinnedWidths.get("sidebar");
|
|
if (current !== sidebarW) {
|
|
useDockviewStore.getState().setPinnedWidth("sidebar", sidebarW);
|
|
}
|
|
}
|
|
if (sv.length >= 3) {
|
|
const rightIdx = sv.length - 1;
|
|
const rightW = sv.getViewSize(rightIdx);
|
|
if (rightW > 50) {
|
|
const current = useDockviewStore.getState().pinnedWidths.get("right");
|
|
if (current !== rightW) {
|
|
useDockviewStore.getState().setPinnedWidth("right", rightW);
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
/* noop */
|
|
}
|
|
}
|
|
|
|
export function setupGroupTracking(api: DockviewReadyEvent["api"]): () => void {
|
|
const d1 = api.onDidActiveGroupChange((group) => {
|
|
useDockviewStore.setState({ activeGroupId: group?.id ?? null });
|
|
});
|
|
useDockviewStore.setState({ activeGroupId: api.activeGroup?.id ?? null });
|
|
const d2 = api.onDidLayoutChange(() => trackPinnedWidths(api));
|
|
trackPinnedWidths(api);
|
|
return () => {
|
|
d1.dispose();
|
|
d2.dispose();
|
|
};
|
|
}
|
|
|
|
export function setupLayoutPersistence(
|
|
api: DockviewReadyEvent["api"],
|
|
saveTimerRef: React.MutableRefObject<ReturnType<typeof setTimeout> | null>,
|
|
sessionIdRef: React.MutableRefObject<string | null>,
|
|
): void {
|
|
api.onDidLayoutChange(() => {
|
|
if (useDockviewStore.getState().isRestoringLayout) return;
|
|
|
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
saveTimerRef.current = setTimeout(() => {
|
|
try {
|
|
const json = api.toJSON();
|
|
localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(json));
|
|
const sid = sessionIdRef.current;
|
|
if (sid) {
|
|
setSessionLayout(sid, json);
|
|
}
|
|
} catch {
|
|
// Ignore serialization errors
|
|
}
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
export function setupPortalCleanup(
|
|
api: DockviewReadyEvent["api"],
|
|
appStore: StoreApi<AppState>,
|
|
): void {
|
|
api.onDidRemovePanel((panel) => {
|
|
if (useDockviewStore.getState().isRestoringLayout) return;
|
|
const isMax = useDockviewStore.getState().preMaximizeLayout !== null;
|
|
const remaining = api.panels.filter((p) => p.id !== panel.id);
|
|
const nonSidebar = remaining.filter((p) => p.api.component !== "sidebar");
|
|
// If we're in maximize mode and the last non-sidebar panel was just closed,
|
|
// exit maximize to restore the pre-maximize layout (avoids empty view).
|
|
// Then remove the closed panel from the restored layout so it doesn't reappear.
|
|
if (isMax && nonSidebar.length === 0) {
|
|
const removedId = panel.id;
|
|
requestAnimationFrame(() => {
|
|
useDockviewStore.getState().exitMaximizedLayout();
|
|
// exitMaximizedLayout schedules a rAF to finalize — wait for that, then
|
|
// remove the panel that was closed (it was re-created from preMaximizeLayout).
|
|
requestAnimationFrame(() => {
|
|
const restoredPanel = api.getPanel(removedId);
|
|
if (restoredPanel) {
|
|
restoredPanel.api.close();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
const entry = panelPortalManager.get(panel.id);
|
|
// vscode is session-scoped so entry.sessionId is always set by PortalSlot.
|
|
if (entry?.component === "vscode" && entry.sessionId) stopVscode(entry.sessionId);
|
|
// terminal is global (no entry.sessionId) — fall back to the active session.
|
|
if (entry?.component === "terminal") {
|
|
const terminalSessionId = entry.sessionId ?? appStore.getState().tasks.activeSessionId;
|
|
const terminalId = entry.params.terminalId as string | undefined;
|
|
if (terminalId && terminalSessionId) stopUserShell(terminalSessionId, terminalId);
|
|
}
|
|
panelPortalManager.release(panel.id);
|
|
});
|
|
}
|