refactor(terminal): inject mac platform flag at mount

Greptile review: the browser tests patched navigator.userAgent/platform
globals to exercise mac-only key handling. Make the platform an
explicit mount option (defaulting to navigator detection) so tests
inject it directly and the runtime stops sniffing navigator per
keydown.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo Ambrozio
2026-10-03 13:06:27 +02:00
committed by Mohamed Boudra
co-authored by Claude Fable 5
parent 7dc2a3568c
commit a983ae0444
3 changed files with 24 additions and 60 deletions
@@ -45,11 +45,6 @@ interface MountedTerminal {
const mountedTerminals: MountedTerminal[] = [];
const MAC_DESKTOP_UA =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
const LINUX_DESKTOP_UA =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
function nextFrame(): Promise<void> {
return new Promise((resolve) => {
requestAnimationFrame(() => {
@@ -82,6 +77,7 @@ function createTerminalHost(input: {
width: number;
height: number;
scrollback?: number;
isMacLikePlatform?: boolean;
}): MountedTerminal {
const root = document.createElement("div");
root.style.width = `${input.width}px`;
@@ -127,6 +123,7 @@ function createTerminalHost(input: {
host,
initialSnapshot: null,
scrollback: input.scrollback ?? 10_000,
isMacLikePlatform: input.isMacLikePlatform,
theme: {
background: "#0b0b0b",
foreground: "#e6e6e6",
@@ -212,35 +209,6 @@ function dispatchTerminalKey(input: {
return textarea.dispatchEvent(event);
}
// The runtime reads the platform per keydown, so shadowing the instance properties
// makes mac-only key handling testable from any CI host.
function withNavigatorPlatform(
input: { userAgent: string; platform: string },
run: () => void,
): void {
const restores = (["userAgent", "platform"] as const).map((property) => {
const original = Object.getOwnPropertyDescriptor(navigator, property);
Object.defineProperty(navigator, property, {
value: input[property],
configurable: true,
});
return () => {
if (original) {
Object.defineProperty(navigator, property, original);
} else {
Reflect.deleteProperty(navigator, property);
}
};
});
try {
run();
} finally {
for (const restore of restores) {
restore();
}
}
}
/** An OSC 8 hyperlink, the escape sequence CLIs such as gh and ls --hyperlink print. */
function hyperlink(input: { url: string; text: string }): string {
return `\x1b]8;;${input.url}\x1b\\${input.text}\x1b]8;;\x1b\\`;
@@ -555,18 +523,16 @@ describe("terminal emulator runtime in a real browser", () => {
it("translates mac editing shortcuts into shell editing sequences", async () => {
await page.viewport(900, 600);
const mounted = createTerminalHost({ width: 720, height: 360 });
const mounted = createTerminalHost({ width: 720, height: 360, isMacLikePlatform: true });
await waitFor({ predicate: () => mounted.sizes.length > 0 });
withNavigatorPlatform({ userAgent: MAC_DESKTOP_UA, platform: "MacIntel" }, () => {
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39 });
dispatchTerminalKey({ host: mounted.host, key: "ArrowLeft", keyCode: 37, metaKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, metaKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowLeft", keyCode: 37, altKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, altKey: true });
dispatchTerminalKey({ host: mounted.host, key: "Backspace", keyCode: 8, metaKey: true });
});
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39 });
dispatchTerminalKey({ host: mounted.host, key: "ArrowLeft", keyCode: 37, metaKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, metaKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowLeft", keyCode: 37, altKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, altKey: true });
dispatchTerminalKey({ host: mounted.host, key: "Backspace", keyCode: 8, metaKey: true });
await nextFrame();
expect(mounted.inputs).toEqual([
@@ -582,18 +548,16 @@ describe("terminal emulator runtime in a real browser", () => {
it("keeps cmd+shift+arrow free for app-level shortcuts", async () => {
await page.viewport(900, 600);
const mounted = createTerminalHost({ width: 720, height: 360 });
const mounted = createTerminalHost({ width: 720, height: 360, isMacLikePlatform: true });
await waitFor({ predicate: () => mounted.sizes.length > 0 });
withNavigatorPlatform({ userAgent: MAC_DESKTOP_UA, platform: "MacIntel" }, () => {
dispatchTerminalKey({
host: mounted.host,
key: "ArrowRight",
keyCode: 39,
metaKey: true,
shiftKey: true,
});
dispatchTerminalKey({
host: mounted.host,
key: "ArrowRight",
keyCode: 39,
metaKey: true,
shiftKey: true,
});
await nextFrame();
@@ -603,14 +567,12 @@ describe("terminal emulator runtime in a real browser", () => {
it("does not remap modified arrows on non-mac platforms", async () => {
await page.viewport(900, 600);
const mounted = createTerminalHost({ width: 720, height: 360 });
const mounted = createTerminalHost({ width: 720, height: 360, isMacLikePlatform: false });
await waitFor({ predicate: () => mounted.sizes.length > 0 });
withNavigatorPlatform({ userAgent: LINUX_DESKTOP_UA, platform: "Linux x86_64" }, () => {
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, metaKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, altKey: true });
});
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, metaKey: true });
dispatchTerminalKey({ host: mounted.host, key: "ArrowRight", keyCode: 39, altKey: true });
await nextFrame();
// Meta+arrow is ignored by xterm; alt+arrow takes xterm's generic encoding.
@@ -60,6 +60,7 @@ export interface TerminalEmulatorRuntimeMountInput {
theme: ITheme;
fontFamily?: string;
fontSize?: number;
isMacLikePlatform?: boolean;
}
export interface TerminalEmulatorRuntimeCallbacks {
@@ -145,7 +146,6 @@ declare global {
}
}
// Read at call time, not module load: keydown handling consults it per event.
function detectMacLikePlatform(): boolean {
return (
typeof navigator !== "undefined" &&
@@ -262,6 +262,7 @@ export class TerminalEmulatorRuntime {
private hasUngatedWrites = false;
private readonly inputModeDecoder = new TextDecoder();
private suppressInput = false;
private isMacLikePlatform = false;
private readonly inputModeTracker = new TerminalInputModeTracker();
private lastInputModeState: TerminalInputModeState = this.inputModeTracker.getState();
private themeBackgroundElements: HTMLElement[] = [];
@@ -453,7 +454,7 @@ export class TerminalEmulatorRuntime {
terminal: Pick<Terminal, "input" | "scrollToBottom">,
event: KeyboardEvent,
): boolean {
if (!detectMacLikePlatform() || hasPendingTerminalModifiers(this.pendingModifiers)) {
if (!this.isMacLikePlatform || hasPendingTerminalModifiers(this.pendingModifiers)) {
return false;
}
const editingShortcutData = resolveMacTerminalEditingShortcut(event);
@@ -474,6 +475,7 @@ export class TerminalEmulatorRuntime {
input.host.innerHTML = "";
this.lastSize = null;
this.isMacLikePlatform = input.isMacLikePlatform ?? detectMacLikePlatform();
this.inputModeTracker.reset();
this.emitInputModeChange();
File diff suppressed because one or more lines are too long