Keep workspaces responsive across app-wide routes (#2791)

* perf(app): retain inactive workspace screens

App-wide routes cleared the active workspace selection, which removed the retained workspace deck from the React tree and made returning rebuild every panel. Keep mounted deck entries inactive across those routes and suspend inactive panel subscriptions so state is preserved without background work.

* perf(app): suspend remaining inactive queries

Retained composers and commit diff panels still subscribed through query hooks that did not receive panel activity. Propagate the existing activity gate so app-wide routes leave those hidden surfaces dormant.

* refactor(app): separate panel activity from connectivity

A hidden retained panel is still connected to its host. Keep connectivity truthful, pause work inside query and stream owners, and preserve terminal emulator identity while its stream is detached.

* test(app): target active retained controls

* fix(app): retain compact and terminal route content

* fix(app): hide inactive workspace overlays
This commit is contained in:
Mohamed Boudra
2026-08-02 14:32:17 +02:00
committed by GitHub
parent 26bdb1d171
commit 2b474f7df9
17 changed files with 355 additions and 105 deletions
+14 -9
View File
@@ -52,6 +52,7 @@ import {
canDesktopAppSidebarShare,
resolveDesktopAppChromeLayout,
resolveDesktopAppContentMinimum,
resolveDesktopSidebarVisibility,
} from "@/components/desktop-sidebar-layout";
import { isNative, isWeb } from "@/constants/platform";
import { HorizontalScrollProvider } from "@/contexts/horizontal-scroll-context";
@@ -78,6 +79,7 @@ import { RosettaCalloutSource } from "@/desktop/updates/rosetta-callout-source";
import { UpdateCalloutSource } from "@/desktop/updates/update-callout-source";
import { useActiveWorktreeNewAction } from "@/hooks/use-active-worktree-new-action";
import { useGlobalNewWorkspaceAction } from "@/hooks/use-global-new-workspace-action";
import { useLatchedBoolean } from "@/hooks/use-latched-boolean";
import { useFaviconStatus } from "@/hooks/use-favicon-status";
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
import { KeyboardShiftProvider } from "@/hooks/use-keyboard-shift-style";
@@ -450,6 +452,7 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
const isWorkspaceRoute = parseHostWorkspaceRouteFromPathname(pathname) !== null;
const isWorkspaceFocusModeEnabled = isWorkspaceRoute && isFocusModeEnabled;
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
const hasMountedDesktopSidebar = useLatchedBoolean(chromeEnabled);
const toggleAgentList = isCompactLayout ? toggleMobileAgentList : toggleDesktopAgentList;
const toggleDesktopSidebars = useCallback(() => {
const { desktop } = usePanelStore.getState();
@@ -491,16 +494,18 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
requestedExplorerWidth: explorerWidth,
viewportWidth,
});
const desktopSidebarMounted = chromeEnabled && !isWorkspaceFocusModeEnabled;
const desktopSidebarVisible =
!isCompactLayout &&
desktopSidebarMounted &&
isDesktopAgentListOpen &&
canDesktopAppSidebarShare({
const desktopSidebarMounted = hasMountedDesktopSidebar && !isWorkspaceFocusModeEnabled;
const desktopSidebarVisible = resolveDesktopSidebarVisibility({
chromeEnabled,
isCompactLayout,
isMounted: desktopSidebarMounted,
isOpen: isDesktopAgentListOpen,
canShare: canDesktopAppSidebarShare({
contentMinimumWidth: appContentMinimumWidth,
requestedSidebarWidth: sidebarWidth,
viewportWidth,
});
}),
});
const hasTopLeftWindowControls = useHasWindowChromeObstruction("top-left");
const appChromeLayout = resolveDesktopAppChromeLayout({
desktopSidebarRendered: desktopSidebarVisible,
@@ -521,9 +526,9 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
{sidebarChrome}
</WindowChromeRegion>
) : null}
{isCompactLayout && chromeEnabled ? (
{isCompactLayout ? (
<CompactExplorerSidebarHost enabled={chromeEnabled}>
<WindowChromeRegion corners="both">
<WindowChromeRegion corners={chromeEnabled ? "both" : appChromeLayout.contentCorners}>
<View style={flexStyle}>{children}</View>
</WindowChromeRegion>
</CompactExplorerSidebarHost>
@@ -18,6 +18,7 @@ import {
getWorkspaceSelectionKey,
orderWorkspaceSelectionsForStableRender,
pruneMountedWorkspaceSelections,
resolveWorkspaceDeckEntries,
shouldKeepWorkspaceDeckEntryMounted,
WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES,
} from "@/screens/workspace/workspace-deck-retention";
@@ -222,6 +223,10 @@ function WorkspaceDeck({
() => orderWorkspaceSelectionsForStableRender(nextMountedSelections),
[nextMountedSelections],
);
const renderedEntries = useMemo(
() => resolveWorkspaceDeckEntries({ selections: renderedSelections, activeSelection }),
[activeSelection, renderedSelections],
);
useLayoutEffect(() => {
if (!areWorkspaceSelectionListsEqual(mountedSelections, nextMountedSelections)) {
@@ -229,18 +234,14 @@ function WorkspaceDeck({
}
}, [mountedSelections, nextMountedSelections]);
if (!activeSelection) {
return null;
}
return (
<View style={styles.deck}>
{renderedSelections.map((selection) => {
{renderedEntries.map(({ selection, active }) => {
return (
<WorkspaceDeckEntry
key={getWorkspaceSelectionKey(selection)}
selection={selection}
activeSelection={activeSelection}
active={active}
recoveryRequested={recoveryRequested}
recoveryAgentId={recoveryAgentId}
onUnmountInactive={unmountWorkspaceSelection}
@@ -253,22 +254,21 @@ function WorkspaceDeck({
function WorkspaceDeckEntry({
selection,
activeSelection,
active,
recoveryRequested,
recoveryAgentId,
onUnmountInactive,
}: {
selection: ActiveWorkspaceSelection;
activeSelection: ActiveWorkspaceSelection;
active: boolean;
recoveryRequested: boolean;
recoveryAgentId: string | null;
onUnmountInactive: (selection: ActiveWorkspaceSelection) => void;
}) {
const isActive = areWorkspaceSelectionsEqual(selection, activeSelection);
const hasHydratedWorkspaces = useHasHydratedWorkspaces(selection.serverId);
const workspaceExists = useWorkspaceExists(selection.serverId, selection.workspaceId);
const shouldKeepMounted = shouldKeepWorkspaceDeckEntryMounted({
isActive,
isActive: active,
hasHydratedWorkspaces,
workspaceExists,
});
@@ -285,15 +285,15 @@ function WorkspaceDeckEntry({
return (
<RetainedPanel
active={isActive}
active={active}
testID={`workspace-deck-entry-${selection.serverId}:${selection.workspaceId}`}
>
<WorkspaceScreen
serverId={selection.serverId}
workspaceId={selection.workspaceId}
isRouteFocused={isActive}
recoveryRequested={isActive && recoveryRequested}
recoveryAgentId={isActive ? recoveryAgentId : null}
isRouteFocused={active}
recoveryRequested={active && recoveryRequested}
recoveryAgentId={active ? recoveryAgentId : null}
/>
</RetainedPanel>
);
@@ -4,10 +4,23 @@ import {
resolveDesktopAppChromeLayout,
resolveDesktopAppContentMinimum,
resolveDesktopExplorerWidth,
resolveDesktopSidebarVisibility,
resolveDesktopSidebarWidth,
} from "@/components/desktop-sidebar-layout";
describe("desktop sidebar layout", () => {
it("keeps a retained sidebar hidden while app chrome is suppressed", () => {
expect(
resolveDesktopSidebarVisibility({
chromeEnabled: false,
isCompactLayout: false,
isMounted: true,
isOpen: true,
canShare: true,
}),
).toBe(false);
});
it("keeps the sidebar toggle window-owned beside left window controls", () => {
expect(
resolveDesktopAppChromeLayout({
@@ -8,6 +8,22 @@ import {
export const MIN_DESKTOP_CENTER_WIDTH = 400;
export function resolveDesktopSidebarVisibility(input: {
chromeEnabled: boolean;
isCompactLayout: boolean;
isMounted: boolean;
isOpen: boolean;
canShare: boolean;
}): boolean {
return (
input.chromeEnabled &&
!input.isCompactLayout &&
input.isMounted &&
input.isOpen &&
input.canShare
);
}
export function resolveDesktopAppChromeLayout(input: {
desktopSidebarRendered: boolean;
hasTopLeftWindowControls: boolean;
+42 -42
View File
@@ -1,4 +1,5 @@
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated";
@@ -172,6 +173,7 @@ export function TerminalPane({
onOpenWorkspaceFile,
}: TerminalPaneProps) {
const { t } = useTranslation();
const retainedPanelActive = useRetainedPanelActive();
const isAppActivelyVisible = useAppActivelyVisible();
const { theme } = useUnistyles();
const { settings } = useAppSettings();
@@ -191,6 +193,7 @@ export function TerminalPane({
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const isTerminalActive = retainedPanelActive && isWorkspaceFocused;
const supportsTerminalRestoreModes = useSessionStore(
(state) => state.sessions[serverId]?.serverInfo?.features?.["terminal-restore-modes"] === true,
);
@@ -215,6 +218,8 @@ export function TerminalPane({
const [resizeRequestToken, setResizeRequestToken] = useState(0);
const emulatorRef = useRef<TerminalEmulatorHandle>(null);
const terminalIdRef = useRef<string>(terminalId);
const terminalActiveRef = useRef(isTerminalActive);
terminalActiveRef.current = isTerminalActive;
const inputModeRef = useRef<TerminalInputModeState>({
kittyKeyboardFlags: 0,
win32InputMode: false,
@@ -356,7 +361,7 @@ export function TerminalPane({
);
useEffect(() => {
if (!client || !isConnected || !isWorkspaceFocused) {
if (!client || !isConnected || !isTerminalActive) {
return;
}
@@ -379,7 +384,7 @@ export function TerminalPane({
});
setModifiers({ ...EMPTY_MODIFIERS });
});
}, [client, isConnected, isWorkspaceFocused, workspaceTerminalSession.snapshots]);
}, [client, isConnected, isTerminalActive, workspaceTerminalSession.snapshots]);
useEffect(() => {
measuredTerminalSizeRef.current = null;
@@ -405,21 +410,21 @@ export function TerminalPane({
client,
getPreferredSize: () => lastSentTerminalSizeRef.current,
onOutput: ({ terminalId: outputTerminalId, data }) => {
if (!isWorkspaceFocused || terminalIdRef.current !== outputTerminalId) {
if (!terminalActiveRef.current || terminalIdRef.current !== outputTerminalId) {
return;
}
emulatorRef.current?.writeOutput(data);
},
onRestore: ({ terminalId: restoreTerminalId, data }) => {
workspaceTerminalSession.snapshots.clear({ terminalId: restoreTerminalId });
if (!isWorkspaceFocused || terminalIdRef.current !== restoreTerminalId) {
if (!terminalActiveRef.current || terminalIdRef.current !== restoreTerminalId) {
return;
}
emulatorRef.current?.restoreOutput(data);
},
onSnapshot: ({ terminalId: snapshotTerminalId, state }) => {
workspaceTerminalSession.snapshots.set({ terminalId: snapshotTerminalId, state });
if (!isWorkspaceFocused || terminalIdRef.current !== snapshotTerminalId) {
if (!terminalActiveRef.current || terminalIdRef.current !== snapshotTerminalId) {
return;
}
emulatorRef.current?.renderSnapshot(state);
@@ -435,7 +440,7 @@ export function TerminalPane({
streamControllerRef.current = controller;
controller.setTerminal({
terminalId: isWorkspaceFocused ? terminalIdRef.current : null,
terminalId: terminalActiveRef.current ? terminalIdRef.current : null,
});
return () => {
@@ -448,18 +453,17 @@ export function TerminalPane({
client,
handleStreamControllerStatus,
isConnected,
isWorkspaceFocused,
supportsTerminalRestoreModes,
workspaceTerminalSession.snapshots,
]);
useEffect(() => {
pendingTerminalInputRef.current = [];
const nextTerminalId = isWorkspaceFocused ? terminalId : null;
const nextTerminalId = isTerminalActive ? terminalId : null;
streamControllerRef.current?.setTerminal({
terminalId: nextTerminalId,
});
}, [isWorkspaceFocused, terminalId]);
}, [isTerminalActive, terminalId]);
const enqueuePendingTerminalInput = useCallback((entry: PendingTerminalInput) => {
const queue = pendingTerminalInputRef.current;
@@ -778,7 +782,7 @@ export function TerminalPane({
onOpenFileExplorer();
}, [swipeGesturesEnabled, onOpenFileExplorer]);
const showLoadingOverlay = shouldShowTerminalLoadingOverlay({
isWorkspaceFocused,
isWorkspaceFocused: isTerminalActive,
hasStreamError: Boolean(streamError),
isAttaching,
rendererReadyStreamKey,
@@ -796,38 +800,34 @@ export function TerminalPane({
return (
<Animated.View style={containerStyle}>
<View style={styles.outputContainer}>
{isWorkspaceFocused ? (
<View style={styles.terminalGestureContainer}>
<TerminalEmulator
ref={emulatorRef}
dom={TERMINAL_EMULATOR_DOM_PROPS}
streamKey={terminalStreamKey}
testId="terminal-surface"
xtermTheme={xtermTheme}
scrollbackLines={settings.terminalScrollbackLines}
fontFamily={terminalFontFamily}
fontSize={settings.codeFontSize}
swipeGesturesEnabled={swipeGesturesEnabled}
initialSnapshot={initialSnapshot}
onRendererReadyChange={handleRendererReadyChange}
onSwipeRight={handleSwipeRight}
onSwipeLeft={handleSwipeLeft}
onInput={handleTerminalData}
onFocus={handleTerminalFocus}
onResize={handleTerminalResize}
onTerminalKey={handleTerminalKey}
onInputModeChange={handleInputModeChange}
onResolveLocalFileLink={handleResolveLocalFileLink}
onOpenLocalFileLink={handleOpenLocalFileLink}
onPendingModifiersConsumed={handlePendingModifiersConsumed}
pendingModifiers={modifiers}
focusRequestToken={focusRequestToken}
resizeRequestToken={resizeRequestToken}
/>
</View>
) : (
<View style={styles.terminalGestureContainer} />
)}
<View style={styles.terminalGestureContainer}>
<TerminalEmulator
ref={emulatorRef}
dom={TERMINAL_EMULATOR_DOM_PROPS}
streamKey={terminalStreamKey}
testId="terminal-surface"
xtermTheme={xtermTheme}
scrollbackLines={settings.terminalScrollbackLines}
fontFamily={terminalFontFamily}
fontSize={settings.codeFontSize}
swipeGesturesEnabled={swipeGesturesEnabled}
initialSnapshot={initialSnapshot}
onRendererReadyChange={handleRendererReadyChange}
onSwipeRight={handleSwipeRight}
onSwipeLeft={handleSwipeLeft}
onInput={handleTerminalData}
onFocus={handleTerminalFocus}
onResize={handleTerminalResize}
onTerminalKey={handleTerminalKey}
onInputModeChange={handleInputModeChange}
onResolveLocalFileLink={handleResolveLocalFileLink}
onOpenLocalFileLink={handleOpenLocalFileLink}
onPendingModifiersConsumed={handlePendingModifiersConsumed}
pendingModifiers={modifiers}
focusRequestToken={focusRequestToken}
resizeRequestToken={resizeRequestToken}
/>
</View>
{showLoadingOverlay ? (
<View style={styles.attachOverlay} pointerEvents="none" testID="terminal-attach-loading">
+5 -2
View File
@@ -1,5 +1,6 @@
import type { CheckoutCommit } from "@getpaseo/protocol/messages";
import invariant from "tiny-invariant";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useFetchQuery } from "@/data/query";
import { checkoutCommitsQueryKey } from "@/git/query-keys";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
@@ -72,6 +73,8 @@ export function useCheckoutCommitsQuery({
cwd,
enabled = true,
}: UseCheckoutCommitsQueryOptions): CheckoutCommitsQueryResult {
const retainedPanelActive = useRetainedPanelActive();
const queryEnabledByCaller = enabled && retainedPanelActive;
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
// COMPAT(commitsList): added in v0.1.110, remove after 2027-01-16.
@@ -84,7 +87,7 @@ export function useCheckoutCommitsQuery({
);
const canFetch = Boolean(cwd) && Boolean(client) && isConnected;
const queryEnabled = enabled && capabilityPresent && canFetch;
const queryEnabled = queryEnabledByCaller && capabilityPresent && canFetch;
const query = useFetchQuery<CheckoutCommitsData>({
queryKey: checkoutCommitsQueryKey(serverId, cwd),
@@ -105,7 +108,7 @@ export function useCheckoutCommitsQuery({
});
return resolveCheckoutCommitsQueryResult({
enabled,
enabled: queryEnabledByCaller,
capabilityPresent,
canFetch,
data: query.data,
+5 -2
View File
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import type { CheckoutCommitFile, ParsedDiffFile } from "@getpaseo/protocol/messages";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useFetchQueries } from "@/data/query";
import { checkoutCommitFileDiffQueryKey, COMMIT_FILE_DIFF_STALE_TIME } from "@/git/query-keys";
import { useCheckoutCommitsQuery } from "@/git/use-commits-query";
@@ -57,9 +58,11 @@ export function resolveCommitDiffFiles(
export function useCommitDiffFiles(ctx: CommitDiffFilesContext): CommitDiffFilesResult {
const { serverId, cwd, sha, enabled = true } = ctx;
const retainedPanelActive = useRetainedPanelActive();
const queryEnabled = enabled && retainedPanelActive;
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const commitsQuery = useCheckoutCommitsQuery({ serverId, cwd, enabled });
const commitsQuery = useCheckoutCommitsQuery({ serverId, cwd, enabled: queryEnabled });
const commitsData = commitsQuery.status === "loaded" ? commitsQuery.data : null;
const commitFiles = useMemo(() => {
if (!sha || !commitsData) {
@@ -69,7 +72,7 @@ export function useCommitDiffFiles(ctx: CommitDiffFilesContext): CommitDiffFiles
}, [commitsData, sha]);
const fileDiffsEnabled =
enabled &&
queryEnabled &&
commitsQuery.status === "loaded" &&
Boolean(cwd) &&
Boolean(sha) &&
+5 -2
View File
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useReplicaQuery } from "@/data/query";
import { checkoutDiffPushRoute } from "@/data/push-router";
import { useHostRuntimeIsConnected } from "@/runtime/host-runtime";
@@ -47,6 +48,8 @@ export function useCheckoutDiffQuery({
enabled = true,
queryScope,
}: UseCheckoutDiffQueryOptions) {
const retainedPanelActive = useRetainedPanelActive();
const queryEnabled = enabled && retainedPanelActive;
const isConnected = useHostRuntimeIsConnected(serverId);
const normalizedCompare = useMemo(
() => normalizeCheckoutDiffCompare({ mode, baseRef, ignoreWhitespace }),
@@ -67,7 +70,7 @@ export function useCheckoutDiffQuery({
return normalizedScope ? [...comparisonKey, "scope", normalizedScope] : comparisonKey;
}, [serverId, cwd, compareMode, compareBaseRef, compareIgnoreWhitespace, queryScope]);
const subscriptionId = useMemo(() => `checkoutDiff:${JSON.stringify(queryKey)}`, [queryKey]);
const routeEnabled = Boolean(enabled && isConnected && cwd);
const routeEnabled = Boolean(queryEnabled && isConnected && cwd);
const query = useReplicaQuery<CheckoutDiffQueryPayload>({
queryKey,
@@ -93,7 +96,7 @@ export function useCheckoutDiffQuery({
files: payload?.files ?? [],
payloadError,
diffTooLarge: payload?.diffTooLarge === true,
isLoading: payload === null && enabled && isConnected,
isLoading: payload === null && queryEnabled && isConnected,
isFetching: false,
isError: Boolean(payloadError),
error: null,
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { agentCommandsQueryKey, type AgentCommandsDraftConfig } from "@/hooks/agent-commands-query";
@@ -51,6 +52,8 @@ export function useAgentCommandsQuery({
draftConfig,
}: UseAgentCommandsQueryOptions) {
const { t } = useTranslation();
const retainedPanelActive = useRetainedPanelActive();
const queryEnabled = enabled && retainedPanelActive;
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
@@ -62,7 +65,7 @@ export function useAgentCommandsQuery({
}
return fetchAgentCommands({ client, agentId, draftConfig });
},
enabled: enabled && !!client && isConnected && (!!agentId || !!draftConfig),
enabled: queryEnabled && !!client && isConnected && (!!agentId || !!draftConfig),
staleTime: draftConfig ? DRAFT_COMMANDS_STALE_TIME : SESSION_COMMANDS_STALE_TIME,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 5000),
@@ -3,6 +3,7 @@ import { useMutation, useQueryClient, type QueryClient } from "@tanstack/react-q
import { useTranslation } from "react-i18next";
import type { AgentProvider, ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import { useRetainedPanelActive } from "@/components/retained-panel";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { useReplicaQuery } from "@/data/query";
@@ -95,10 +96,11 @@ export function useProvidersSnapshot(
options: UseProvidersSnapshotOptions = {},
): UseProvidersSnapshotResult {
const { t } = useTranslation();
const retainedPanelActive = useRetainedPanelActive();
const queryClient = useQueryClient();
const client = useHostRuntimeClient(serverId ?? "");
const enabled = (options.enabled ?? true) && retainedPanelActive;
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
const enabled = options.enabled ?? true;
const cwd = normalizeProvidersSnapshotCwd(options.cwd);
const supportsSnapshot = useSessionStore(
(state) => state.sessions[serverId ?? ""]?.serverInfo?.features?.providersSnapshot === true,
@@ -17,7 +17,6 @@ import { useWorkspaceDirectory, useWorkspaceFields } from "@/stores/session-stor
type ListTerminalsPayload = ListTerminalsResponse["payload"];
const FLEX_FILL_STYLE = { flex: 1 } as const;
const CENTERED_PADDED_STYLE = {
flex: 1,
alignItems: "center",
@@ -97,10 +96,6 @@ function TerminalPanel() {
}, [isGitCheckout, openFileExplorerForCheckout, serverId, workspaceDirectory]);
invariant(target.kind === "terminal", "TerminalPanel requires terminal target");
if (!isWorkspaceFocused) {
return <View style={FLEX_FILL_STYLE} />;
}
if (!workspaceDirectory) {
return (
<View style={CENTERED_PADDED_STYLE}>
@@ -3,6 +3,7 @@ import type { ActiveWorkspaceSelection } from "@/stores/navigation-active-worksp
import {
orderWorkspaceSelectionsForStableRender,
pruneMountedWorkspaceSelections,
resolveWorkspaceDeckEntries,
shouldKeepWorkspaceDeckEntryMounted,
} from "@/screens/workspace/workspace-deck-retention";
@@ -92,6 +93,20 @@ describe("orderWorkspaceSelectionsForStableRender", () => {
});
});
describe("resolveWorkspaceDeckEntries", () => {
it("keeps retained workspaces rendered but inactive on an app-wide route", () => {
expect(
resolveWorkspaceDeckEntries({
selections: [workspace("A"), workspace("B")],
activeSelection: null,
}),
).toEqual([
{ selection: workspace("A"), active: false },
{ selection: workspace("B"), active: false },
]);
});
});
describe("shouldKeepWorkspaceDeckEntryMounted", () => {
it("keeps the active workspace mounted even when it is missing from hydrated workspaces", () => {
expect(
@@ -14,6 +14,11 @@ interface WorkspaceDeckEntryMountInput {
workspaceExists: boolean;
}
interface ResolveWorkspaceDeckEntriesInput {
selections: ActiveWorkspaceSelection[];
activeSelection: ActiveWorkspaceSelection | null;
}
export function getWorkspaceSelectionKey(selection: ActiveWorkspaceSelection): string {
return `${selection.serverId}:${selection.workspaceId}`;
}
@@ -82,6 +87,19 @@ export function orderWorkspaceSelectionsForStableRender(
);
}
export function resolveWorkspaceDeckEntries({
selections,
activeSelection,
}: ResolveWorkspaceDeckEntriesInput): Array<{
selection: ActiveWorkspaceSelection;
active: boolean;
}> {
return selections.map((selection) => ({
selection,
active: areWorkspaceSelectionsEqual(selection, activeSelection),
}));
}
export function shouldKeepWorkspaceDeckEntryMounted({
isActive,
hasHydratedWorkspaces,
@@ -102,7 +102,7 @@ import {
useHostRuntimeSnapshot,
useHosts,
} from "@/runtime/host-runtime";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { prefetchProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { shouldShowWorkspaceSetup, useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
import { useWorkspace } from "@/stores/session-store-hooks";
import { useWorkspaceTerminalSessionRetention } from "@/terminal/hooks/use-workspace-terminal-session-retention";
@@ -1770,6 +1770,9 @@ function WorkspaceScreenContent({
const client = useHostRuntimeClient(normalizedServerId);
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
const supportsProvidersSnapshot = useSessionStore(
(state) => state.sessions[normalizedServerId]?.serverInfo?.features?.providersSnapshot === true,
);
const workspaceDirectory = workspaceDescriptor?.workspaceDirectory || null;
const isMissingWorkspaceDirectory = Boolean(workspaceDescriptor) && !workspaceDirectory;
const [isImportSheetVisible, setIsImportSheetVisible] = useState(false);
@@ -1781,11 +1784,25 @@ function WorkspaceScreenContent({
setIsImportSheetVisible(false);
}, []);
// Warm the workspace-scoped provider snapshot so the model picker is ready when opened.
useProvidersSnapshot(normalizedServerId, {
cwd: workspaceDirectory,
enabled: isRouteFocused,
});
useEffect(() => {
if (
!isRouteFocused ||
!isConnected ||
!client ||
!workspaceDirectory ||
!supportsProvidersSnapshot
) {
return;
}
prefetchProvidersSnapshot(normalizedServerId, client, { cwd: workspaceDirectory });
}, [
client,
isConnected,
isRouteFocused,
normalizedServerId,
supportsProvidersSnapshot,
workspaceDirectory,
]);
const persistenceKey = useMemo(
() =>
@@ -3870,7 +3887,7 @@ function WorkspaceScreenContent({
{workspaceCenterColumn}
</WorkspaceChromeRow>
<ImportSessionSheet
visible={isImportSheetVisible}
visible={isRouteFocused && isImportSheetVisible}
client={client}
serverId={normalizedServerId}
cwd={workspaceDirectory}
@@ -3879,7 +3896,7 @@ function WorkspaceScreenContent({
onImportedAgent={handleImportedAgent}
/>
<WorkspaceTabRenameModal
renamingTab={renamingTab}
renamingTab={isRouteFocused ? renamingTab : null}
onSubmit={handleRenameModalSubmit}
onClose={handleRenameModalClose}
/>