Tooltips truncate their text with plenty of room #310

Closed
opened 2026-11-02 14:02:22 +00:00 by kayg · 7 comments
Owner

Owner report (production https://ssh.kayg.org, 2026-11-02): tooltips get truncated even when there is plenty of room. A sidebar row's action group (info, pencil, trash) shows a tooltip reading "In…" over the info button; the same happens on the window split icons.

Verbatim: "this is a new tooltip problem i noticed even in the production deployment is that they get truncated even when there's enough space? happens on the split window icons as well".

Expected: tooltips size to their text up to a sensible maximum, wrap instead of ellipsizing, and flip or shift to stay inside the viewport. Fix once in the shared tooltip (packages/ui/src/tooltip.ts, packages/ui/styles.css); no per-call-site patches.

Plan (about 2 h): reproduce on a dev VM with a production build, find the root cause, fix in the shared component, add a Playwright spec that hovers every tooltip-carrying icon button at 1440x900 and 393x852 (full text visible, scrollWidth <= clientWidth, box inside viewport), add a TASTE.md row, attach screenshots here.

Owner report (production https://ssh.kayg.org, 2026-11-02): tooltips get truncated even when there is plenty of room. A sidebar row's action group (info, pencil, trash) shows a tooltip reading "In…" over the info button; the same happens on the window split icons. Verbatim: "this is a new tooltip problem i noticed even in the production deployment is that they get truncated even when there's enough space? happens on the split window icons as well". Expected: tooltips size to their text up to a sensible maximum, wrap instead of ellipsizing, and flip or shift to stay inside the viewport. Fix once in the shared tooltip (`packages/ui/src/tooltip.ts`, `packages/ui/styles.css`); no per-call-site patches. Plan (about 2 h): reproduce on a dev VM with a production build, find the root cause, fix in the shared component, add a Playwright spec that hovers every tooltip-carrying icon button at 1440x900 and 393x852 (full text visible, scrollWidth <= clientWidth, box inside viewport), add a TASTE.md row, attach screenshots here.
Author
Owner

Start: branch j107-tooltip-truncation, base origin/dev 17d282a187. Worktree ~/Developer/Worktrees/infraternal-j107.

Start: branch `j107-tooltip-truncation`, base `origin/dev` 17d282a187611c85aca4058fac161268454c5b8d. Worktree `~/Developer/Worktrees/infraternal-j107`.
Author
Owner

Finding (root cause, from reading the code; reproduction on a production build follows):

packages/ui/src/tooltip.ts place() sized the surface as ceil(measured text width) + 22 px, a constant that assumed the surface's horizontal padding (0.625rem × 2 = 20 px at a 16 px root) plus 2 px for the 1 px .ui-overlay-surface border. #283 moved the padding to rem and the desktop default root font size is now 17.6 px (110 %), so the padding is 22 px and the border adds 2 more: the content box ends up 2 px narrower than the text, and .tip-label (overflow: hidden; text-overflow: ellipsis; white-space: nowrap) cuts the last glyphs ("Info" → "In…"). Any larger UI size makes it worse. The height 27 px and the arrow offset have the same stale-constant problem.

Fix (shared component only): measure a hidden copy of the real surface (same CSS: real padding, border, font and cap) instead of a hand-written constant; the label wraps at the cap (max-width: min(17.5rem, 100vw − 2 × margin)) instead of ellipsizing; flip to the other side when one side does not fit; clamp the surface and its arrow inside the viewport; re-measure an open tooltip when a font finishes loading.

Finding (root cause, from reading the code; reproduction on a production build follows): `packages/ui/src/tooltip.ts` `place()` sized the surface as `ceil(measured text width) + 22` px, a constant that assumed the surface's horizontal padding (0.625rem × 2 = 20 px at a 16 px root) plus 2 px for the 1 px `.ui-overlay-surface` border. #283 moved the padding to rem and the desktop default root font size is now 17.6 px (110 %), so the padding is 22 px and the border adds 2 more: the content box ends up 2 px narrower than the text, and `.tip-label` (`overflow: hidden; text-overflow: ellipsis; white-space: nowrap`) cuts the last glyphs ("Info" → "In…"). Any larger UI size makes it worse. The height `27` px and the arrow offset have the same stale-constant problem. Fix (shared component only): measure a hidden copy of the real surface (same CSS: real padding, border, font and cap) instead of a hand-written constant; the label wraps at the cap (`max-width: min(17.5rem, 100vw − 2 × margin)`) instead of ellipsizing; flip to the other side when one side does not fit; clamp the surface and its arrow inside the viewport; re-measure an open tooltip when a font finishes loading.
Author
Owner

Progress (branch j107-tooltip-truncation, WIP pushed):

  • Reproduced on a production build (dev-02, tracer build, mock canvas, default 110 % UI size) with the new spec apps/tracer/test/tooltip-fit.spec.ts against the unfixed origin/dev tooltip: it fails on "Split right", "Split down", "Sessions", "Close", "Connected", "Show hosts", "Hide hosts", "New window" and the long status/scene tooltips, at 1440x900 and at the 150 % UI size ("label scrolls (clipped)": the label's scrollWidth exceeds its clientWidth, a glyph box sits outside the surface). That is the owner's "happens on the split window icons as well".
  • Same spec against the fixed shared tooltip: desktop, phone (393x852) and 150 % size all pass; the long-label case (wrap, no ellipsis, shifts to stay on screen) is being tightened.
  • Note: the sidebar row action group in the owner's screenshot (info, pencil, trash) is not in origin/dev (it lives on row-actions-242-215, not merged); the bug is in the shared tooltip, so those buttons are fixed by the same change. The spec hovers every tooltip trigger that exists in the build.
  • Logs: dev-02 ~/worktrees/J107-*.log, evidence ~/worktrees/J107-evidence-*.
Progress (branch `j107-tooltip-truncation`, WIP pushed): - Reproduced on a production build (dev-02, tracer build, mock canvas, default 110 % UI size) with the new spec `apps/tracer/test/tooltip-fit.spec.ts` against the **unfixed** `origin/dev` tooltip: it fails on "Split right", "Split down", "Sessions", "Close", "Connected", "Show hosts", "Hide hosts", "New window" and the long status/scene tooltips, at 1440x900 and at the 150 % UI size ("label scrolls (clipped)": the label's `scrollWidth` exceeds its `clientWidth`, a glyph box sits outside the surface). That is the owner's "happens on the split window icons as well". - Same spec against the fixed shared tooltip: desktop, phone (393x852) and 150 % size all pass; the long-label case (wrap, no ellipsis, shifts to stay on screen) is being tightened. - Note: the sidebar row action group in the owner's screenshot (info, pencil, trash) is not in `origin/dev` (it lives on `row-actions-242-215`, not merged); the bug is in the shared tooltip, so those buttons are fixed by the same change. The spec hovers every tooltip trigger that exists in the build. - Logs: dev-02 `~/worktrees/J107-*.log`, evidence `~/worktrees/J107-evidence-*`.
Author
Owner

Progress 2 (branch head 096d80998, pushed; gate still running):

  • The fix is in the shared tooltip only: packages/ui/src/tooltip.ts + packages/ui/styles.css (hidden copy of the real surface is measured, label wraps at min(17.5rem, viewport − 16 px), flip + clamp inside the viewport, arrow clamped to the surface, re-measure on font load).
  • Existing specs encoded the old behaviour and are updated in their own commit: terminal-links.spec.ts asserted nowrap + ellipsis on a plain tooltip (that is the reported bug) and compared the arrow with a pixel constant. A first version of my fix also added a second .tip element (the measuring copy), which broke connection-pill and terminal-links locators: caught by running the neighbouring specs, fixed by giving the copy its own class, and tooltip-fit.spec.ts now asserts there is exactly one .tip in the page.
  • Before/after captures are from a production build at DPR 2 (dev-02). Before: "Split rig…". After: "Split right". Captures attached next.
Progress 2 (branch head `096d80998`, pushed; gate still running): - The fix is in the shared tooltip only: `packages/ui/src/tooltip.ts` + `packages/ui/styles.css` (hidden copy of the real surface is measured, label wraps at `min(17.5rem, viewport − 16 px)`, flip + clamp inside the viewport, arrow clamped to the surface, re-measure on font load). - Existing specs encoded the old behaviour and are updated in their own commit: `terminal-links.spec.ts` asserted `nowrap` + ellipsis on a plain tooltip (that is the reported bug) and compared the arrow with a pixel constant. A first version of my fix also added a second `.tip` element (the measuring copy), which broke `connection-pill` and `terminal-links` locators: caught by running the neighbouring specs, fixed by giving the copy its own class, and `tooltip-fit.spec.ts` now asserts there is exactly one `.tip` in the page. - Before/after captures are from a production build at DPR 2 (dev-02). Before: "Split rig…". After: "Split right". Captures attached next.
Author
Owner

Finish. Branch j107-tooltip-truncation, head 0cbf746af024d0696871c1b2ce165c8f902d3d6b (base origin/dev 294042b56d). Three commits: the fix (packages/ui/src/tooltip.ts, packages/ui/styles.css), the new spec + TASTE row, and the terminal-links assertions that encoded the old behaviour.

scripts/verify fast on infraternal-dev-02-std at that head, verbatim:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| test                      | PASS         |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
cargo test results: passed=2456 failed=0
src/mesh-vectors.test.ts:
(pass) every shared Mesh action vector passes through the typed client unchanged [2.43ms]
(pass) shared Mesh event vectors drive the typed watch adapter [5.49ms]

 39 pass
 0 fail
 59970 expect() calls
Ran 39 tests across 10 files. [2.64s]
VERIFY_EXIT=0

(No Rust changed, so the static-size delta is zero. An earlier gate run on the first version of the fix had 3 agent_daemon failures with terminal unavailable / resource_unavailable while the VM was loaded by other jobs; the same 11 tests pass when run alone, and the final gate above passes them all.)

Browser evidence (production build, dev-02):

  • New tooltip-fit.spec.ts: fails on unmodified origin/dev's tooltip (label scrollWidth > clientWidth, glyphs outside the surface: Split right, Split down, Sessions, Close, Connected, Show hosts, Hide hosts, New window, Views, status and scene tooltips; 1440x900 and the 150 % UI size), passes with the fix: 14 tooltips at 1440x900, 9 at 393x852, 14 at the largest UI size, plus the over-long-label case (wraps, no ellipsis, shifts to stay on screen).
  • Neighbouring specs on the final head: terminal-links, connection-pill, clusters-stray-tooltip, window-option-new, tooltip-fit all pass; bun run check (tsc) clean.
  • 11 other tracer specs fail (add-host-prototype x2, canvas x4, clusters x2, motion-audit, sessions-canvas x3). I ran the same tests on unmodified origin/dev (294042b56): the same 11 fail there too, so they are not from this change (log ~/worktrees/J107-devbase.log on dev-02; mine J107-reg5.log).

Not done / for the owner: the info/pencil/trash row-action group from the screenshot is not in origin/dev (it is on the unmerged row-actions-242-215), so the spec could not hover those exact buttons; they use the same shared tooltip, so the same fix applies. A 450-character unbroken label wraps to about 15 lines (wrap rather than ellipsize, as asked); there is no max height beyond the viewport clamp. Nothing was pushed to dev/main and nothing deployed.

Finish. Branch `j107-tooltip-truncation`, head `0cbf746af024d0696871c1b2ce165c8f902d3d6b` (base `origin/dev` 294042b56daac3d4ab2289508b7f8a6ddb0b24a4). Three commits: the fix (`packages/ui/src/tooltip.ts`, `packages/ui/styles.css`), the new spec + TASTE row, and the terminal-links assertions that encoded the old behaviour. **`scripts/verify fast`** on infraternal-dev-02-std at that head, verbatim: ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | test | PASS | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | static builds | PASS | +---------------------------+--------------+ cargo test results: passed=2456 failed=0 src/mesh-vectors.test.ts: (pass) every shared Mesh action vector passes through the typed client unchanged [2.43ms] (pass) shared Mesh event vectors drive the typed watch adapter [5.49ms] 39 pass 0 fail 59970 expect() calls Ran 39 tests across 10 files. [2.64s] VERIFY_EXIT=0 ``` (No Rust changed, so the static-size delta is zero. An earlier gate run on the first version of the fix had 3 `agent_daemon` failures with `terminal unavailable / resource_unavailable` while the VM was loaded by other jobs; the same 11 tests pass when run alone, and the final gate above passes them all.) **Browser evidence** (production build, dev-02): - New `tooltip-fit.spec.ts`: **fails on unmodified origin/dev's tooltip** (label `scrollWidth > clientWidth`, glyphs outside the surface: Split right, Split down, Sessions, Close, Connected, Show hosts, Hide hosts, New window, Views, status and scene tooltips; 1440x900 and the 150 % UI size), **passes with the fix**: 14 tooltips at 1440x900, 9 at 393x852, 14 at the largest UI size, plus the over-long-label case (wraps, no ellipsis, shifts to stay on screen). - Neighbouring specs on the final head: `terminal-links`, `connection-pill`, `clusters-stray-tooltip`, `window-option-new`, `tooltip-fit` all pass; `bun run check` (tsc) clean. - 11 other tracer specs fail (add-host-prototype x2, canvas x4, clusters x2, motion-audit, sessions-canvas x3). I ran the same tests on **unmodified origin/dev (294042b56)**: the same 11 fail there too, so they are not from this change (log `~/worktrees/J107-devbase.log` on dev-02; mine `J107-reg5.log`). **Not done / for the owner:** the info/pencil/trash row-action group from the screenshot is not in `origin/dev` (it is on the unmerged `row-actions-242-215`), so the spec could not hover those exact buttons; they use the same shared tooltip, so the same fix applies. A 450-character unbroken label wraps to about 15 lines (wrap rather than ellipsize, as asked); there is no max height beyond the viewport clamp. Nothing was pushed to dev/main and nothing deployed.
Author
Owner

Correction to the finish comment: window-option-new.spec.ts was last run on head 096d80998 (it passed there, with canvas/clusters etc. in the broad run); the last change after it (h kept exact instead of rounded up, for the arrow) was verified with terminal-links, connection-pill, clusters-stray-tooltip and tooltip-fit on 0cbf746af, not with window-option-new.

Correction to the finish comment: `window-option-new.spec.ts` was last run on head `096d80998` (it passed there, with `canvas`/`clusters` etc. in the broad run); the last change after it (`h` kept exact instead of rounded up, for the arrow) was verified with `terminal-links`, `connection-pill`, `clusters-stray-tooltip` and `tooltip-fit` on `0cbf746af`, not with `window-option-new`.
Author
Owner

J210 owner-authorized housekeeping: closing this completed scope because its work is merged on dev in ebf0ad13c. Verified the merge commit is an ancestor of current origin/dev. The implementation reports and evidence remain in this issue.

J210 owner-authorized housekeeping: closing this completed scope because its work is merged on dev in [ebf0ad13c](https://git.kayg.org/kayg/infraternal/commit/ebf0ad13c4fa58fd089c32e86298b518cdd7a91f). Verified the merge commit is an ancestor of current origin/dev. The implementation reports and evidence remain in this issue.
kayg closed this issue 2026-11-04 17:04:47 +00:00
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#310
No description provided.