diff --git a/.changeset/fn-6638-terminal-render.md b/.changeset/fn-6638-terminal-render.md new file mode 100644 index 0000000000..a78e84e445 --- /dev/null +++ b/.changeset/fn-6638-terminal-render.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix mobile iOS terminal cell measurement by making xterm font remeasure resilient to strict FontFaceSet shorthand rejection and pinning text-size adjustment on terminal viewports. 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 b2006e0d0b..eee69957c3 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 @@ -14,10 +14,15 @@ resolution_type: code_fix severity: high related_components: - packages/dashboard/app/components/TerminalModal.tsx + - packages/dashboard/app/components/TerminalModal.css - packages/dashboard/app/components/SessionTerminal.tsx + - packages/dashboard/app/components/SessionTerminal.css + - packages/dashboard/app/utils/terminalPreferences.ts - packages/dashboard/app/components/__tests__/TerminalModal.test.tsx - packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx + - packages/dashboard/app/__tests__/terminal-input.test.ts - FN-6390 + - FN-6638 tags: - xterm - font-loading @@ -33,6 +38,10 @@ tags: xterm.js measures character-cell geometry when `terminal.open()` runs. If a custom web font is declared with `font-display: swap`, a cold load can let xterm cache fallback-font metrics and then swap to the real font later. The renderer may keep the stale cell width, producing widely spaced glyphs on mobile/DOM-renderer surfaces. +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. + 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. ## Solution @@ -41,11 +50,12 @@ Keep one canonical paste path and remeasure after font resolution. - Prefer xterm's native helper-textarea paste for Cmd/Ctrl+V; return `true` from the custom key handler so the browser/xterm path runs, and do not read/send clipboard text manually. - Preserve custom copy behavior only for selected text, where suppressing terminal input is intentional. -- After `terminal.open()`, call `document.fonts.load()` for the terminal font stack and await `document.fonts.ready` when the FontFaceSet API exists. +- 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 web font has resolved. +- 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. -`SessionTerminal` is unaffected by the custom-font symptom because it uses a system monospace stack, and unaffected by paste duplication because it does not install a custom paste handler; native xterm paste is its only input path. +`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. ## Regression coverage @@ -54,6 +64,8 @@ Cover the invariant across terminal surfaces and input paths: - Keyboard paste on macOS (`metaKey`) and non-mac (`ctrlKey`) returns `true`, does not call `clipboard.readText()`, and sends exactly one PTY input frame via xterm `onData`. - 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. -- `SessionTerminal` asserts it uses the system monospace stack, does not attach a custom key handler, and sends one native xterm paste input frame. +- 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%. +- `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 0b37c4c9bd..07d4e4ec69 100644 --- a/packages/dashboard/app/__tests__/terminal-input.test.ts +++ b/packages/dashboard/app/__tests__/terminal-input.test.ts @@ -14,6 +14,24 @@ function findHelperTextareaRule(): string { return match?.[1] ?? ""; } +function findTerminalTextSizingRule(): string { + const match = css.match(/\.terminal-xterm\s*,\s*\.terminal-xterm \*\s*\{([^}]*)\}/); + return match?.[1] ?? ""; +} + +function findSessionTerminalTextSizingRule(): string { + const match = css.match( + /\.cli-session-terminal__viewport\s*,\s*\.cli-session-terminal__viewport \*\s*\{([^}]*)\}/, + ); + return match?.[1] ?? ""; +} + +function expectTextSizeAdjustPinned(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*;/); +} + function findTerminalSymbolsFontFaceRule(): string { const fontFaceRules = css.match(/@font-face\s*\{[^}]*\}/g) ?? []; return ( @@ -77,6 +95,14 @@ describe("terminal helper textarea CSS contract", () => { const ruleBody = findHelperTextareaRule(); expect(ruleBody).toMatch(/opacity:\s*0\.01\b/); }); + + it("pins iOS text-size adjustment across the xterm measurement subtree", () => { + expectTextSizeAdjustPinned(findTerminalTextSizingRule()); + }); + + it("pins iOS text-size adjustment on the SessionTerminal xterm viewport", () => { + expectTextSizeAdjustPinned(findSessionTerminalTextSizingRule()); + }); }); describe("FN-6424 terminal symbols font CSS contract", () => { diff --git a/packages/dashboard/app/components/SessionTerminal.css b/packages/dashboard/app/components/SessionTerminal.css index 098397b745..5be9f21275 100644 --- a/packages/dashboard/app/components/SessionTerminal.css +++ b/packages/dashboard/app/components/SessionTerminal.css @@ -122,6 +122,16 @@ background: var(--terminal-bg, var(--bg)); } +/* +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. +*/ +.cli-session-terminal__viewport, +.cli-session-terminal__viewport * { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + .cli-session-terminal__advance-strip { display: flex; align-items: center; diff --git a/packages/dashboard/app/components/SessionTerminal.tsx b/packages/dashboard/app/components/SessionTerminal.tsx index 16ea32a556..0e78e5db58 100644 --- a/packages/dashboard/app/components/SessionTerminal.tsx +++ b/packages/dashboard/app/components/SessionTerminal.tsx @@ -12,6 +12,7 @@ import { TERMINAL_PREFERENCES_KEY, readTerminalPreferences, resolveTerminalFontFamily, + waitForTerminalFontMetrics, } from "../utils/terminalPreferences"; /** @@ -401,6 +402,34 @@ export function SessionTerminal({ /* container not measurable yet */ } + void (async () => { + const fontMetricsSettled = await waitForTerminalFontMetrics( + terminalPreferences.fontSize, + resolvedFontFamily, + ); + if ( + !fontMetricsSettled || + disposed || + xtermRef.current !== term || + fitAddonRef.current !== fitAddon + ) { + return; + } + try { + /* + FNXC:Terminal 2026-06-18-07:15: + SessionTerminal shares TerminalModal's real-iOS DOM/canvas measurement path and the same user-selectable font presets. FN-6638 ruled out stack ordering with the 66.76px-identical diagnostic, so this attach surface must also reapply font options and refit after best-effort FontFaceSet settlement even when iOS rejects the multi-family shorthand; WebGL desktop remains safe because the same invalidation path refreshes renderer metrics without changing renderer selection. + */ + term.options.fontFamily = resolvedFontFamily; + term.options.fontSize = terminalPreferences.fontSize; + (fitAddon as unknown as { fit: () => void }).fit(); + sendResize(term.cols, term.rows); + term.refresh(0, Math.max(0, term.rows - 1)); + } catch { + /* ignore teardown or transient measure failures */ + } + })(); + // term.onData → input frames (skip entirely when read-only). if (!readOnly) { term.onData((data: string) => { diff --git a/packages/dashboard/app/components/TerminalModal.css b/packages/dashboard/app/components/TerminalModal.css index 1a6ace21c7..04dab42dd6 100644 --- a/packages/dashboard/app/components/TerminalModal.css +++ b/packages/dashboard/app/components/TerminalModal.css @@ -545,6 +545,16 @@ FN-6603 found that unicode-range scoping is not enough when the symbols face is padding: var(--space-xs); } +/* +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. +*/ +.terminal-xterm, +.terminal-xterm * { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + /* * xterm fit may apply inline pixel heights on the `.xterm` root after * row/line-height recomputation (e.g. after font-size changes). Keep the root diff --git a/packages/dashboard/app/components/TerminalModal.tsx b/packages/dashboard/app/components/TerminalModal.tsx index 51b6d7804f..80b8342fd3 100644 --- a/packages/dashboard/app/components/TerminalModal.tsx +++ b/packages/dashboard/app/components/TerminalModal.tsx @@ -24,6 +24,7 @@ import { clampTerminalFontSize, readTerminalPreferences, resolveTerminalFontFamily, + waitForTerminalFontMetrics, writeTerminalPreferences, type TerminalPreferences, type TerminalRenderer, @@ -490,16 +491,12 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG terminal: XTerm, fitAddon: InstanceType, ) => { - if (typeof document === "undefined" || !document.fonts?.load) { - return; - } + const fontMetricsSettled = await waitForTerminalFontMetrics( + fontSizeRef.current, + resolvedFontFamilyRef.current, + ); - try { - await document.fonts.load(`${fontSizeRef.current}px ${resolvedFontFamilyRef.current}`); - await document.fonts.ready; - } catch { - // Font loading support is best-effort; keep the terminal usable if the - // browser rejects due to permissions, syntax, or unsupported APIs. + if (!fontMetricsSettled) { return; } @@ -512,11 +509,10 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG } try { - // xterm measures cell geometry at open() time. The terminal Nerd Font - // is loaded with font-display: swap, so a cold load can replace the - // fallback font after open(); re-applying font options and fitting after - // FontFaceSet resolution forces the DOM/canvas and WebGL renderers to - // remeasure against the actual glyph metrics. + /* + FNXC:Terminal 2026-06-18-07:23: + FN-6638 recurrence #4 showed the previous symbols-last stack-order fix was inert: the supplied diagnostic measured AGENTS.md at the same 66.76px for symbols-first, symbols-last, and system-mono stacks while real iOS Safari still widened ASCII cells. xterm measures cell geometry at open() time, so after best-effort FontFaceSet settlement we must always reapply the active preset's font options, fit, resize, and refresh; that invalidates stale DOM/canvas metrics on real iOS when the full shorthand is rejected and keeps desktop WebGL using the same renderer-neutral metric refresh. + */ terminal.options.fontFamily = resolvedFontFamilyRef.current; terminal.options.fontSize = fontSizeRef.current; fitAddon.fit(); diff --git a/packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx b/packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx index 7ff9e12252..e03d513909 100644 --- a/packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx +++ b/packages/dashboard/app/components/__tests__/SessionTerminal.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { act, render, screen, fireEvent, waitFor } from "@testing-library/react"; // ── Mock xterm + addon dynamic imports (jsdom has no canvas/WebGL) ────────── const mockFitAddon = { fit: vi.fn() }; @@ -9,6 +9,7 @@ const mockTerm = { onData: vi.fn(), attachCustomKeyEventHandler: vi.fn(), write: vi.fn((_data: string, cb?: () => void) => cb?.()), + refresh: vi.fn(), dispose: vi.fn(), unicode: { activeVersion: "6" }, options: {} as Record, @@ -85,8 +86,13 @@ beforeEach(() => { mockTerm.onData.mockReset(); mockTerm.attachCustomKeyEventHandler.mockClear(); mockTerm.write.mockClear(); + mockTerm.refresh.mockClear(); mockTerm.dispose.mockClear(); mockTerm.options = {}; + Object.defineProperty(document, "fonts", { + value: undefined, + configurable: true, + }); mockFitAddon.fit.mockClear(); apiMock.mockReset(); apiMock.mockResolvedValue({ ticket: "tkt-1", expiresAt: "", readOnly: false }); @@ -154,6 +160,39 @@ describe("SessionTerminal", () => { ]); }); + it("refits after font settlement even when iOS rejects the font-load shorthand", async () => { + const load = vi.fn(() => Promise.reject(new DOMException("Invalid font shorthand"))); + Object.defineProperty(document, "fonts", { + value: { + load, + ready: Promise.resolve(), + }, + configurable: true, + }); + const fitCallBaseline = mockFitAddon.fit.mock.calls.length; + + render(); + + await waitFor(() => { + expect(FakeWS.instances.length).toBe(1); + expect(load).toHaveBeenCalledWith( + expect.stringContaining("Fusion Terminal Nerd Font Symbols"), + ); + }); + + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(mockTerm.options.fontFamily).toBe(resolveTerminalFontFamily("nerd-font")); + expect(mockTerm.options.fontSize).toBe(DEFAULT_TERMINAL_PREFERENCES.fontSize); + expect(mockFitAddon.fit.mock.calls.length).toBeGreaterThan(fitCallBaseline); + expect(mockTerm.refresh).toHaveBeenCalledWith(0, mockTerm.rows - 1); + }); + }); + it("applies validated terminal preferences at xterm init", async () => { const { Terminal } = await import("@xterm/xterm"); window.localStorage.setItem( diff --git a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx index 7c314366a2..809ffa80cf 100644 --- a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx @@ -4694,6 +4694,44 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => { }); }); + it("still refits xterm when iOS rejects the multi-family font-load shorthand", async () => { + const load = vi.fn(() => Promise.reject(new DOMException("Invalid font shorthand"))); + Object.defineProperty(document, "fonts", { + value: { + load, + ready: Promise.resolve(), + }, + configurable: true, + }); + + const fitCallBaseline = mockFitAddonFit.mock.calls.length; + + render(); + + await waitFor(() => { + expect(mockTerminalInstance.open).toHaveBeenCalled(); + expect(load).toHaveBeenCalledWith( + expect.stringContaining("Fusion Terminal Nerd Font Symbols"), + ); + }); + + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(mockTerminalInstance.options.fontFamily).toBe(XTERM_FONT_FAMILY); + expect(mockTerminalInstance.options.fontSize).toBe(DEFAULT_TERMINAL_PREFERENCES.fontSize); + expect(mockFitAddonFit.mock.calls.length).toBeGreaterThan(fitCallBaseline); + expect(mockResize).toHaveBeenCalledWith( + mockTerminalInstance.cols, + mockTerminalInstance.rows, + ); + expect(mockTerminalInstance.refresh).toHaveBeenCalledWith(0, mockTerminalInstance.rows - 1); + }); + }); + it("leaves unrelated key handling untouched", async () => { render(); diff --git a/packages/dashboard/app/utils/__tests__/terminalPreferences.test.ts b/packages/dashboard/app/utils/__tests__/terminalPreferences.test.ts index 7337a4157c..291b69c128 100644 --- a/packages/dashboard/app/utils/__tests__/terminalPreferences.test.ts +++ b/packages/dashboard/app/utils/__tests__/terminalPreferences.test.ts @@ -1,9 +1,11 @@ -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_TERMINAL_PREFERENCES, LEGACY_TERMINAL_FONT_SIZE_KEY, TERMINAL_PREFERENCES_KEY, + XTERM_FONT_FAMILY, readTerminalPreferences, + waitForTerminalFontMetrics, writeTerminalPreferences, } from "../terminalPreferences"; @@ -87,4 +89,29 @@ describe("terminalPreferences", () => { expect(readTerminalPreferences()).toEqual(written); expect(localStorage.getItem(LEGACY_TERMINAL_FONT_SIZE_KEY)).toBe("22"); }); + + it("keeps terminal font metrics wait best-effort when iOS rejects the full stack shorthand", async () => { + let readyAwaited = false; + const load = vi.fn((font: string) => { + if (font.includes(",")) { + return Promise.reject(new DOMException("Invalid font shorthand")); + } + return Promise.resolve([]); + }); + const ready = Promise.resolve().then(() => { + readyAwaited = true; + }); + + await expect( + waitForTerminalFontMetrics(12, XTERM_FONT_FAMILY, { + load, + ready, + }), + ).resolves.toBe(true); + + expect(load).toHaveBeenCalledWith(expect.stringContaining("MesloLGS NF")); + expect(load).toHaveBeenCalledWith("12px \"MesloLGS NF\""); + expect(load).toHaveBeenCalledWith("12px \"Fusion Terminal Nerd Font Symbols\""); + expect(readyAwaited).toBe(true); + }); }); diff --git a/packages/dashboard/app/utils/terminalPreferences.ts b/packages/dashboard/app/utils/terminalPreferences.ts index c473abe3a0..1d879d6d8b 100644 --- a/packages/dashboard/app/utils/terminalPreferences.ts +++ b/packages/dashboard/app/utils/terminalPreferences.ts @@ -71,6 +71,113 @@ export function resolveTerminalFontFamily(fontFamily: TerminalFontFamily): strin ); } +const CSS_GENERIC_FONT_FAMILIES = new Set([ + "serif", + "sans-serif", + "monospace", + "cursive", + "fantasy", + "system-ui", + "ui-serif", + "ui-sans-serif", + "ui-monospace", + "ui-rounded", + "emoji", + "math", + "fangsong", +]); + +type TerminalFontFaceSet = { + load?: (font: string, text?: string) => PromiseLike; + ready?: PromiseLike; +}; + +export function splitTerminalFontFamilies(stack: string): string[] { + return stack + .split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/) + .map((family) => family.trim()) + .filter(Boolean); +} + +function normalizeFontFamilyName(family: string): string { + const trimmed = family.trim(); + if ( + (trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { + return trimmed.slice(1, -1).trim(); + } + return trimmed; +} + +function isLoadableConcreteFontFamily(family: string): boolean { + const normalized = normalizeFontFamilyName(family).toLowerCase(); + return normalized !== "" && !CSS_GENERIC_FONT_FAMILIES.has(normalized); +} + +function getDocumentFonts(): TerminalFontFaceSet | undefined { + if (typeof document === "undefined") { + return undefined; + } + return document.fonts; +} + +async function settleFontLoad(fonts: TerminalFontFaceSet, font: string): Promise { + if (!fonts.load) { + return false; + } + + try { + await fonts.load(font); + return true; + } catch { + // Best-effort: strict iOS FontFaceSet parsing can reject one shorthand while + // later declarations or fonts.ready still give xterm a safe remeasure point. + return false; + } +} + +export async function waitForTerminalFontMetrics( + fontSize: number, + fontFamily: string, + fonts: TerminalFontFaceSet | undefined = getDocumentFonts(), +): Promise { + if (!fonts?.load) { + return false; + } + + const fontSizeCss = `${fontSize}px`; + const declarations = [ + `${fontSizeCss} ${fontFamily}`, + ...splitTerminalFontFamilies(fontFamily) + .filter(isLoadableConcreteFontFamily) + .map((family) => `${fontSizeCss} ${family}`), + ]; + + /* + FNXC:Terminal 2026-06-18-07:02: + FN-6638 recurrence #4 showed font-stack order was inert: the supplied diagnostic measured AGENTS.md at the same 66.76px with symbols-first, symbols-last, and system-mono stacks while real iOS Safari still rendered wide ASCII cells. Treat FontFaceSet loading as best-effort and always leave callers free to reapply xterm font options; strict iOS WebKit can reject the long multi-family shorthand, and that rejection must not suppress DOM/canvas or WebGL metric invalidation for any preset. + */ + const [fullStackDeclaration, ...individualDeclarations] = declarations; + const fullStackLoaded = fullStackDeclaration + ? await settleFontLoad(fonts, fullStackDeclaration) + : false; + + if (!fullStackLoaded) { + for (const declaration of individualDeclarations) { + await settleFontLoad(fonts, declaration); + } + } + + try { + await fonts.ready; + } catch { + // Continue to xterm remeasure even if the FontFaceSet settles rejected. + } + + return true; +} + function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }