* perf(terminal): smooth layout drags and faster tab switching Defer xterm refit during split, sidebar, and host-tree drags while keeping pane containers in sync with live layout measurements. Refactor TerminalLayer into focused sections with TabBridge/memo optimizations and add the terminal host tree sidebar. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(terminal): keep side panels alive and guard session attach races Prevent terminal boot unmount from leaking backend sessions, keep SFTP/scripts/theme/AI state when switching side tabs, and defer heavy SFTP UI mount so first entry stays responsive. Co-authored-by: Cursor <cursoragent@cursor.com> * perf(terminal): reduce tab switch jank --------- Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
4.2 KiB
TypeScript
155 lines
4.2 KiB
TypeScript
import { activeTabStore } from "../application/state/activeTabStore";
|
|
import type { Workspace } from "../types";
|
|
|
|
export const HIDDEN_TERMINAL_PANE_SNAPSHOT = "hidden";
|
|
|
|
export type TerminalPaneSnapshot =
|
|
| typeof HIDDEN_TERMINAL_PANE_SNAPSHOT
|
|
| `solo|${string}`
|
|
| `workspace|split|${string}`
|
|
| `workspace|focus|${string}|${string}`;
|
|
|
|
export type TerminalPaneFocusSnapshot = "na" | "focused" | "unfocused";
|
|
|
|
interface GetTerminalPaneSnapshotOptions {
|
|
activeTabId: string | null;
|
|
sessionId: string;
|
|
sessionWorkspaceId?: string;
|
|
workspaceById: Map<string, Workspace>;
|
|
isTerminalLayerVisible: boolean;
|
|
}
|
|
|
|
export function getTerminalPaneSnapshot({
|
|
activeTabId,
|
|
sessionId,
|
|
sessionWorkspaceId,
|
|
workspaceById,
|
|
isTerminalLayerVisible,
|
|
}: GetTerminalPaneSnapshotOptions): TerminalPaneSnapshot {
|
|
if (!isTerminalLayerVisible || !activeTabId) {
|
|
return HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
|
}
|
|
|
|
const activeWorkspace = workspaceById.get(activeTabId);
|
|
if (activeWorkspace) {
|
|
if (sessionWorkspaceId !== activeWorkspace.id) {
|
|
return HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
|
}
|
|
|
|
const focusedSessionId = activeWorkspace.focusedSessionId ?? "";
|
|
if (activeWorkspace.viewMode === "focus") {
|
|
return sessionId === focusedSessionId
|
|
? `workspace|focus|${activeWorkspace.id}|${focusedSessionId}`
|
|
: HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
|
}
|
|
|
|
return `workspace|split|${activeWorkspace.id}`;
|
|
}
|
|
|
|
return activeTabId === sessionId
|
|
? `solo|${sessionId}`
|
|
: HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
|
}
|
|
|
|
export function parseTerminalPaneSnapshot(snapshot: TerminalPaneSnapshot): {
|
|
isVisible: boolean;
|
|
mode: "hidden" | "solo" | "split" | "focus";
|
|
workspaceId: string | null;
|
|
focusedSessionId: string | null;
|
|
} {
|
|
if (snapshot === HIDDEN_TERMINAL_PANE_SNAPSHOT) {
|
|
return {
|
|
isVisible: false,
|
|
mode: "hidden",
|
|
workspaceId: null,
|
|
focusedSessionId: null,
|
|
};
|
|
}
|
|
|
|
const parts = snapshot.split("|");
|
|
if (parts[0] === "solo") {
|
|
return {
|
|
isVisible: true,
|
|
mode: "solo",
|
|
workspaceId: null,
|
|
focusedSessionId: null,
|
|
};
|
|
}
|
|
|
|
if (parts[1] === "focus") {
|
|
return {
|
|
isVisible: true,
|
|
mode: "focus",
|
|
workspaceId: parts[2] || null,
|
|
focusedSessionId: parts[3] || null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
isVisible: true,
|
|
mode: "split",
|
|
workspaceId: parts[2] || null,
|
|
focusedSessionId: null,
|
|
};
|
|
}
|
|
|
|
export function getTerminalPaneFocusSnapshot({
|
|
activeTabId: activeTabIdOverride,
|
|
sessionId,
|
|
sessionWorkspaceId,
|
|
workspaceById,
|
|
}: {
|
|
activeTabId?: string | null;
|
|
sessionId: string;
|
|
sessionWorkspaceId?: string;
|
|
workspaceById: Map<string, Workspace>;
|
|
}): TerminalPaneFocusSnapshot {
|
|
const activeTabId = activeTabIdOverride ?? activeTabStore.getActiveTabId();
|
|
if (!activeTabId) return "na";
|
|
|
|
const activeWorkspace = workspaceById.get(activeTabId);
|
|
if (!activeWorkspace || activeWorkspace.viewMode === "focus") return "na";
|
|
if (sessionWorkspaceId !== activeWorkspace.id) return "na";
|
|
|
|
return activeWorkspace.focusedSessionId === sessionId ? "focused" : "unfocused";
|
|
}
|
|
|
|
/** Combined visibility + focus snapshot for a single useSyncExternalStore subscription. */
|
|
export function getTerminalPaneRenderSnapshot(
|
|
options: GetTerminalPaneSnapshotOptions,
|
|
): string {
|
|
const pane = getTerminalPaneSnapshot(options);
|
|
if (pane === HIDDEN_TERMINAL_PANE_SNAPSHOT) {
|
|
return HIDDEN_TERMINAL_PANE_SNAPSHOT;
|
|
}
|
|
const focus = getTerminalPaneFocusSnapshot({
|
|
activeTabId: options.activeTabId,
|
|
sessionId: options.sessionId,
|
|
sessionWorkspaceId: options.sessionWorkspaceId,
|
|
workspaceById: options.workspaceById,
|
|
});
|
|
return `${pane}|${focus}`;
|
|
}
|
|
|
|
export function parseTerminalPaneRenderSnapshot(snapshot: string): {
|
|
paneState: ReturnType<typeof parseTerminalPaneSnapshot>;
|
|
isFocusedPane: boolean;
|
|
} {
|
|
if (snapshot === HIDDEN_TERMINAL_PANE_SNAPSHOT) {
|
|
return {
|
|
paneState: parseTerminalPaneSnapshot(HIDDEN_TERMINAL_PANE_SNAPSHOT),
|
|
isFocusedPane: false,
|
|
};
|
|
}
|
|
|
|
const focusSep = snapshot.lastIndexOf("|");
|
|
const focusToken = snapshot.slice(focusSep + 1);
|
|
const paneSnapshot = snapshot.slice(0, focusSep) as TerminalPaneSnapshot;
|
|
const paneState = parseTerminalPaneSnapshot(paneSnapshot);
|
|
|
|
return {
|
|
paneState,
|
|
isFocusedPane: focusToken === "focused",
|
|
};
|
|
}
|