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:
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user