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)
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
import { useEffect, useCallback, useRef, useState } from "react";
|
|
import { useAppStore } from "@/components/state-provider";
|
|
import { getWebSocketClient } from "@/lib/ws/connection";
|
|
import type { CumulativeDiff } from "@/lib/state/slices/session-runtime/types";
|
|
|
|
const cumulativeDiffCache: Record<string, CumulativeDiff | null> = {};
|
|
const loadingState: Record<string, boolean> = {};
|
|
|
|
const listeners = new Set<(envKey: string) => void>();
|
|
|
|
/**
|
|
* Invalidate the cumulative diff cache for the given environment key.
|
|
* Callers should resolve sessionId → envKey before calling this.
|
|
*/
|
|
export function invalidateCumulativeDiffCache(envKey: string) {
|
|
delete cumulativeDiffCache[envKey];
|
|
listeners.forEach((fn) => fn(envKey));
|
|
}
|
|
|
|
export function useCumulativeDiff(sessionId: string | null) {
|
|
// Resolve to environment key so sessions sharing the same environment share the cache.
|
|
const envKey = useAppStore((state) => {
|
|
if (!sessionId) return null;
|
|
return state.environmentIdBySessionId[sessionId] ?? sessionId;
|
|
});
|
|
|
|
// Guard against stale responses after an environment switch.
|
|
const requestVersionRef = useRef(0);
|
|
|
|
const [diff, setDiff] = useState<CumulativeDiff | null>(
|
|
envKey ? (cumulativeDiffCache[envKey] ?? null) : null,
|
|
);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [invalidationCount, setInvalidationCount] = useState(0);
|
|
|
|
const fetchCumulativeDiff = useCallback(async () => {
|
|
if (!sessionId || !envKey) return;
|
|
if (loadingState[envKey]) return;
|
|
|
|
const client = getWebSocketClient();
|
|
if (!client) return;
|
|
|
|
const version = ++requestVersionRef.current;
|
|
|
|
setLoading(true);
|
|
loadingState[envKey] = true;
|
|
setError(null);
|
|
|
|
try {
|
|
// Backend routes by session_id, but we cache by envKey
|
|
const response = await client.request<{ cumulative_diff?: CumulativeDiff }>(
|
|
"session.cumulative_diff",
|
|
{ session_id: sessionId },
|
|
);
|
|
|
|
// Discard if the environment changed while the request was in flight
|
|
if (version !== requestVersionRef.current) return;
|
|
|
|
if (response?.cumulative_diff) {
|
|
cumulativeDiffCache[envKey] = response.cumulative_diff;
|
|
setDiff(response.cumulative_diff);
|
|
}
|
|
} catch (err) {
|
|
if (version !== requestVersionRef.current) return;
|
|
console.error("Failed to fetch cumulative diff:", err);
|
|
setError(err instanceof Error ? err.message : "Failed to fetch cumulative diff");
|
|
} finally {
|
|
if (version === requestVersionRef.current) {
|
|
setLoading(false);
|
|
}
|
|
loadingState[envKey] = false;
|
|
}
|
|
}, [sessionId, envKey]);
|
|
|
|
// Sync cached state when envKey changes. Must run BEFORE the fetch effect
|
|
// so that fetchCumulativeDiff's setLoading(true) wins the React 18 batch.
|
|
useEffect(() => {
|
|
// Bump version so any in-flight fetch for the previous envKey is discarded.
|
|
// Clear the per-key loading flag so the fetch effect isn't blocked on re-entry
|
|
// (e.g. A→B→A where A's original fetch is still in-flight).
|
|
requestVersionRef.current++;
|
|
if (envKey) {
|
|
loadingState[envKey] = false;
|
|
setDiff(cumulativeDiffCache[envKey] ?? null);
|
|
} else {
|
|
setDiff(null);
|
|
}
|
|
setLoading(false);
|
|
}, [envKey]);
|
|
|
|
// Fetch on mount and when cache is invalidated
|
|
useEffect(() => {
|
|
if (!envKey) return;
|
|
fetchCumulativeDiff();
|
|
}, [envKey, invalidationCount, fetchCumulativeDiff]);
|
|
|
|
// Subscribe to cache invalidation from WS handlers
|
|
useEffect(() => {
|
|
if (!envKey) return;
|
|
const handler = (invalidatedEnvKey: string) => {
|
|
if (invalidatedEnvKey === envKey) {
|
|
setInvalidationCount((c) => c + 1);
|
|
}
|
|
};
|
|
listeners.add(handler);
|
|
return () => {
|
|
listeners.delete(handler);
|
|
};
|
|
}, [envKey]);
|
|
|
|
return {
|
|
diff,
|
|
loading,
|
|
error,
|
|
refetch: fetchCumulativeDiff,
|
|
};
|
|
}
|