diff --git a/experiments/screenshots/image1-slider.png b/experiments/screenshots/image1-slider.png new file mode 100644 index 00000000..de447a4a Binary files /dev/null and b/experiments/screenshots/image1-slider.png differ diff --git a/experiments/screenshots/image2-plan.png b/experiments/screenshots/image2-plan.png new file mode 100644 index 00000000..77848d78 Binary files /dev/null and b/experiments/screenshots/image2-plan.png differ diff --git a/experiments/screenshots/image3-strike.png b/experiments/screenshots/image3-strike.png new file mode 100644 index 00000000..9baf8661 Binary files /dev/null and b/experiments/screenshots/image3-strike.png differ diff --git a/experiments/screenshots/image4-system-bug.png b/experiments/screenshots/image4-system-bug.png new file mode 100644 index 00000000..e37740c8 Binary files /dev/null and b/experiments/screenshots/image4-system-bug.png differ diff --git a/experiments/terminal-font-ready-analysis.md b/experiments/terminal-font-ready-analysis.md new file mode 100644 index 00000000..2130009a --- /dev/null +++ b/experiments/terminal-font-ready-analysis.md @@ -0,0 +1,75 @@ +# Terminal rendering bugs investigation — issue #273 + +## Symptoms reported + +Screenshots gathered from the issue and follow-up comments (saved under +`experiments/screenshots/`): + +| File | Visible defect | +| --- | --- | +| `image1-slider.png` | Typed text (`asd`) appears below the prompt bar instead of inside it; the input slider falls outside the visible row. | +| `image2-plan.png` | Claude Code plan dialog (`Yes, and bypass permissions / manually approve / refine / Tell Claude what to change`) is visible but key presses do not select an option. | +| `image3-strike.png` | A line of typed text (`больше уязвимостей в коде нету?`) renders with a horizontal stroke crossing every character cell. | +| `image4-system-bug.png` | Chrome devtools issue panel reports "A form field element should have an id or name attribute" twice. | + +All four are observed in the web terminal that hosts the Claude Code TUI +(see `packages/app/src/web/terminal-panel-runtime-core.ts`). + +## Root cause + +xterm.js measures the character cell once when `terminal.open(host)` is +called and again whenever `FitAddon.fit()` runs. The measurement is taken +from a canvas `measureText` call using the configured `fontFamily`. + +`packages/app/src/web/terminal-panel-runtime-core.ts:79` configures +`fontFamily: "'IBM Plex Mono', 'SFMono-Regular', monospace"`. `IBM Plex +Mono` is loaded asynchronously from Google Fonts in +`packages/app/index.html:9-12`: + +```html + +``` + +`display=swap` means the browser paints the fallback (`SFMono-Regular` or +generic `monospace`) until the webfont arrives. xterm.js initialises +before the swap, caches the fallback metrics, and never rechecks them. As +soon as `IBM Plex Mono` is applied the on-screen glyphs grow/shrink +relative to the cell grid, which is what produces: + +* the cursor/prompt offset in `image1-slider.png`; +* the apparent strikethrough in `image3-strike.png` — every cell paints a + background underline/strikethrough decoration on its baseline, but the + glyphs from the new font are rendered at a different vertical offset, + so the decoration cuts through the middle of the characters; +* the unclickable plan options in `image2-plan.png` — xterm.js maps the + pointer to the wrong text cell because the cached cell width no longer + matches the painted width, so the buttons (`1/2/3/4` rows rendered by + the TUI) never receive focus when clicked. + +`image4-system-bug.png` is unrelated to fonts: the "Show system" toggle +in `packages/app/src/web/panel-tasks.tsx:54` declares an `` without `id`/`name`, and Chrome's Issues panel flags +two such elements. + +## Fix outline + +1. Resolve `IBM Plex Mono` from `document.fonts.ready` (and explicit + `document.fonts.load(...)` requests) before mounting the xterm.js + instance. While the font is still loading, keep the host hidden so + that the canvas measurement is taken with the final font metrics. +2. After mount, re-run `fitAddon.fit()` whenever `document.fonts` reports + that the relevant face has loaded, to handle slow networks. +3. Provide an `id` and `name` on the checkbox so Chrome stops flagging + it. + +## Verification + +* `terminal-font-readiness.test.ts` exercises the new helper with mock + `FontFaceSet` implementations covering the happy path, the timeout + path, and the environment without `document.fonts` support. +* Manual: run `bun run --cwd packages/app dev:web`, throttle network to + "Slow 3G" in devtools, and confirm the terminal stays empty until the + webfont arrives, then renders without strikethrough artifacts. diff --git a/packages/app/src/web/panel-tasks.tsx b/packages/app/src/web/panel-tasks.tsx index 39811f92..ece9cbae 100644 --- a/packages/app/src/web/panel-tasks.tsx +++ b/packages/app/src/web/panel-tasks.tsx @@ -53,6 +53,8 @@ const TaskSystemToggle = (