|
| 1 | +# Worktree Tooltip Width Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans |
| 4 | +> to implement this plan directly in the current agent, task-by-task. Never use |
| 5 | +> subagent-driven development. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 6 | +
|
| 7 | +**Goal:** Let the session-vitals worktree tooltip use up to half of the viewport |
| 8 | +so ordinary paths stay on one line while oversized paths wrap. |
| 9 | + |
| 10 | +**Approved spec/design:** |
| 11 | +`docs/superpowers/specs/2026-07-30-worktree-tooltip-width-design.md` |
| 12 | + |
| 13 | +**Architecture:** Keep kit-ui's generic tooltip sizing unchanged. Pass a |
| 14 | +dedicated class to the existing worktree `Tooltip`, then use the supported class |
| 15 | +hook to override only that popover's maximum width while retaining kit-ui's |
| 16 | +fixed viewport positioning and end-aligning its arrow with the trigger. |
| 17 | + |
| 18 | +**Tech Stack:** Svelte 5, TypeScript, `@kenn-io/kit-ui`, Playwright, Vitest |
| 19 | + |
| 20 | +## Global Constraints |
| 21 | + |
| 22 | +- The generic kit-ui tooltip must retain its 280-pixel default maximum width. |
| 23 | +- The worktree tooltip maximum width is `min(50vw, calc(100vw - 32px))`. |
| 24 | +- Paths that fit within the cap stay on one line; longer paths wrap. |
| 25 | +- The tooltip may extend left of the analysis sidebar but must remain inside the |
| 26 | + viewport. |
| 27 | +- Unbroken path segments must wrap rather than overflow, and the tooltip arrow |
| 28 | + must remain over the worktree trigger. |
| 29 | +- Preserve the existing LTR-isolated path text, start truncation, hover and |
| 30 | + keyboard behavior, copy control, and empty state. |
| 31 | + |
| 32 | +______________________________________________________________________ |
| 33 | + |
| 34 | +### Task 1: Make the worktree tooltip viewport-relative |
| 35 | + |
| 36 | +**Files:** |
| 37 | + |
| 38 | +- Modify: `cmd/testfixture/main.go` |
| 39 | +- Modify: `frontend/e2e/session-timing.spec.ts` |
| 40 | +- Modify: `frontend/src/lib/components/content/SessionVitals.svelte` |
| 41 | + |
| 42 | +**Interfaces:** |
| 43 | + |
| 44 | +- Consumes: kit-ui `Tooltip`'s supported `class?: string` and `align` props plus |
| 45 | + existing fixed-position viewport clamping. |
| 46 | + |
| 47 | +- Produces: a `worktree-path-tooltip` popover class scoped to the session-vitals |
| 48 | + worktree tooltip. |
| 49 | + |
| 50 | +- [ ] **Step 1: Make the fixture deterministic and write the failing browser |
| 51 | + test** |
| 52 | + |
| 53 | +In `cmd/testfixture/main.go`, lengthen the showcase session's existing synthetic |
| 54 | +worktree path to exactly this value: |
| 55 | + |
| 56 | +```go |
| 57 | +Cwd: "/workspace/مشروع/.worktrees/שלוםfeaturewithalongcheckoutnamefortooltipwrappingwithoutbreakopportunities", |
| 58 | +``` |
| 59 | + |
| 60 | +Update `SHOWCASE_WORKTREE` in `frontend/e2e/session-timing.spec.ts` to the same |
| 61 | +literal: |
| 62 | + |
| 63 | +```ts |
| 64 | +const SHOWCASE_WORKTREE = |
| 65 | + "/workspace/مشروع/.worktrees/שלוםfeaturewithalongcheckoutnamefortooltipwrappingwithoutbreakopportunities"; |
| 66 | +``` |
| 67 | + |
| 68 | +Add this test after the existing mixed-direction worktree-path regression in |
| 69 | +`frontend/e2e/session-timing.spec.ts`: |
| 70 | + |
| 71 | +```ts |
| 72 | +test("uses available viewport width for the worktree tooltip", async ({ |
| 73 | + page, |
| 74 | +}) => { |
| 75 | + await gotoShowcase(page); |
| 76 | + |
| 77 | + const panel = page.locator("aside.vitals"); |
| 78 | + const path = page.locator(".context-value--path"); |
| 79 | + const trigger = page.locator(".context-tooltip .kit-tooltip-trigger"); |
| 80 | + await path.hover(); |
| 81 | + |
| 82 | + const tooltip = page.getByRole("tooltip"); |
| 83 | + await expect(tooltip).toHaveText(SHOWCASE_WORKTREE); |
| 84 | + const panelLeft = await panel.evaluate( |
| 85 | + (element) => element.getBoundingClientRect().left, |
| 86 | + ); |
| 87 | + const wideLayout = await tooltip.evaluate((element) => { |
| 88 | + const range = document.createRange(); |
| 89 | + range.selectNodeContents(element); |
| 90 | + const lineTops = Array.from( |
| 91 | + range.getClientRects(), |
| 92 | + (rect) => Math.round(rect.top), |
| 93 | + ); |
| 94 | + const rect = element.getBoundingClientRect(); |
| 95 | + const arrowStyle = getComputedStyle(element, "::before"); |
| 96 | + const arrowWidth = Number.parseFloat(arrowStyle.width); |
| 97 | + const arrowCenter = |
| 98 | + arrowStyle.left === "auto" |
| 99 | + ? rect.right - Number.parseFloat(arrowStyle.right) - arrowWidth / 2 |
| 100 | + : rect.left + Number.parseFloat(arrowStyle.left) + arrowWidth / 2; |
| 101 | + return { |
| 102 | + arrowCenter, |
| 103 | + left: rect.left, |
| 104 | + lineCount: new Set(lineTops).size, |
| 105 | + right: rect.right, |
| 106 | + viewportWidth: window.innerWidth, |
| 107 | + width: rect.width, |
| 108 | + }; |
| 109 | + }); |
| 110 | + const triggerBounds = await trigger.boundingBox(); |
| 111 | + expect(triggerBounds).not.toBeNull(); |
| 112 | + |
| 113 | + expect(wideLayout.width).toBeLessThanOrEqual( |
| 114 | + wideLayout.viewportWidth * 0.5 + 1, |
| 115 | + ); |
| 116 | + expect(wideLayout.lineCount).toBe(1); |
| 117 | + expect(wideLayout.left).toBeGreaterThanOrEqual(0); |
| 118 | + expect(wideLayout.right).toBeLessThanOrEqual(wideLayout.viewportWidth); |
| 119 | + expect(wideLayout.left).toBeLessThan(panelLeft); |
| 120 | + expect(wideLayout.arrowCenter).toBeGreaterThanOrEqual(triggerBounds!.x); |
| 121 | + expect(wideLayout.arrowCenter).toBeLessThanOrEqual( |
| 122 | + triggerBounds!.x + triggerBounds!.width, |
| 123 | + ); |
| 124 | + |
| 125 | + await page.setViewportSize({ width: 1000, height: 900 }); |
| 126 | + await expect(path).toBeVisible(); |
| 127 | + await path.hover(); |
| 128 | + const narrowLayout = await tooltip.evaluate((element) => { |
| 129 | + const range = document.createRange(); |
| 130 | + range.selectNodeContents(element); |
| 131 | + const lineTops = Array.from( |
| 132 | + range.getClientRects(), |
| 133 | + (rect) => Math.round(rect.top), |
| 134 | + ); |
| 135 | + const rect = element.getBoundingClientRect(); |
| 136 | + return { |
| 137 | + clientWidth: element.clientWidth, |
| 138 | + left: rect.left, |
| 139 | + lineCount: new Set(lineTops).size, |
| 140 | + right: rect.right, |
| 141 | + scrollWidth: element.scrollWidth, |
| 142 | + viewportWidth: window.innerWidth, |
| 143 | + width: rect.width, |
| 144 | + }; |
| 145 | + }); |
| 146 | + |
| 147 | + expect(narrowLayout.width).toBeLessThanOrEqual( |
| 148 | + narrowLayout.viewportWidth * 0.5 + 1, |
| 149 | + ); |
| 150 | + expect(narrowLayout.lineCount).toBeGreaterThan(1); |
| 151 | + expect(narrowLayout.scrollWidth).toBeLessThanOrEqual( |
| 152 | + narrowLayout.clientWidth + 1, |
| 153 | + ); |
| 154 | + expect(narrowLayout.left).toBeGreaterThanOrEqual(0); |
| 155 | + expect(narrowLayout.right).toBeLessThanOrEqual(narrowLayout.viewportWidth); |
| 156 | +}); |
| 157 | +``` |
| 158 | + |
| 159 | +- [ ] **Step 2: Run the browser test to verify it fails** |
| 160 | + |
| 161 | +Run: |
| 162 | + |
| 163 | +```bash |
| 164 | +cd frontend |
| 165 | +npm run e2e -- session-timing.spec.ts --project=chromium \ |
| 166 | + --grep "uses available viewport width for the worktree tooltip" |
| 167 | +``` |
| 168 | + |
| 169 | +Expected: FAIL because the current 280-pixel maximum wraps the fixture path at |
| 170 | +the default 1600-pixel viewport, so `wideLayout.lineCount` is greater than 1. |
| 171 | +After adding only the width override, the arrow assertion also fails until the |
| 172 | +tooltip is end-aligned. |
| 173 | + |
| 174 | +- [ ] **Step 3: Add the per-use width override** |
| 175 | + |
| 176 | +In `frontend/src/lib/components/content/SessionVitals.svelte`, pass the popover |
| 177 | +class through the existing tooltip: |
| 178 | + |
| 179 | +```svelte |
| 180 | +<Tooltip |
| 181 | + text={session.cwd} |
| 182 | + focusable |
| 183 | + align="end" |
| 184 | + class="worktree-path-tooltip" |
| 185 | +> |
| 186 | +``` |
| 187 | + |
| 188 | +Add the scoped global rule next to the existing `.context-tooltip` integration |
| 189 | +styles: |
| 190 | + |
| 191 | +```css |
| 192 | +.context-tooltip :global(.worktree-path-tooltip) { |
| 193 | + max-width: min(50vw, calc(100vw - 32px)); |
| 194 | + overflow-wrap: anywhere; |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +Do not modify kit-ui source or the shared `.kit-tooltip` rule. |
| 199 | + |
| 200 | +- [ ] **Step 4: Run the focused browser regression in both engines** |
| 201 | + |
| 202 | +Run: |
| 203 | + |
| 204 | +```bash |
| 205 | +cd frontend |
| 206 | +npm run e2e -- session-timing.spec.ts --project=chromium \ |
| 207 | + --grep "uses available viewport width for the worktree tooltip" |
| 208 | +npm run e2e -- session-timing.spec.ts --project=webkit \ |
| 209 | + --grep "uses available viewport width for the worktree tooltip" |
| 210 | +``` |
| 211 | + |
| 212 | +Expected: PASS in Chromium and WebKit. The default viewport renders one line and |
| 213 | +positions the tooltip left of the sidebar; the 1000-pixel viewport wraps the |
| 214 | +same complete path without exceeding 500 pixels or overflowing its unbroken |
| 215 | +segment. Both layouts remain viewport-contained, with the arrow over the |
| 216 | +trigger. |
| 217 | + |
| 218 | +- [ ] **Step 5: Run the affected component and integration checks** |
| 219 | + |
| 220 | +Run: |
| 221 | + |
| 222 | +```bash |
| 223 | +cd frontend |
| 224 | +npm test -- --run src/lib/components/content/SessionVitals.test.ts |
| 225 | +npm run e2e -- session-timing.spec.ts --project=chromium |
| 226 | +npm run check |
| 227 | +npm run check:kit-ui |
| 228 | +cd .. |
| 229 | +go fmt ./... |
| 230 | +go vet ./... |
| 231 | +``` |
| 232 | + |
| 233 | +Expected: 12 component tests and all 7 Chromium session-vitals tests pass, |
| 234 | +Svelte reports 0 errors, kit-ui reports 0 findings, and Go formatting and vet |
| 235 | +complete successfully. Existing unrelated Svelte unused-selector warnings may |
| 236 | +remain. |
| 237 | + |
| 238 | +- [ ] **Step 6: Commit the implementation** |
| 239 | + |
| 240 | +```bash |
| 241 | +git add cmd/testfixture/main.go \ |
| 242 | + frontend/e2e/session-timing.spec.ts \ |
| 243 | + frontend/src/lib/components/content/SessionVitals.svelte |
| 244 | +git commit -m "fix: widen worktree path tooltip" \ |
| 245 | + -m "The shared 280-pixel default suits prose but forces filesystem paths to wrap inside the analysis sidebar. Give only the worktree tooltip a viewport-relative limit so ordinary paths stay on one line while oversized paths wrap at half the viewport." |
| 246 | +``` |
0 commit comments