From 7300bf54ce52af73d4de677fecba1fa8be6e1a83 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 3 Jul 2026 00:25:49 -0700 Subject: [PATCH] FN-7460: disable WebKit text scaling in terminals Disable WebKit terminal text scaling so iPhone Safari preserves exact xterm cell metrics. - Set terminal xterm host subtrees to disable text-size adjustment instead of pinning it at 100%. - Update terminal CSS contract coverage for modal and session terminal surfaces. - Document the real-iPhone recurrence and add a patch changeset for the published CLI package. Files changed: .changeset/fn-7460-ios-terminal-spacing.md | 7 +++++++ .../ui-bugs/xterm-async-font-remeasure-paste-dedupe.md | 7 ++++--- packages/dashboard/app/__tests__/terminal-input.test.ts | 14 +++++++------- packages/dashboard/app/components/SessionTerminal.css | 7 +++++-- packages/dashboard/app/components/TerminalModal.css | 9 ++++++--- .../app/components/__tests__/TerminalModal.test.tsx | 7 +++++++ 6 files changed, 36 insertions(+), 15 deletions(-) Fusion-Task-Id: FN-7460 Fusion-Task-Lineage: 00542017-457b-40b8-9139-084218bc3533 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7460-ios-terminal-spacing.md | 7 +++++++ .../xterm-async-font-remeasure-paste-dedupe.md | 7 ++++--- .../dashboard/app/__tests__/terminal-input.test.ts | 14 +++++++------- .../dashboard/app/components/SessionTerminal.css | 7 +++++-- .../dashboard/app/components/TerminalModal.css | 9 ++++++--- .../components/__tests__/TerminalModal.test.tsx | 7 +++++++ 6 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 .changeset/fn-7460-ios-terminal-spacing.md diff --git a/.changeset/fn-7460-ios-terminal-spacing.md b/.changeset/fn-7460-ios-terminal-spacing.md new file mode 100644 index 0000000000..dc52ceeee5 --- /dev/null +++ b/.changeset/fn-7460-ios-terminal-spacing.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix iPhone Safari terminal text spacing with the keyboard open. +category: fix +dev: Disables WebKit text-size adjustment inside dashboard xterm measurement subtrees. diff --git a/docs/solutions/ui-bugs/xterm-async-font-remeasure-paste-dedupe.md b/docs/solutions/ui-bugs/xterm-async-font-remeasure-paste-dedupe.md index eee69957c3..aafcc1cb4e 100644 --- a/docs/solutions/ui-bugs/xterm-async-font-remeasure-paste-dedupe.md +++ b/docs/solutions/ui-bugs/xterm-async-font-remeasure-paste-dedupe.md @@ -23,6 +23,7 @@ related_components: - packages/dashboard/app/__tests__/terminal-input.test.ts - FN-6390 - FN-6638 + - FN-7460 tags: - xterm - font-loading @@ -40,7 +41,7 @@ xterm.js measures character-cell geometry when `terminal.open()` runs. If a cust FN-6638 was the fourth recurrence of the mobile wide-cell defect (FN-6390 → FN-6424 → FN-6603 → FN-6638). The FN-6603 font-stack ordering hypothesis was ruled out: the supplied diagnostic measured `66.76px for AGENTS.md` identically for symbols-first, symbols-last, and system-mono stacks, and desktop/mobile-emulated WebKit rendered ASCII tightly while a real iOS Safari screenshot still showed `A G E N T S . m d`. Treat Playwright/desktop WebKit emulation as a blind spot for this class; it can prove CSS contracts and fallback paths but cannot be the acceptance surface. -The recurrence path was stricter real-iOS font/text measurement behavior. A long `document.fonts.load(`${fontSize}px ${resolvedFontFamily}`)` shorthand can reject on iOS WebKit; returning from that catch prevented xterm from reapplying `fontFamily`/`fontSize`, running `fitAddon.fit()`, publishing resize, and refreshing rows. Separately, the xterm measurement subtree lacked `-webkit-text-size-adjust: 100%`, allowing iOS Safari text inflation to perturb cell metrics. +The recurrence path was stricter real-iOS font/text measurement behavior. A long `document.fonts.load(`${fontSize}px ${resolvedFontFamily}`)` shorthand can reject on iOS WebKit; returning from that catch prevented xterm from reapplying `fontFamily`/`fontSize`, running `fitAddon.fit()`, publishing resize, and refreshing rows. Separately, the xterm measurement subtree must disable WebKit text-size adjustment entirely. FN-7460 reopened the issue after the 100% pin still let a real iPhone Safari keyboard-open 12px terminal render prompt and ASCII segments with excessive inter-character spacing. A second pitfall is custom paste handling. If an `attachCustomKeyEventHandler` Cmd/Ctrl+V branch reads `navigator.clipboard.readText()` and forwards that text to the PTY while the browser also performs the native paste into xterm's helper textarea, the same payload reaches `terminal.onData` and is sent twice. @@ -53,7 +54,7 @@ Keep one canonical paste path and remeasure after font resolution. - After `terminal.open()`, treat FontFaceSet loading as best-effort: try the full stack, fall back to concrete individual families only if the full shorthand rejects, await `document.fonts.ready`, and never let an iOS shorthand rejection skip the later remeasure. - Guard async remeasure work with the expected session id and current terminal/addon refs so stale font-load promises cannot mutate a disposed or switched terminal. - Reapply font options, run `fitAddon.fit()`, publish the resized cols/rows, and refresh visible rows once the FontFaceSet has settled. -- Pin `-webkit-text-size-adjust: 100%` / `text-size-adjust: 100%` on the xterm host subtree (`.terminal-xterm` and `.cli-session-terminal__viewport`) so iOS Safari cannot inflate DOM/canvas measurement nodes. +- Disable `-webkit-text-size-adjust` / `text-size-adjust` on the xterm host subtree (`.terminal-xterm` and `.cli-session-terminal__viewport`) so iOS Safari cannot inflate DOM/canvas measurement nodes while xterm still honors the user's exact 10px/12px terminal font preference. `SessionTerminal` is unaffected by paste duplication because it does not install a custom paste handler; native xterm paste is its only input path. It is affected by the font/cell-measurement invariant because it constructs xterm with the same user-selectable font presets and mobile DOM/canvas renderer path, so it must share both the best-effort font-load remeasure and the text-size-adjust pin. @@ -65,7 +66,7 @@ Cover the invariant across terminal surfaces and input paths: - Native helper-textarea paste without the shortcut handler sends exactly once, covering mobile/iOS context-menu paste. - A controlled `document.fonts.load()` promise resolving after `terminal.open()` triggers a post-font-load fit, resize, and refresh. - A controlled `document.fonts.load()` rejection (the real-iOS shorthand failure mode) still triggers font option reapply, fit/resize, and refresh for both `TerminalModal` and `SessionTerminal`. -- CSS contract tests assert both xterm host subtrees pin `text-size-adjust` to 100%. +- CSS contract tests assert both xterm host subtrees disable `text-size-adjust` for exact xterm cell metrics. - `SessionTerminal` asserts it uses the shared terminal font presets, does not attach a custom key handler, and sends one native xterm paste input frame. This avoids downstream byte de-duplication and fixes the two root causes at their renderer/input seams. diff --git a/packages/dashboard/app/__tests__/terminal-input.test.ts b/packages/dashboard/app/__tests__/terminal-input.test.ts index 666986da52..135dd607fc 100644 --- a/packages/dashboard/app/__tests__/terminal-input.test.ts +++ b/packages/dashboard/app/__tests__/terminal-input.test.ts @@ -50,10 +50,10 @@ function findSessionTerminalGlyphFallbackRule(): string { return match?.[1] ?? ""; } -function expectTextSizeAdjustPinned(ruleBody: string): void { +function expectTextSizeAdjustDisabledForExactXtermMetrics(ruleBody: string): void { expect(ruleBody).not.toBe(""); - expect(ruleBody).toMatch(/-webkit-text-size-adjust\s*:\s*100%\s*;/); - expect(ruleBody).toMatch(/text-size-adjust\s*:\s*100%\s*;/); + expect(ruleBody).toMatch(/-webkit-text-size-adjust\s*:\s*none\s*;/); + expect(ruleBody).toMatch(/text-size-adjust\s*:\s*none\s*;/); } function findTerminalSymbolsFontFaceRule(cssSource = css): string { @@ -120,12 +120,12 @@ describe("terminal helper textarea CSS contract", () => { expect(ruleBody).toMatch(/opacity:\s*0\.01\b/); }); - it("pins iOS text-size adjustment across the xterm measurement subtree", () => { - expectTextSizeAdjustPinned(findTerminalTextSizingRule()); + it("disables iOS text-size adjustment across the xterm measurement subtree", () => { + expectTextSizeAdjustDisabledForExactXtermMetrics(findTerminalTextSizingRule()); }); - it("pins iOS text-size adjustment on the SessionTerminal xterm viewport", () => { - expectTextSizeAdjustPinned(findSessionTerminalTextSizingRule()); + it("disables iOS text-size adjustment on the SessionTerminal xterm viewport", () => { + expectTextSizeAdjustDisabledForExactXtermMetrics(findSessionTerminalTextSizingRule()); }); it("keeps a DOM glyph fallback mechanism outside xterm measurement options", () => { diff --git a/packages/dashboard/app/components/SessionTerminal.css b/packages/dashboard/app/components/SessionTerminal.css index fcd16d4647..c8d8661172 100644 --- a/packages/dashboard/app/components/SessionTerminal.css +++ b/packages/dashboard/app/components/SessionTerminal.css @@ -136,11 +136,14 @@ FN-6811 recurrence #6 found the attach terminal imported its own CSS chunk but d /* FNXC:Terminal 2026-06-18-07:34: SessionTerminal hosts the same xterm DOM/canvas measurement subtree as TerminalModal under a different wrapper. Apply the FN-6638 real-iOS text-size-adjust invariant here too so CLI-agent attach terminals do not inherit Safari-inflated ASCII cell metrics while still using the shared font-load remeasure path for every preset and desktop WebGL. + +FNXC:Terminal 2026-07-03-00:14: +Mirror TerminalModal's post-FN-7456 iPhone fix on the embedded attach surface: iOS keyboard-open 10px/12px terminal cells must ignore browser text-size inflation while keeping the user's exact terminal font-size preference and DOM-only prompt glyph fallback. */ .cli-session-terminal__viewport, .cli-session-terminal__viewport * { - -webkit-text-size-adjust: 100%; - text-size-adjust: 100%; + -webkit-text-size-adjust: none; + text-size-adjust: none; } /* diff --git a/packages/dashboard/app/components/TerminalModal.css b/packages/dashboard/app/components/TerminalModal.css index f10239adec..8b4867a034 100644 --- a/packages/dashboard/app/components/TerminalModal.css +++ b/packages/dashboard/app/components/TerminalModal.css @@ -953,12 +953,15 @@ The terminal header pop-out/dock affordance is an icon-only utility control. It /* FNXC:Terminal 2026-06-18-07:04: -Real iOS Safari recurrence #4 kept wide ASCII cells even after unicode-range scoping and symbols-last ordering; the supplied harness measured AGENTS.md at the same 66.76px for every stack, so ordering is ruled out. Pin text-size adjustment across xterm's measurement subtree so mobile WebKit cannot inflate DOM/canvas cell metrics, while WebGL/desktop keep the same 100% text scale and every terminal preset still resolves through terminalPreferences. +Real iOS Safari recurrence #4 kept wide ASCII cells even after unicode-range scoping and symbols-last ordering; the supplied harness measured AGENTS.md at the same 66.76px for every stack, so ordering is ruled out. Pin text-size adjustment across xterm's measurement subtree so mobile WebKit cannot inflate DOM/canvas cell metrics, while WebGL/desktop keep the same exact user-selected terminal font size and every terminal preset still resolves through terminalPreferences. + +FNXC:Terminal 2026-07-03-00:10: +Post-FN-7456 real iPhone Safari can still render initial keyboard-open 12px terminal text with excessive inter-character spacing when browser text-size adjustment inflates the xterm measurement subtree. Disable text-size adjustment, not the terminal font-size preference, so WebKit measures the same 10px/12px ASCII cells that xterm renders before any close/open, orientation, reconnect, or font reset can repair spacing. */ .terminal-xterm, .terminal-xterm * { - -webkit-text-size-adjust: 100%; - text-size-adjust: 100%; + -webkit-text-size-adjust: none; + text-size-adjust: none; } /* diff --git a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx index 5cc9df833a..faf7978c34 100644 --- a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx @@ -34,6 +34,12 @@ function expectMeasurementSafeFontStack(stack: string): void { expect(families).not.toContain(TERMINAL_SYMBOLS_FONT_FAMILY); } +function expectTextSizeAdjustmentDisabledForExactXtermMetrics(cssSource: string): void { + const match = cssSource.match(/\.terminal-xterm\s*,\s*\.terminal-xterm \*\s*\{([^}]*)\}/); + expect(match?.[1] ?? "").toMatch(/-webkit-text-size-adjust\s*:\s*none\s*;/); + expect(match?.[1] ?? "").toMatch(/text-size-adjust\s*:\s*none\s*;/); +} + // Mock hooks and API vi.mock("../../hooks/useTerminal", () => ({ useTerminal: vi.fn(), @@ -5095,6 +5101,7 @@ describe("TerminalModal — FN-872 real-device keyboard overlap refinement", () await waitFor(() => expect(resizeForInitialIOSKeyboard).toHaveBeenCalledWith(80, 24)); expectMeasurementSafeFontStack(mockTerminalInstance.options.fontFamily as string); expect(mockTerminalInstance.options.fontSize).toBe(12); + expectTextSizeAdjustmentDisabledForExactXtermMetrics(terminalModalCss); } finally { helperTextarea.remove(); Object.defineProperty(window, "screen", { configurable: true, value: originalScreen });