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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7460-ios-terminal-spacing.md
Normal file
7
.changeset/fn-7460-ios-terminal-spacing.md
Normal file
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user