Fix terminal resize after splitting panes (#1249)
This commit is contained in:
@@ -183,6 +183,8 @@ Terminal I/O is sent as binary WebSocket frames decoded by `decodeTerminalStream
|
|||||||
- 1-byte slot: terminal slot id
|
- 1-byte slot: terminal slot id
|
||||||
- variable payload: bytes for output/input, JSON-encoded `{ rows, cols }` for resize, terminal snapshot for snapshot
|
- variable payload: bytes for output/input, JSON-encoded `{ rows, cols }` for resize, terminal snapshot for snapshot
|
||||||
|
|
||||||
|
Terminal PTY size is last-interacting-client-wins. A client claims the PTY size only when its terminal viewport genuinely changes size or the user focuses/taps the terminal. Passive rendering work — attaching, restoring visibility, font settling, renderer refits, or just looking at a visible terminal — must not send a resize frame. The server does not broadcast resize ownership; the resized PTY redraws through normal output, and every attached client renders that output in its own local viewport.
|
||||||
|
|
||||||
There is also a separate file-transfer binary frame format in the same directory, used for download/upload streams.
|
There is also a separate file-transfer binary frame format in the same directory, used for download/upload streams.
|
||||||
|
|
||||||
### Compatibility rules
|
### Compatibility rules
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import { test, expect } from "./fixtures";
|
||||||
|
import { TerminalE2EHarness, withTerminalInApp } from "./helpers/terminal-dsl";
|
||||||
|
import { getTerminalBufferText, waitForTerminalContent } from "./helpers/terminal-perf";
|
||||||
|
|
||||||
|
interface TerminalSize {
|
||||||
|
rows: number | null;
|
||||||
|
cols: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The xterm/client view resizes immediately on split, so it is not enough to
|
||||||
|
// prove the bug. It is the misleading symptom.
|
||||||
|
async function readXtermSize(page: Page): Promise<TerminalSize> {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const term = (window as Window & { __paseoTerminal?: { rows?: number; cols?: number } })
|
||||||
|
.__paseoTerminal;
|
||||||
|
return {
|
||||||
|
rows: typeof term?.rows === "number" ? term.rows : null,
|
||||||
|
cols: typeof term?.cols === "number" ? term.cols : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The PTY's own view of its size, reported by an `stty size` loop running in the
|
||||||
|
// shell. This needs no focus or click, so it observes whether the daemon-side PTY
|
||||||
|
// actually received the resize frame after the split.
|
||||||
|
async function readLatestPtySize(page: Page): Promise<TerminalSize | null> {
|
||||||
|
const text = await getTerminalBufferText(page);
|
||||||
|
const matches = [...text.matchAll(/PTYSIZE (\d+) (\d+)/g)];
|
||||||
|
const last = matches.at(-1);
|
||||||
|
if (!last) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { rows: Number(last[1]), cols: Number(last[2]) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPtySizeReport(text: string): boolean {
|
||||||
|
return /PTYSIZE \d+ \d+/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readXtermRows(page: Page): Promise<number | null> {
|
||||||
|
return (await readXtermSize(page)).rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ptyRowsMatchXtermRows(page: Page): Promise<boolean> {
|
||||||
|
const xterm = await readXtermSize(page);
|
||||||
|
const pty = await readLatestPtySize(page);
|
||||||
|
return pty?.rows === xterm.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifySplitDownResizesPty(page: Page, harness: TerminalE2EHarness): Promise<void> {
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
|
||||||
|
await withTerminalInApp(page, harness, { name: "split-resize" }, async () => {
|
||||||
|
await harness.setupPrompt(page);
|
||||||
|
|
||||||
|
const terminal = harness.terminalSurface(page);
|
||||||
|
// Continuously echo the PTY's own size. `stty size` prints "rows cols" and
|
||||||
|
// reads the controlling tty, so this keeps reporting the real PTY size
|
||||||
|
// without the test ever clicking the terminal back into focus.
|
||||||
|
await terminal.pressSequentially(
|
||||||
|
'while true; do echo "PTYSIZE $(stty size)"; sleep 0.3; done\n',
|
||||||
|
{ delay: 0 },
|
||||||
|
);
|
||||||
|
await waitForTerminalContent(page, hasPtySizeReport, 10_000);
|
||||||
|
|
||||||
|
const beforeXterm = await readXtermSize(page);
|
||||||
|
const beforePty = await readLatestPtySize(page);
|
||||||
|
expect(beforePty, "the PTY should report its size before splitting").not.toBeNull();
|
||||||
|
expect(beforeXterm.rows, "xterm should report its row count before splitting").not.toBeNull();
|
||||||
|
expect(beforePty?.rows, "while focused, the PTY size should already match the xterm size").toBe(
|
||||||
|
beforeXterm.rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Split the pane downward. This focuses the new empty pane, so the terminal
|
||||||
|
// pane is unfocused at the exact moment its container shrinks.
|
||||||
|
await page.getByRole("button", { name: "Split pane down" }).first().click();
|
||||||
|
|
||||||
|
// The local xterm renderer shrinks immediately on split - the part of the
|
||||||
|
// behaviour that already works and that makes the bug look like nothing changed.
|
||||||
|
await expect
|
||||||
|
.poll(() => readXtermRows(page), {
|
||||||
|
message: "xterm should shrink after splitting the pane down",
|
||||||
|
timeout: 8_000,
|
||||||
|
})
|
||||||
|
.toBeLessThan(beforeXterm.rows ?? Number.POSITIVE_INFINITY);
|
||||||
|
|
||||||
|
// The PTY must follow the shrunken terminal, even though focus moved to the
|
||||||
|
// new pane. Today it stays stuck at the pre-split size until the terminal is
|
||||||
|
// clicked back into focus. This poll fails without the fix.
|
||||||
|
await expect
|
||||||
|
.poll(() => ptyRowsMatchXtermRows(page), {
|
||||||
|
message: "the PTY rows should match the resized terminal after split-down",
|
||||||
|
timeout: 8_000,
|
||||||
|
})
|
||||||
|
.toBe(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Terminal split resize", () => {
|
||||||
|
test.describe.configure({ timeout: 120_000 });
|
||||||
|
|
||||||
|
let harness: TerminalE2EHarness;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
harness = await TerminalE2EHarness.create({ tempPrefix: "terminal-split-resize-" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await harness?.cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("splitting the pane down resizes the PTY even though focus moves to the new pane", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.setTimeout(90_000);
|
||||||
|
await verifySplitDownResizesPty(page, harness);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -50,7 +50,8 @@ interface TerminalEmulatorProps {
|
|||||||
onSwipeRight?: () => void;
|
onSwipeRight?: () => void;
|
||||||
initialSnapshot?: TerminalState | null;
|
initialSnapshot?: TerminalState | null;
|
||||||
onInput?: (data: string) => Promise<void> | void;
|
onInput?: (data: string) => Promise<void> | void;
|
||||||
onResize?: (input: { rows: number; cols: number }) => Promise<void> | void;
|
onFocus?: () => Promise<void> | void;
|
||||||
|
onResize?: (input: { rows: number; cols: number; shouldClaim: boolean }) => Promise<void> | void;
|
||||||
onTerminalKey?: (input: {
|
onTerminalKey?: (input: {
|
||||||
key: string;
|
key: string;
|
||||||
ctrl: boolean;
|
ctrl: boolean;
|
||||||
@@ -89,7 +90,7 @@ type BridgeInboundMessage =
|
|||||||
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
||||||
| { type: "clear"; streamKey: string }
|
| { type: "clear"; streamKey: string }
|
||||||
| { type: "focus"; streamKey: string; forceRefocus?: boolean }
|
| { type: "focus"; streamKey: string; forceRefocus?: boolean }
|
||||||
| { type: "resize"; streamKey: string }
|
| { type: "resize"; streamKey: string; shouldClaim?: boolean }
|
||||||
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
||||||
| { type: "setScrollback"; streamKey: string; lines: number }
|
| { type: "setScrollback"; streamKey: string; lines: number }
|
||||||
| { type: "setPendingModifiers"; streamKey: string; pendingModifiers: PendingTerminalModifiers }
|
| { type: "setPendingModifiers"; streamKey: string; pendingModifiers: PendingTerminalModifiers }
|
||||||
@@ -105,7 +106,7 @@ type BridgeOutboundMessage =
|
|||||||
| { type: "bridgeReady" }
|
| { type: "bridgeReady" }
|
||||||
| { type: "rendererReady"; streamKey: string; isReady: boolean }
|
| { type: "rendererReady"; streamKey: string; isReady: boolean }
|
||||||
| { type: "input"; streamKey: string; data: string }
|
| { type: "input"; streamKey: string; data: string }
|
||||||
| { type: "resize"; streamKey: string; rows: number; cols: number }
|
| { type: "resize"; streamKey: string; rows: number; cols: number; shouldClaim?: boolean }
|
||||||
| {
|
| {
|
||||||
type: "terminalKey";
|
type: "terminalKey";
|
||||||
streamKey: string;
|
streamKey: string;
|
||||||
@@ -188,6 +189,7 @@ export default function TerminalEmulator({
|
|||||||
onSwipeRight,
|
onSwipeRight,
|
||||||
initialSnapshot = null,
|
initialSnapshot = null,
|
||||||
onInput,
|
onInput,
|
||||||
|
onFocus,
|
||||||
onResize,
|
onResize,
|
||||||
onTerminalKey,
|
onTerminalKey,
|
||||||
onPendingModifiersConsumed,
|
onPendingModifiersConsumed,
|
||||||
@@ -229,6 +231,7 @@ export default function TerminalEmulator({
|
|||||||
};
|
};
|
||||||
const callbacksRef = useRef({
|
const callbacksRef = useRef({
|
||||||
onInput,
|
onInput,
|
||||||
|
onFocus,
|
||||||
onResize,
|
onResize,
|
||||||
onTerminalKey,
|
onTerminalKey,
|
||||||
onPendingModifiersConsumed,
|
onPendingModifiersConsumed,
|
||||||
@@ -241,6 +244,7 @@ export default function TerminalEmulator({
|
|||||||
});
|
});
|
||||||
callbacksRef.current = {
|
callbacksRef.current = {
|
||||||
onInput,
|
onInput,
|
||||||
|
onFocus,
|
||||||
onResize,
|
onResize,
|
||||||
onTerminalKey,
|
onTerminalKey,
|
||||||
onPendingModifiersConsumed,
|
onPendingModifiersConsumed,
|
||||||
@@ -401,14 +405,14 @@ export default function TerminalEmulator({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusRequestToken <= 0) return;
|
if (focusRequestToken <= 0) return;
|
||||||
sendToWebView({ type: "resize", streamKey });
|
sendToWebView({ type: "resize", streamKey, shouldClaim: true });
|
||||||
sendToWebView({ type: "focus", streamKey });
|
sendToWebView({ type: "focus", streamKey });
|
||||||
webViewRef.current?.requestFocus();
|
webViewRef.current?.requestFocus();
|
||||||
}, [focusRequestToken, sendToWebView, streamKey]);
|
}, [focusRequestToken, sendToWebView, streamKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resizeRequestToken <= 0) return;
|
if (resizeRequestToken <= 0) return;
|
||||||
sendToWebView({ type: "resize", streamKey });
|
sendToWebView({ type: "resize", streamKey, shouldClaim: true });
|
||||||
}, [resizeRequestToken, sendToWebView, streamKey]);
|
}, [resizeRequestToken, sendToWebView, streamKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -495,7 +499,11 @@ export default function TerminalEmulator({
|
|||||||
callbacksRef.current.onInput?.(message.data);
|
callbacksRef.current.onInput?.(message.data);
|
||||||
break;
|
break;
|
||||||
case "resize":
|
case "resize":
|
||||||
callbacksRef.current.onResize?.({ rows: message.rows, cols: message.cols });
|
callbacksRef.current.onResize?.({
|
||||||
|
rows: message.rows,
|
||||||
|
cols: message.cols,
|
||||||
|
shouldClaim: message.shouldClaim !== false,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case "terminalKey":
|
case "terminalKey":
|
||||||
callbacksRef.current.onTerminalKey?.({
|
callbacksRef.current.onTerminalKey?.({
|
||||||
@@ -592,6 +600,7 @@ export default function TerminalEmulator({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
webViewRef.current?.requestFocus();
|
webViewRef.current?.requestFocus();
|
||||||
|
callbacksRef.current.onFocus?.();
|
||||||
sendToWebView({ type: "focus", streamKey, forceRefocus: true });
|
sendToWebView({ type: "focus", streamKey, forceRefocus: true });
|
||||||
}, [sendToWebView, streamKey]);
|
}, [sendToWebView, streamKey]);
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,8 @@ interface TerminalEmulatorProps {
|
|||||||
onSwipeRight?: () => void;
|
onSwipeRight?: () => void;
|
||||||
initialSnapshot?: TerminalState | null;
|
initialSnapshot?: TerminalState | null;
|
||||||
onInput?: (data: string) => Promise<void> | void;
|
onInput?: (data: string) => Promise<void> | void;
|
||||||
onResize?: (input: { rows: number; cols: number }) => Promise<void> | void;
|
onFocus?: () => Promise<void> | void;
|
||||||
|
onResize?: (input: { rows: number; cols: number; shouldClaim: boolean }) => Promise<void> | void;
|
||||||
onTerminalKey?: (input: {
|
onTerminalKey?: (input: {
|
||||||
key: string;
|
key: string;
|
||||||
ctrl: boolean;
|
ctrl: boolean;
|
||||||
@@ -219,6 +220,7 @@ export default function TerminalEmulator({
|
|||||||
onSwipeRight,
|
onSwipeRight,
|
||||||
initialSnapshot = null,
|
initialSnapshot = null,
|
||||||
onInput,
|
onInput,
|
||||||
|
onFocus,
|
||||||
onResize,
|
onResize,
|
||||||
onTerminalKey,
|
onTerminalKey,
|
||||||
onPendingModifiersConsumed,
|
onPendingModifiersConsumed,
|
||||||
@@ -527,7 +529,7 @@ export default function TerminalEmulator({
|
|||||||
if (focusRequestToken <= 0) {
|
if (focusRequestToken <= 0) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
runtimeRef.current?.resize({ force: true });
|
runtimeRef.current?.resize({ force: true, shouldClaim: true });
|
||||||
return focusWithRetries({
|
return focusWithRetries({
|
||||||
focus: () => {
|
focus: () => {
|
||||||
runtimeRef.current?.focus();
|
runtimeRef.current?.focus();
|
||||||
@@ -547,7 +549,7 @@ export default function TerminalEmulator({
|
|||||||
if (resizeRequestToken <= 0) {
|
if (resizeRequestToken <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runtimeRef.current?.resize({ force: true });
|
runtimeRef.current?.resize({ force: true, shouldClaim: true });
|
||||||
}, [resizeRequestToken]);
|
}, [resizeRequestToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -742,8 +744,9 @@ export default function TerminalEmulator({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRootPointerDown = useCallback(() => {
|
const handleRootPointerDown = useCallback(() => {
|
||||||
|
onFocus?.();
|
||||||
runtimeRef.current?.focus();
|
runtimeRef.current?.focus();
|
||||||
}, []);
|
}, [onFocus]);
|
||||||
|
|
||||||
const handleRootContextMenu = useCallback(
|
const handleRootContextMenu = useCallback(
|
||||||
(event: ReactMouseEvent) => {
|
(event: ReactMouseEvent) => {
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export function TerminalPane({
|
|||||||
const pendingTerminalInputRef = useRef<PendingTerminalInput[]>([]);
|
const pendingTerminalInputRef = useRef<PendingTerminalInput[]>([]);
|
||||||
const keyboardRefitTimeoutsRef = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
const keyboardRefitTimeoutsRef = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
||||||
const lastAutoFocusKeyRef = useRef<string | null>(null);
|
const lastAutoFocusKeyRef = useRef<string | null>(null);
|
||||||
|
const lastPaneFocusResizeKeyRef = useRef<string | null>(null);
|
||||||
const initialSnapshot = workspaceTerminalSession.snapshots.get({ terminalId });
|
const initialSnapshot = workspaceTerminalSession.snapshots.get({ terminalId });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -252,7 +253,7 @@ export function TerminalPane({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile || !isWorkspaceFocused || !isPaneFocused || !terminalId) {
|
if (isMobile || !isPaneFocused || !terminalId) {
|
||||||
lastAutoFocusKeyRef.current = null;
|
lastAutoFocusKeyRef.current = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -263,15 +264,36 @@ export function TerminalPane({
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastAutoFocusKeyRef.current = nextFocusKey;
|
lastAutoFocusKeyRef.current = nextFocusKey;
|
||||||
|
if (!isWorkspaceFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
requestTerminalFocus();
|
requestTerminalFocus();
|
||||||
}, [isMobile, isPaneFocused, isWorkspaceFocused, requestTerminalFocus, scopeKey, terminalId]);
|
}, [isMobile, isPaneFocused, isWorkspaceFocused, requestTerminalFocus, scopeKey, terminalId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPaneFocused && isWorkspaceFocused && isAppVisible && terminalId) {
|
if (!isPaneFocused || !terminalId) {
|
||||||
lastSentTerminalSizeRef.current = null;
|
lastPaneFocusResizeKeyRef.current = null;
|
||||||
requestTerminalReflow();
|
return;
|
||||||
}
|
}
|
||||||
}, [isAppVisible, isPaneFocused, isWorkspaceFocused, requestTerminalReflow, terminalId]);
|
|
||||||
|
const focusResizeKey = `${scopeKey}:${terminalId}`;
|
||||||
|
if (lastPaneFocusResizeKeyRef.current === focusResizeKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastPaneFocusResizeKeyRef.current = focusResizeKey;
|
||||||
|
if (!isWorkspaceFocused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSentTerminalSizeRef.current = null;
|
||||||
|
requestTerminalReflow();
|
||||||
|
}, [isPaneFocused, isWorkspaceFocused, requestTerminalReflow, scopeKey, terminalId]);
|
||||||
|
|
||||||
|
const handleTerminalFocus = useCallback(() => {
|
||||||
|
lastSentTerminalSizeRef.current = null;
|
||||||
|
requestTerminalReflow();
|
||||||
|
}, [requestTerminalReflow]);
|
||||||
|
|
||||||
const clearKeyboardRefitTimeouts = useCallback(() => {
|
const clearKeyboardRefitTimeouts = useCallback(() => {
|
||||||
if (keyboardRefitTimeoutsRef.current.length === 0) {
|
if (keyboardRefitTimeoutsRef.current.length === 0) {
|
||||||
@@ -308,25 +330,6 @@ export function TerminalPane({
|
|||||||
[pulseKeyboardRefits],
|
[pulseKeyboardRefits],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isWorkspaceFocused || !terminalId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Focus transitions can temporarily report stale dimensions.
|
|
||||||
// Pulse forced refits so xterm fills the pane when returning to a terminal.
|
|
||||||
const timeoutHandles = TERMINAL_REFIT_DELAYS_MS.map((delayMs) =>
|
|
||||||
setTimeout(() => {
|
|
||||||
requestTerminalReflow();
|
|
||||||
}, delayMs),
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const handle of timeoutHandles) {
|
|
||||||
clearTimeout(handle);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isWorkspaceFocused, requestTerminalReflow, terminalId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!client || !isConnected || !isWorkspaceFocused) {
|
if (!client || !isConnected || !isWorkspaceFocused) {
|
||||||
return;
|
return;
|
||||||
@@ -375,7 +378,7 @@ export function TerminalPane({
|
|||||||
|
|
||||||
const controller = new TerminalStreamController({
|
const controller = new TerminalStreamController({
|
||||||
client,
|
client,
|
||||||
getPreferredSize: () => measuredTerminalSizeRef.current,
|
getPreferredSize: () => lastSentTerminalSizeRef.current,
|
||||||
onOutput: ({ terminalId: outputTerminalId, data }) => {
|
onOutput: ({ terminalId: outputTerminalId, data }) => {
|
||||||
if (!isWorkspaceFocused || terminalIdRef.current !== outputTerminalId) {
|
if (!isWorkspaceFocused || terminalIdRef.current !== outputTerminalId) {
|
||||||
return;
|
return;
|
||||||
@@ -605,33 +608,35 @@ export function TerminalPane({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTerminalResize = useStableEvent((input: { rows: number; cols: number }) => {
|
const handleTerminalResize = useStableEvent(
|
||||||
const { rows, cols } = input;
|
(input: { rows: number; cols: number; shouldClaim: boolean }) => {
|
||||||
if (rows <= 0 || cols <= 0) {
|
const { rows, cols } = input;
|
||||||
return;
|
if (rows <= 0 || cols <= 0) {
|
||||||
}
|
return;
|
||||||
const normalizedRows = Math.floor(rows);
|
}
|
||||||
const normalizedCols = Math.floor(cols);
|
const normalizedRows = Math.floor(rows);
|
||||||
const nextSize = { rows: normalizedRows, cols: normalizedCols };
|
const normalizedCols = Math.floor(cols);
|
||||||
measuredTerminalSizeRef.current = nextSize;
|
const nextSize = { rows: normalizedRows, cols: normalizedCols };
|
||||||
if (!client || !terminalId || !isPaneFocused || !isWorkspaceFocused || !isAppVisible) {
|
measuredTerminalSizeRef.current = nextSize;
|
||||||
return;
|
if (!input.shouldClaim || !client || !terminalId || !isWorkspaceFocused || !isAppVisible) {
|
||||||
}
|
return;
|
||||||
const previousSent = lastSentTerminalSizeRef.current;
|
}
|
||||||
if (
|
const previousSent = lastSentTerminalSizeRef.current;
|
||||||
previousSent &&
|
if (
|
||||||
previousSent.rows === normalizedRows &&
|
previousSent &&
|
||||||
previousSent.cols === normalizedCols
|
previousSent.rows === normalizedRows &&
|
||||||
) {
|
previousSent.cols === normalizedCols
|
||||||
return;
|
) {
|
||||||
}
|
return;
|
||||||
lastSentTerminalSizeRef.current = nextSize;
|
}
|
||||||
client.sendTerminalInput(terminalId, {
|
lastSentTerminalSizeRef.current = nextSize;
|
||||||
type: "resize",
|
client.sendTerminalInput(terminalId, {
|
||||||
rows: normalizedRows,
|
type: "resize",
|
||||||
cols: normalizedCols,
|
rows: normalizedRows,
|
||||||
});
|
cols: normalizedCols,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const handleTerminalKey = useCallback(
|
const handleTerminalKey = useCallback(
|
||||||
async (input: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean }) => {
|
async (input: { key: string; ctrl: boolean; shift: boolean; alt: boolean; meta: boolean }) => {
|
||||||
@@ -764,6 +769,7 @@ export function TerminalPane({
|
|||||||
onSwipeRight={handleSwipeRight}
|
onSwipeRight={handleSwipeRight}
|
||||||
onSwipeLeft={handleSwipeLeft}
|
onSwipeLeft={handleSwipeLeft}
|
||||||
onInput={handleTerminalData}
|
onInput={handleTerminalData}
|
||||||
|
onFocus={handleTerminalFocus}
|
||||||
onResize={handleTerminalResize}
|
onResize={handleTerminalResize}
|
||||||
onTerminalKey={handleTerminalKey}
|
onTerminalKey={handleTerminalKey}
|
||||||
onInputModeChange={handleInputModeChange}
|
onInputModeChange={handleInputModeChange}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ vi.mock("@xterm/addon-webgl", () => ({
|
|||||||
interface TerminalSize {
|
interface TerminalSize {
|
||||||
rows: number;
|
rows: number;
|
||||||
cols: number;
|
cols: number;
|
||||||
|
shouldClaim: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TerminalKeyRecord {
|
interface TerminalKeyRecord {
|
||||||
@@ -198,6 +199,15 @@ describe("terminal emulator runtime in a real browser", () => {
|
|||||||
expect(window.__paseoTerminal?.options.scrollback).toBe(42_000);
|
expect(window.__paseoTerminal?.options.scrollback).toBe(42_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not claim PTY ownership from passive mount refits", async () => {
|
||||||
|
await page.viewport(900, 600);
|
||||||
|
const mounted = createTerminalHost({ width: 720, height: 360 });
|
||||||
|
|
||||||
|
await waitFor({ predicate: () => mounted.sizes.length > 0 });
|
||||||
|
|
||||||
|
expect(mounted.sizes[0]?.shouldClaim).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("reports a larger PTY size when the terminal container grows", async () => {
|
it("reports a larger PTY size when the terminal container grows", async () => {
|
||||||
await page.viewport(900, 600);
|
await page.viewport(900, 600);
|
||||||
const mounted = createTerminalHost({ width: 360, height: 180 });
|
const mounted = createTerminalHost({ width: 360, height: 180 });
|
||||||
@@ -220,6 +230,7 @@ describe("terminal emulator runtime in a real browser", () => {
|
|||||||
const grownSize = latestSize(mounted.sizes);
|
const grownSize = latestSize(mounted.sizes);
|
||||||
expect(grownSize.cols).toBeGreaterThan(initialSize.cols);
|
expect(grownSize.cols).toBeGreaterThan(initialSize.cols);
|
||||||
expect(grownSize.rows).toBeGreaterThan(initialSize.rows);
|
expect(grownSize.rows).toBeGreaterThan(initialSize.rows);
|
||||||
|
expect(grownSize.shouldClaim).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refreshes visible rows on a forced same-size resize", async () => {
|
it("refreshes visible rows on a forced same-size resize", async () => {
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ interface StubTerminal {
|
|||||||
cols?: number;
|
cols?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RuntimeFitProbe {
|
||||||
|
fitAndEmitResize: (input?: { force?: boolean; shouldClaim?: boolean }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function createRuntimeWithTerminal(): {
|
function createRuntimeWithTerminal(): {
|
||||||
runtime: TerminalEmulatorRuntime;
|
runtime: TerminalEmulatorRuntime;
|
||||||
terminal: StubTerminal & {
|
terminal: StubTerminal & {
|
||||||
@@ -328,14 +332,13 @@ describe("terminal-emulator-runtime", () => {
|
|||||||
const runtime = new TerminalEmulatorRuntime();
|
const runtime = new TerminalEmulatorRuntime();
|
||||||
const fitAndEmitResize = vi.fn();
|
const fitAndEmitResize = vi.fn();
|
||||||
|
|
||||||
(runtime as unknown as { fitAndEmitResize: (force: boolean) => void }).fitAndEmitResize =
|
(runtime as unknown as RuntimeFitProbe).fitAndEmitResize = fitAndEmitResize;
|
||||||
fitAndEmitResize;
|
|
||||||
|
|
||||||
runtime.resize();
|
runtime.resize();
|
||||||
runtime.resize({ force: true });
|
runtime.resize({ force: true });
|
||||||
|
|
||||||
expect(fitAndEmitResize).toHaveBeenNthCalledWith(1, false);
|
expect(fitAndEmitResize).toHaveBeenNthCalledWith(1, undefined);
|
||||||
expect(fitAndEmitResize).toHaveBeenNthCalledWith(2, true);
|
expect(fitAndEmitResize).toHaveBeenNthCalledWith(2, { force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates terminal theme without remounting", () => {
|
it("updates terminal theme without remounting", () => {
|
||||||
@@ -381,12 +384,11 @@ describe("terminal-emulator-runtime", () => {
|
|||||||
expect(refresh).toHaveBeenCalledWith(0, 11);
|
expect(refresh).toHaveBeenCalledWith(0, 11);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forces a refit when the page becomes visible again", () => {
|
it("passively refits when the page becomes visible again", () => {
|
||||||
const runtime = new TerminalEmulatorRuntime();
|
const runtime = new TerminalEmulatorRuntime();
|
||||||
const fitAndEmitResize = vi.fn();
|
const fitAndEmitResize = vi.fn();
|
||||||
|
|
||||||
(runtime as unknown as { fitAndEmitResize: (force: boolean) => void }).fitAndEmitResize =
|
(runtime as unknown as RuntimeFitProbe).fitAndEmitResize = fitAndEmitResize;
|
||||||
fitAndEmitResize;
|
|
||||||
(globalThis as { document?: { visibilityState?: string } }).document = {
|
(globalThis as { document?: { visibilityState?: string } }).document = {
|
||||||
visibilityState: "visible",
|
visibilityState: "visible",
|
||||||
};
|
};
|
||||||
@@ -397,15 +399,14 @@ describe("terminal-emulator-runtime", () => {
|
|||||||
}
|
}
|
||||||
).handleVisibilityRestore();
|
).handleVisibilityRestore();
|
||||||
|
|
||||||
expect(fitAndEmitResize).toHaveBeenCalledWith(true);
|
expect(fitAndEmitResize).toHaveBeenCalledWith({ force: true, shouldClaim: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not refit while the page is still hidden", () => {
|
it("does not refit while the page is still hidden", () => {
|
||||||
const runtime = new TerminalEmulatorRuntime();
|
const runtime = new TerminalEmulatorRuntime();
|
||||||
const fitAndEmitResize = vi.fn();
|
const fitAndEmitResize = vi.fn();
|
||||||
|
|
||||||
(runtime as unknown as { fitAndEmitResize: (force: boolean) => void }).fitAndEmitResize =
|
(runtime as unknown as RuntimeFitProbe).fitAndEmitResize = fitAndEmitResize;
|
||||||
fitAndEmitResize;
|
|
||||||
(globalThis as { document?: { visibilityState?: string } }).document = {
|
(globalThis as { document?: { visibilityState?: string } }).document = {
|
||||||
visibilityState: "hidden",
|
visibilityState: "hidden",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export interface TerminalEmulatorRuntimeMountInput {
|
|||||||
|
|
||||||
export interface TerminalEmulatorRuntimeCallbacks {
|
export interface TerminalEmulatorRuntimeCallbacks {
|
||||||
onInput?: (data: string) => Promise<void> | void;
|
onInput?: (data: string) => Promise<void> | void;
|
||||||
onResize?: (input: { rows: number; cols: number }) => Promise<void> | void;
|
onResize?: (input: { rows: number; cols: number; shouldClaim: boolean }) => Promise<void> | void;
|
||||||
onTerminalKey?: (input: {
|
onTerminalKey?: (input: {
|
||||||
key: string;
|
key: string;
|
||||||
ctrl: boolean;
|
ctrl: boolean;
|
||||||
@@ -68,7 +68,6 @@ interface TerminalEmulatorRuntimeDisposables {
|
|||||||
removeWindowFocus: () => void;
|
removeWindowFocus: () => void;
|
||||||
removeDocumentVisibilityChange: () => void;
|
removeDocumentVisibilityChange: () => void;
|
||||||
removeVisualViewportResize: () => void;
|
removeVisualViewportResize: () => void;
|
||||||
clearFitInterval: () => void;
|
|
||||||
clearFitTimeouts: () => void;
|
clearFitTimeouts: () => void;
|
||||||
removeFontListeners: () => void;
|
removeFontListeners: () => void;
|
||||||
removeTouchListeners: () => void;
|
removeTouchListeners: () => void;
|
||||||
@@ -164,7 +163,8 @@ export class TerminalEmulatorRuntime {
|
|||||||
};
|
};
|
||||||
private terminal: Terminal | null = null;
|
private terminal: Terminal | null = null;
|
||||||
private fitAddon: FitAddon | null = null;
|
private fitAddon: FitAddon | null = null;
|
||||||
private fitAndEmitResize: ((force: boolean) => void) | null = null;
|
private fitAndEmitResize: ((input?: { force?: boolean; shouldClaim?: boolean }) => void) | null =
|
||||||
|
null;
|
||||||
private lastSize: { rows: number; cols: number } | null = null;
|
private lastSize: { rows: number; cols: number } | null = null;
|
||||||
private cleanup: (() => void) | null = null;
|
private cleanup: (() => void) | null = null;
|
||||||
private outputOperations: TerminalOutputOperation[] = [];
|
private outputOperations: TerminalOutputOperation[] = [];
|
||||||
@@ -181,10 +181,10 @@ export class TerminalEmulatorRuntime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fitAndEmitResize?.(true);
|
this.fitAndEmitResize?.({ force: true, shouldClaim: false });
|
||||||
if (typeof window.requestAnimationFrame === "function") {
|
if (typeof window.requestAnimationFrame === "function") {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
this.fitAndEmitResize?.(true);
|
this.fitAndEmitResize?.({ force: true, shouldClaim: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -281,7 +281,7 @@ export class TerminalEmulatorRuntime {
|
|||||||
webglAddon = null;
|
webglAddon = null;
|
||||||
disposeImageAddon();
|
disposeImageAddon();
|
||||||
// WebGL and DOM renderers can have different cell dimensions.
|
// WebGL and DOM renderers can have different cell dimensions.
|
||||||
this.fitAndEmitResize?.(true);
|
this.fitAndEmitResize?.({ force: true, shouldClaim: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Browser xterm is a renderer only; it never replies to terminal protocol queries.
|
// Browser xterm is a renderer only; it never replies to terminal protocol queries.
|
||||||
@@ -319,7 +319,7 @@ export class TerminalEmulatorRuntime {
|
|||||||
imageAddon = new ImageAddon();
|
imageAddon = new ImageAddon();
|
||||||
terminal.loadAddon(imageAddon);
|
terminal.loadAddon(imageAddon);
|
||||||
registerProtocolQuerySuppression();
|
registerProtocolQuerySuppression();
|
||||||
this.fitAndEmitResize?.(true);
|
this.fitAndEmitResize?.({ force: true, shouldClaim: false });
|
||||||
} catch {
|
} catch {
|
||||||
disposeWebglRenderer();
|
disposeWebglRenderer();
|
||||||
}
|
}
|
||||||
@@ -336,7 +336,9 @@ export class TerminalEmulatorRuntime {
|
|||||||
this.fitAddon = fitAddon;
|
this.fitAddon = fitAddon;
|
||||||
window.__paseoTerminal = terminal;
|
window.__paseoTerminal = terminal;
|
||||||
|
|
||||||
const fitAndEmitResize = (force: boolean): void => {
|
const fitAndEmitResize = (resizeInput?: { force?: boolean; shouldClaim?: boolean }): void => {
|
||||||
|
const force = resizeInput?.force ?? false;
|
||||||
|
const shouldClaim = resizeInput?.shouldClaim ?? true;
|
||||||
const currentTerminal = this.terminal;
|
const currentTerminal = this.terminal;
|
||||||
const currentFitAddon = this.fitAddon;
|
const currentFitAddon = this.fitAddon;
|
||||||
if (!currentTerminal || !currentFitAddon) {
|
if (!currentTerminal || !currentFitAddon) {
|
||||||
@@ -365,11 +367,12 @@ export class TerminalEmulatorRuntime {
|
|||||||
this.callbacks.onResize?.({
|
this.callbacks.onResize?.({
|
||||||
rows: nextRows,
|
rows: nextRows,
|
||||||
cols: nextCols,
|
cols: nextCols,
|
||||||
|
shouldClaim,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
this.fitAndEmitResize = fitAndEmitResize;
|
this.fitAndEmitResize = fitAndEmitResize;
|
||||||
|
|
||||||
fitAndEmitResize(true);
|
fitAndEmitResize({ force: true, shouldClaim: false });
|
||||||
|
|
||||||
const inputDisposable = terminal.onData((data) => {
|
const inputDisposable = terminal.onData((data) => {
|
||||||
if (this.suppressInput) {
|
if (this.suppressInput) {
|
||||||
@@ -454,12 +457,12 @@ export class TerminalEmulatorRuntime {
|
|||||||
terminal,
|
terminal,
|
||||||
});
|
});
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
fitAndEmitResize(false);
|
fitAndEmitResize({ shouldClaim: true });
|
||||||
});
|
});
|
||||||
resizeObserver.observe(input.root);
|
resizeObserver.observe(input.root);
|
||||||
resizeObserver.observe(input.host);
|
resizeObserver.observe(input.host);
|
||||||
|
|
||||||
const windowResizeHandler = () => fitAndEmitResize(false);
|
const windowResizeHandler = () => fitAndEmitResize({ shouldClaim: true });
|
||||||
window.addEventListener("resize", windowResizeHandler);
|
window.addEventListener("resize", windowResizeHandler);
|
||||||
const windowFocusHandler = () => {
|
const windowFocusHandler = () => {
|
||||||
this.handleVisibilityRestore();
|
this.handleVisibilityRestore();
|
||||||
@@ -472,26 +475,23 @@ export class TerminalEmulatorRuntime {
|
|||||||
document.addEventListener("visibilitychange", documentVisibilityChangeHandler);
|
document.addEventListener("visibilitychange", documentVisibilityChangeHandler);
|
||||||
|
|
||||||
const visualViewport = window.visualViewport;
|
const visualViewport = window.visualViewport;
|
||||||
const visualViewportResizeHandler = () => fitAndEmitResize(false);
|
const visualViewportResizeHandler = () => fitAndEmitResize({ shouldClaim: true });
|
||||||
visualViewport?.addEventListener("resize", visualViewportResizeHandler);
|
visualViewport?.addEventListener("resize", visualViewportResizeHandler);
|
||||||
|
|
||||||
const fitInterval = window.setInterval(() => {
|
|
||||||
fitAndEmitResize(false);
|
|
||||||
}, 250);
|
|
||||||
const fitTimeouts = FIT_TIMEOUT_DELAYS_MS.map((delayMs) =>
|
const fitTimeouts = FIT_TIMEOUT_DELAYS_MS.map((delayMs) =>
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
fitAndEmitResize(true);
|
fitAndEmitResize({ force: true, shouldClaim: false });
|
||||||
}, delayMs),
|
}, delayMs),
|
||||||
);
|
);
|
||||||
|
|
||||||
const fontSet = document.fonts;
|
const fontSet = document.fonts;
|
||||||
const fontReadyHandler = () => {
|
const fontReadyHandler = () => {
|
||||||
fitAndEmitResize(true);
|
fitAndEmitResize({ force: true, shouldClaim: false });
|
||||||
};
|
};
|
||||||
fontSet?.addEventListener?.("loadingdone", fontReadyHandler);
|
fontSet?.addEventListener?.("loadingdone", fontReadyHandler);
|
||||||
void fontSet?.ready
|
void fontSet?.ready
|
||||||
.then(() => {
|
.then(() => {
|
||||||
fitAndEmitResize(true);
|
fitAndEmitResize({ force: true, shouldClaim: false });
|
||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -499,7 +499,7 @@ export class TerminalEmulatorRuntime {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
fitAndEmitResize(true);
|
fitAndEmitResize({ force: true, shouldClaim: false });
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
if (input.initialSnapshot) {
|
if (input.initialSnapshot) {
|
||||||
@@ -527,9 +527,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
removeVisualViewportResize: () => {
|
removeVisualViewportResize: () => {
|
||||||
visualViewport?.removeEventListener("resize", visualViewportResizeHandler);
|
visualViewport?.removeEventListener("resize", visualViewportResizeHandler);
|
||||||
},
|
},
|
||||||
clearFitInterval: () => {
|
|
||||||
window.clearInterval(fitInterval);
|
|
||||||
},
|
|
||||||
clearFitTimeouts: () => {
|
clearFitTimeouts: () => {
|
||||||
for (const handle of fitTimeouts) {
|
for (const handle of fitTimeouts) {
|
||||||
window.clearTimeout(handle);
|
window.clearTimeout(handle);
|
||||||
@@ -565,7 +562,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
disposables.removeWindowFocus();
|
disposables.removeWindowFocus();
|
||||||
disposables.removeDocumentVisibilityChange();
|
disposables.removeDocumentVisibilityChange();
|
||||||
disposables.removeVisualViewportResize();
|
disposables.removeVisualViewportResize();
|
||||||
disposables.clearFitInterval();
|
|
||||||
disposables.clearFitTimeouts();
|
disposables.clearFitTimeouts();
|
||||||
disposables.removeFontListeners();
|
disposables.removeFontListeners();
|
||||||
disposables.removeTouchListeners();
|
disposables.removeTouchListeners();
|
||||||
@@ -635,8 +631,8 @@ export class TerminalEmulatorRuntime {
|
|||||||
this.processOutputQueue();
|
this.processOutputQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(input?: { force?: boolean }): void {
|
resize(input?: { force?: boolean; shouldClaim?: boolean }): void {
|
||||||
this.fitAndEmitResize?.(input?.force ?? false);
|
this.fitAndEmitResize?.(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTheme(input: { theme: ITheme }): void {
|
setTheme(input: { theme: ITheme }): void {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type InboundMessage =
|
|||||||
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
| { type: "renderSnapshot"; streamKey: string; state: TerminalState | null }
|
||||||
| { type: "clear"; streamKey: string }
|
| { type: "clear"; streamKey: string }
|
||||||
| { type: "focus"; streamKey: string; forceRefocus?: boolean }
|
| { type: "focus"; streamKey: string; forceRefocus?: boolean }
|
||||||
| { type: "resize"; streamKey: string }
|
| { type: "resize"; streamKey: string; shouldClaim?: boolean }
|
||||||
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
| { type: "setTheme"; streamKey: string; theme: ITheme }
|
||||||
| { type: "setScrollback"; streamKey: string; lines: number }
|
| { type: "setScrollback"; streamKey: string; lines: number }
|
||||||
| { type: "setPendingModifiers"; streamKey: string; pendingModifiers: PendingTerminalModifiers }
|
| { type: "setPendingModifiers"; streamKey: string; pendingModifiers: PendingTerminalModifiers }
|
||||||
@@ -46,7 +46,7 @@ type OutboundMessage =
|
|||||||
| { type: "bridgeReady" }
|
| { type: "bridgeReady" }
|
||||||
| { type: "rendererReady"; streamKey: string; isReady: boolean }
|
| { type: "rendererReady"; streamKey: string; isReady: boolean }
|
||||||
| { type: "input"; streamKey: string; data: string }
|
| { type: "input"; streamKey: string; data: string }
|
||||||
| { type: "resize"; streamKey: string; rows: number; cols: number }
|
| { type: "resize"; streamKey: string; rows: number; cols: number; shouldClaim?: boolean }
|
||||||
| {
|
| {
|
||||||
type: "terminalKey";
|
type: "terminalKey";
|
||||||
streamKey: string;
|
streamKey: string;
|
||||||
@@ -244,7 +244,7 @@ class TerminalWebViewBridge {
|
|||||||
this.runtime?.focus({ forceRefocus: message.forceRefocus });
|
this.runtime?.focus({ forceRefocus: message.forceRefocus });
|
||||||
break;
|
break;
|
||||||
case "resize":
|
case "resize":
|
||||||
this.runtime?.resize({ force: true });
|
this.runtime?.resize({ force: true, shouldClaim: message.shouldClaim !== false });
|
||||||
break;
|
break;
|
||||||
case "setTheme":
|
case "setTheme":
|
||||||
this.applyThemeBackground(message.theme);
|
this.applyThemeBackground(message.theme);
|
||||||
@@ -273,8 +273,8 @@ class TerminalWebViewBridge {
|
|||||||
runtime.setCallbacks({
|
runtime.setCallbacks({
|
||||||
callbacks: {
|
callbacks: {
|
||||||
onInput: (data) => sendToNative({ type: "input", streamKey: message.streamKey, data }),
|
onInput: (data) => sendToNative({ type: "input", streamKey: message.streamKey, data }),
|
||||||
onResize: ({ rows, cols }) =>
|
onResize: ({ rows, cols, shouldClaim }) =>
|
||||||
sendToNative({ type: "resize", streamKey: message.streamKey, rows, cols }),
|
sendToNative({ type: "resize", streamKey: message.streamKey, rows, cols, shouldClaim }),
|
||||||
onTerminalKey: (input) =>
|
onTerminalKey: (input) =>
|
||||||
sendToNative({ type: "terminalKey", streamKey: message.streamKey, ...input }),
|
sendToNative({ type: "terminalKey", streamKey: message.streamKey, ...input }),
|
||||||
onPendingModifiersConsumed: () =>
|
onPendingModifiersConsumed: () =>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user