Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 1 addition & 16 deletions apps/desktop/src/renderer/lib/terminal/terminal-addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,9 @@ import { installRectangleRendererAlphaPatch } from "./webgl-vibrancy-patch";
export interface LoadAddonsResult {
searchAddon: SearchAddon;
progressAddon: ProgressAddon;
clearTextureAtlas: () => void;
dispose: () => void;
}

interface LoadAddonsOptions {
onRendererChange?: () => void;
}

// Once WebGL fails, skip it for all subsequent runtimes (VS Code pattern).
let suggestedRendererType: "webgl" | "dom" | undefined;

Expand All @@ -27,10 +22,7 @@ let suggestedRendererType: "webgl" | "dom" | undefined;
* function and addon instances. WebGL is deferred to rAF to avoid
* racing with xterm's post-open viewport sync.
*/
export function loadAddons(
terminal: XTerm,
options: LoadAddonsOptions = {},
): LoadAddonsResult {
export function loadAddons(terminal: XTerm): LoadAddonsResult {
let disposed = false;
let webglAddon: WebglAddon | null = null;

Expand Down Expand Up @@ -60,7 +52,6 @@ export function loadAddons(
webglAddon.onContextLoss(() => {
webglAddon?.dispose();
webglAddon = null;
options.onRendererChange?.();
terminal.refresh(0, terminal.rows - 1);
});
terminal.loadAddon(webglAddon);
Expand All @@ -69,7 +60,6 @@ export function loadAddons(
// Claude Code TUI blocks render as opaque black even though the
// rest of the terminal is transparent. See `webgl-vibrancy-patch.ts`.
installRectangleRendererAlphaPatch(webglAddon);
options.onRendererChange?.();
} catch {
suggestedRendererType = "dom";
webglAddon = null;
Expand All @@ -79,11 +69,6 @@ export function loadAddons(
return {
searchAddon,
progressAddon,
clearTextureAtlas: () => {
try {
webglAddon?.clearTextureAtlas();
} catch {}
},
dispose: () => {
disposed = true;
cancelAnimationFrame(rafId);
Expand Down
103 changes: 2 additions & 101 deletions apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ const DIMS_KEY_PREFIX = "terminal-dims:";
const DEFAULT_COLS = 120;
const DEFAULT_ROWS = 32;
const RESIZE_DEBOUNCE_MS = 75;
const FONT_SETTLE_TIMEOUT_MS = 1000;
const FONT_LOAD_SAMPLE_TEXT = "W";

// xterm's _keyDown calls stopPropagation after processing, so any chord we
// want the host (react-hotkeys-hook, Electron menu accelerators) or the shell
Expand Down Expand Up @@ -89,10 +87,6 @@ export interface TerminalRuntime {
container: HTMLDivElement | null;
resizeObserver: ResizeObserver | null;
_disposeResizeObserver: (() => void) | null;
_disposeFontSettle: (() => void) | null;
_onResize: (() => void) | null;
_clearTextureAtlas: (() => void) | null;
_fontSettleToken: number;
lastCols: number;
lastRows: number;
_disposeAddons: (() => void) | null;
Expand Down Expand Up @@ -191,53 +185,6 @@ function hostIsVisible(container: HTMLDivElement | null): boolean {
return container.clientWidth > 0 && container.clientHeight > 0;
}

function waitForNextFrame(): Promise<void> {
if (typeof requestAnimationFrame !== "function") {
return Promise.resolve();
}
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
}

function waitForTerminalFont(
terminal: XTerm,
timeoutMs = FONT_SETTLE_TIMEOUT_MS,
): Promise<void> {
const fontFamily = terminal.options.fontFamily;
const fontSize = terminal.options.fontSize;
if (
typeof document === "undefined" ||
!("fonts" in document) ||
typeof fontFamily !== "string" ||
typeof fontSize !== "number"
) {
return waitForNextFrame();
}

const fontSpec = `${fontSize}px ${fontFamily}`;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const timeout = new Promise<void>((resolve) => {
timeoutId = setTimeout(resolve, timeoutMs);
});
let fontLoad: Promise<void>;
try {
fontLoad = document.fonts
.load(fontSpec, FONT_LOAD_SAMPLE_TEXT)
.catch(() => document.fonts.ready)
.then(() => undefined)
.catch(() => undefined);
} catch {
fontLoad = document.fonts.ready
.then(() => undefined)
.catch(() => undefined);
}

return Promise.race([fontLoad, timeout])
.then(() => waitForNextFrame())
.finally(() => {
if (timeoutId !== null) clearTimeout(timeoutId);
});
}

// Body-level hidden container that owns wrapper divs of terminals whose
// React component is currently unmounted (e.g. workspace switch). Keeps
// xterm attached to the document so it survives provider remounts without
Expand Down Expand Up @@ -293,33 +240,6 @@ function measureAndResize(runtime: TerminalRuntime): boolean {
return terminal.cols !== prevCols || terminal.rows !== prevRows;
}

function scheduleFontSettleRefit(runtime: TerminalRuntime) {
runtime._disposeFontSettle?.();

let disposed = false;
const token = runtime._fontSettleToken + 1;
runtime._fontSettleToken = token;

runtime._disposeFontSettle = () => {
disposed = true;
if (runtime._fontSettleToken === token) {
runtime._disposeFontSettle = null;
}
};

void waitForTerminalFont(runtime.terminal).then(() => {
if (disposed || runtime._fontSettleToken !== token) return;
runtime._disposeFontSettle = null;
if (!hostIsVisible(runtime.container)) return;

// A late-loading font can change cell metrics after xterm's first fit.
runtime._clearTextureAtlas?.();
if (measureAndResize(runtime)) {
runtime._onResize?.();
}
});
}

function createResizeScheduler(
runtime: TerminalRuntime,
onResize?: () => void,
Expand Down Expand Up @@ -383,19 +303,14 @@ export function createRuntime(

// Activate Unicode 11 widths (inside loadAddons) before restoring the buffer,
// else CJK/emoji/ZWJ widths get baked wrong into the replay. (#3572)
let runtime: TerminalRuntime | null = null;
const addonsResult = loadAddons(terminal, {
onRendererChange: () => {
if (runtime) scheduleFontSettleRefit(runtime);
},
});
const addonsResult = loadAddons(terminal);
if (options.initialBuffer !== undefined) {
terminal.write(options.initialBuffer);
} else {
restoreBuffer(terminalId, terminal);
}

runtime = {
return {
terminalId,
terminal,
fitAddon,
Expand All @@ -406,15 +321,10 @@ export function createRuntime(
container: null,
resizeObserver: null,
_disposeResizeObserver: null,
_disposeFontSettle: null,
_onResize: null,
_clearTextureAtlas: addonsResult.clearTextureAtlas,
_fontSettleToken: 0,
lastCols: cols,
lastRows: rows,
_disposeAddons: addonsResult.dispose,
};
return runtime;
}

export function attachToContainer(
Expand All @@ -425,7 +335,6 @@ export function attachToContainer(
// If we're already attached to this exact container, do nothing. Prevents
// redundant refresh/focus/fit from transient remounts during provider key
// churn — VSCode setVisible() is idempotent for the same host element.
runtime._onResize = onResize ?? null;
const sameContainer =
runtime.container === container &&
runtime.wrapper.parentElement === container;
Expand All @@ -441,7 +350,6 @@ export function attachToContainer(
containerHeight: container.clientHeight,
});
if (measureAndResize(runtime)) onResize?.();
scheduleFontSettleRefit(runtime);

// Renderer may have skipped frames while the wrapper was detached.
// (refresh is now handled inside measureAndResize)
Expand Down Expand Up @@ -486,9 +394,6 @@ export function detachFromContainer(runtime: TerminalRuntime) {
);
runtime._disposeResizeObserver?.();
runtime._disposeResizeObserver = null;
runtime._disposeFontSettle?.();
runtime._disposeFontSettle = null;
runtime._onResize = null;
runtime.resizeObserver?.disconnect();
runtime.resizeObserver = null;
// Park instead of .remove() so xterm survives the React unmount —
Expand All @@ -513,7 +418,6 @@ export function updateRuntimeAppearance(
terminal.options.fontSize = appearance.fontSize;
if (hostIsVisible(runtime.container)) {
measureAndResize(runtime);
scheduleFontSettleRefit(runtime);
}
}
}
Expand All @@ -531,9 +435,6 @@ export function disposeRuntime(
runtime._disposeAddons = null;
runtime._disposeResizeObserver?.();
runtime._disposeResizeObserver = null;
runtime._disposeFontSettle?.();
runtime._disposeFontSettle = null;
runtime._onResize = null;
runtime.resizeObserver?.disconnect();
runtime.resizeObserver = null;
runtime.wrapper.remove();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export function TerminalPane({
onOpenFile,
onRevealPath,
}: TerminalPaneProps) {
const openInExternalEditor = useOpenInExternalEditor(workspaceId);
const { data: fileDragBehavior } =
electronTrpc.settings.getFileDragBehavior.useQuery();
const { data: fileOpenMode } =
Expand All @@ -74,6 +73,11 @@ export function TerminalPane({
() => paneData.terminalId ?? crypto.randomUUID(),
[paneData.terminalId],
);
// FORK NOTE: paneData.workspaceId fallback for cross-workspace terminal
// sessions (#3751). Older pane data without workspaceId falls back to
// the current workspace.
const sessionWorkspaceId = paneData.workspaceId ?? workspaceId;
const openInExternalEditor = useOpenInExternalEditor(sessionWorkspaceId);
const terminalInstanceId = ctx.pane.id;
const containerRef = useRef<HTMLDivElement | null>(null);
const activeTheme = useTheme();
Expand All @@ -95,8 +99,8 @@ export function TerminalPane({
const websocketUrl = useWorkspaceWsUrl(`/terminal/${terminalId}`);
const websocketUrlRef = useRef(websocketUrl);
websocketUrlRef.current = websocketUrl;
const workspaceIdRef = useRef(workspaceId);
workspaceIdRef.current = workspaceId;
const sessionWorkspaceIdRef = useRef(sessionWorkspaceId);
sessionWorkspaceIdRef.current = sessionWorkspaceId;

const ensureSession = workspaceTrpc.terminal.ensureSession.useMutation();
const ensureSessionRef = useRef(ensureSession);
Expand Down Expand Up @@ -146,7 +150,7 @@ export function TerminalPane({
// "Session not found."
// Deps narrowed to [terminalId] so provider key remount churn (workspaceId
// briefly flipping while pane data catches up) doesn't re-run this effect.
// workspaceId / websocketUrl are read through refs.
// sessionWorkspaceId / websocketUrl are read through refs.
useEffect(() => {
const container = containerRef.current;
if (!container) return;
Expand All @@ -159,7 +163,7 @@ export function TerminalPane({
);

let cancelled = false;
const sessionWorkspaceId = workspaceIdRef.current;
const activeSessionWorkspaceId = sessionWorkspaceIdRef.current;

// Always connect after ensureSession settles, even on error: if the
// session actually exists on the server (e.g. we raced another client),
Expand All @@ -169,14 +173,12 @@ export function TerminalPane({
ensureSessionRef.current
.mutateAsync({
terminalId,
workspaceId: sessionWorkspaceId,
workspaceId: activeSessionWorkspaceId,
themeType: initialThemeTypeRef.current,
})
.then((result) => {
if (result.status === "active") {
void invalidateTerminalSessionsRef.current({
workspaceId: sessionWorkspaceId,
});
void invalidateTerminalSessionsRef.current();
}
})
.catch((err) => {
Expand Down Expand Up @@ -232,7 +234,7 @@ export function TerminalPane({
stat: async (path) => {
try {
const result = await statPathRef.current({
workspaceId,
workspaceId: sessionWorkspaceId,
path,
});
if (!result) return null;
Expand Down Expand Up @@ -303,7 +305,7 @@ export function TerminalPane({
}, [
terminalId,
terminalInstanceId,
workspaceId,
sessionWorkspaceId,
ctx.store,
onOpenFile,
onRevealPath,
Expand Down
Loading
Loading