fix(ui): render timeline text with the line breaks it was measured with #175
- IA
Closes #168. Long timeline messages could hide their last line. Pretext measured each text block's height, but the browser wrapped the HTML on its own, inside a box that `overflow: hidden` clipped to that height.
**Cause** (reproduced in a real browser with the exact message from the issue: at a 706px bubble it ends `…save your` and `manifest.` is clipped) - **Break rules differ.** Pretext breaks after `/` in `body/meta` and `#23/#34`. Chromium doesn't break there. - **Widths drift.** The DOM line came out about 1px wider than Pretext's 334.86px at a 335px fit. The bubble shrink-wraps to Pretext's widest line, so any drift moves a word down a line. - **Font loading.** Text measured before Inter Variable loaded kept fallback-font widths in Pretext's caches. On a cold load, 700 of 741 widths clipped. - **Root font size.** Mention, document chip, list and quote sizes and the lane width were in rem, but the layout math is fixed px. At 12px and 20px roots, lines overflowed or clipped.
**Fix** - Text blocks render at Pretext's own breaks (`wrapsAt` in `blocks/text.tsx`), for both settled and streaming text. Each block is `white-space: nowrap`, and each wrap is a `::before` line break, so copied text keeps its original spacing. Rich-inline cursors map back to source offsets through per-item tables from `prepareWithSegments`, so collapsed whitespace, soft hyphens and surrogate pairs land on the right offsets. Each `Line` caches only its latest width. - Overflow stays clipped vertically. Horizontally it is visible, so sub-pixel drift paints into the bubble padding instead of cutting glyphs. - An unbreakable token wider than its line stays clipped to the measured width, the same bounded behavior as before. A user bubble no longer grows wider than the lane for such a token. Revealing the clipped text is #174. - `timeline/layout.ts` clears Pretext's caches and the parse and measurement caches when web fonts finish loading. Mounted timelines then reparse. A parse that was already running when fonts changed isn't cached, and `document.fonts.ready` is checked again after subscribing. - Geometry now uses the same units as the layout math: - Chips, list indent and quote indent in measured text blocks use the px constants. The `.content.measured` scope leaves the rich-text composer's sizes alone. - `--main-content-max` is `832px`, matching `LANE`. - The avatar gap uses `AVATAR_GAP`.
**Validation** at head `9570146`, rebased on `10f06d2`. It merges cleanly with current main. - `bun types` and `bun run ci` passed. - React Doctor on the changed files reported no diagnostics. Its project score is 63/100. - **Headless browser harness.** This is real Chromium running the real `Timeline` and app `toEvents`, with the backed-up message from issue-triage entry 200 plus assistant, rich (mention, link, inline code, multi-line), list and quote messages. It checks every width from 360 to 1100px (741 widths). A width fails on any vertical overflow, a hidden last character, a row taller than its virtual height, paint past the bubble or viewport, or copied text that doesn't match. - **Before**, with fonts preloaded: 71 widths failed at a 16px root, 472 at 12px and 561 at 20px. On a cold font load, 700 failed. - **After**: 0 widths failed at 16px, 12px and 20px roots and on a cold font load. 0 widths failed on the streaming render path with a fixed draft, which doesn't exercise incremental chunks or the settle transition; the real provider exchange below streamed for real. Text painted at most 0.8px into the padding. - A 1,050px inline code token is clipped inside its bubble both before and after. - **Native.** A signed Ace-dev build of this head ran with a real Anthropic `claude-opus-5-5` exchange: the long human message with a mention and the same `/` tokens, a rich reply and a list. After a relaunch, an Ace agent used native desktop tools to capture the window at 1100px wide. Both bubbles end with `save your manifest.` fully visible, with no clipped glyphs. - Narrow native widths are **unverified**: at 760px and 640px the details panel covered the chat. The browser sweep covers narrow widths.
### Built in Ace
The code, checks, commits and native runs were made in an Ace channel, except for: - **Codex.** Codex coordinated and did read-only review outside Ace (#9). - **Headless Chrome.** The browser harness ran in headless Chrome with a temporary profile, launched from the shell, and reported DOM measurements to a local Vite server. Ace's browser tools can't read DOM measurements or drive input (#50). - **Issue screenshot.** The private screenshot from #168 couldn't be read with Ace's tools (#51). - **Native desktop tools.** The channel's own source worker couldn't use them (#128), so a separate agent channel on the Ace-dev host ran the native checks. - **Native run issues.** These weren't investigated further: - Ace-dev exited once without explanation, with code 0. - After the host restarted, an interrupted agent run resumed and controlled the same window as the second agent. - Point clicks and scrolls were refused or had no effect, and the app's message text isn't in the accessibility tree (#8). - `desktop_activate` timed out with an unknown result (#73).
iamnbutlermerged ad3f7313ea18last month on GitHub