From b330f1d9e808dd886aef5334a8ef067f1a068990 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 24 Aug 2026 04:45:02 +0200 Subject: [PATCH 1/3] feat(tabs): give the vertical rail the home screen's per-session detail The vertical tab rail (tabOrientation 'vertical') listed names and nothing else, while the rich sidebar and both home screens already answered the question a docked column exists to answer: which of these sessions wants me next, and how long has it been like that. The rail is a docked column too, so it now draws the same row. - New per-device setting tabRailDetail ('rich' | 'simple', default rich), App Settings -> Appearance -> Tabs, in SettingsUpdateSchema + displayKeys and stamped as data-tab-rail-detail by the pre-paint script, so a detailed rail does not flash through simple rows on every load. - ONE gate for both vertical surfaces: isRichTabRows() = isSessionSidebarRich() || isTabRailRich(). The row model, the markup and the 20s in-place clock are the existing rich-sidebar ones, classified by _mobileOverviewState/_mobileOverviewSince, so the rail, the sidebar, the desktop home rail and the phone overview cannot disagree about what "working" means or which stamp measures it. - Detail rides on its OWN attribute, exactly as the sidebar's does, so every existing [data-tab-orientation='vertical'] rule keeps matching both variants untouched. A flip of detail ALONE still forces a full render (the stamps line is emitted by the row template, not toggled by CSS) and re-runs applyTabWrapSettings(), which owns the folder line and is now rail-aware. - CSS: every rich paint rule gains a rail twin as a COMMA-GROUPED selector, never :is() - an :is() list takes its most specific argument, which would lift the sidebar arm from (0,3,1) to the rail's (0,5,1) and let these rules outrank things they never used to. - Width is why there are thresholds. At 256px the stamps line ellipsizes mid-word, the same reason the rich sidebar is 300px, so a rail that has never been sized defaults to 320 (RICH_DEFAULT_WIDTH, the existing Wide preset, which also keeps the settings select on a named choice). A width the user has chosen is never overridden: below 288px the created stamp is dropped rather than truncated (tab-rail-tight, CSS only) and below 240px the rows go back to simple (tab-rail-compact, which re-renders). - The rich clock is armed and disarmed by applyTabOrientation() as well as applySessionListLayout(); a leaked interval would rewrite stamps in a list that no longer has any. Also fixes a data-loss bug in the inline tab rename that predates the rail and reproduces in every layout, header strip included: Escape set the input to '' and blurred it, and the blur handler commits - so cancelling a rename PUT an empty name, and the tab fell back to its folder label (measured against a live server: ["rail-alpha","","rail-gamma"]). Escape now calls cancelRename(), which invalidates the edit so the blur that follows the input's removal is a no-op. Tests: rail-detail gate, the three ways it turns back off (simple, compact, horizontal), sidebar-wins, render-on-detail-flip and the plumbing/CSS guards in test/session-list-layout.test.ts; the rename cancel in test/inline-rename.test.ts (browser suite), pinned by running it against the old code first. Verified live against a real server on an isolated instance: detailed/simple/compact/header/ sidebar variants, click-select, the ... menu, inline rename, Alt+N, the in-place stamp tick and a full settings-picker round-trip including reload. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/vertical-rail-detailed-rows.md | 20 ++++ CLAUDE.md | 2 +- src/web/public/app.js | 58 ++++++++--- src/web/public/constants.js | 4 + src/web/public/index.html | 12 ++- src/web/public/session-ui.js | 9 +- src/web/public/settings-ui.js | 53 +++++++++- src/web/public/styles.css | 75 +++++++++++---- src/web/public/tab-rail-resize.js | 27 +++++- src/web/schemas.ts | 1 + test/inline-rename.test.ts | 39 ++++++++ test/session-list-layout.test.ts | 112 +++++++++++++++++++++- 12 files changed, 372 insertions(+), 40 deletions(-) create mode 100644 .changeset/vertical-rail-detailed-rows.md diff --git a/.changeset/vertical-rail-detailed-rows.md b/.changeset/vertical-rail-detailed-rows.md new file mode 100644 index 000000000..950b2f7ad --- /dev/null +++ b/.changeset/vertical-rail-detailed-rows.md @@ -0,0 +1,20 @@ +--- +"aicodeman": patch +--- + +Vertical tab rail: detailed rows, and a rename cancel that no longer wipes the name. + +The vertical rail (Tab Orientation → Vertical) now draws the same per-session +line the home screen and the rich sidebar draw — when the session was created, +how long it has been in the state it is in, the folder it runs in, and a status +pill — instead of just the name. New per-device setting **Vertical Rail Rows** +(`tabRailDetail`, App Settings → Appearance → Tabs) with `Detailed` as the +default and `Simple (name only)` as the opt-out. A rail that has never been +sized now opens at 320px (the existing Wide preset) so the line fits; a narrower +rail sheds the created stamp below 288px and falls back to simple rows below +240px. + +Also fixes a data-loss bug in the inline tab rename that predates the rail: +pressing Escape cleared the input and blurred it, and the blur handler commits — +so cancelling a rename stored an EMPTY session name and the tab fell back to its +folder label. Escape now cancels without a request, in every layout. diff --git a/CLAUDE.md b/CLAUDE.md index 35e0addc9..4b4842a39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -270,7 +270,7 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L **Mobile tab strip scrolling** (issue #257): under 768px the tab strip is a horizontal scroller (desktop wraps to a second row instead), so the active tab can sit off-screen. Three rules keep it reachable and they only work together: `_updateActiveTabImmediate()` scrolls the selected tab into view via `computeTabScrollLeft()` (pure, in constants.js) using **rect math on the strip's own `scrollLeft`**, never `scrollIntoView()`, which would also scroll the document under a fixed header; `_fullRenderSessionTabs()` **restores `scrollLeft`** across the `innerHTML` rebuild, since ambient rebuilds (a task badge appearing, a session created elsewhere) otherwise snap a mid-swipe strip back to 0; and it re-reveals the active tab **only when it changed** (`_lastRenderedActiveTabId`), so browsing the far end of the strip is not undone by background renders. ⚠️ **The ACTIVE tab is the only one with action icons, and on a phone they can eat it**: `.session-tab.active .tab-name` reserves `min-width: 44px` in the ≤430px block, because a short session name rendered a 13px label against a 50px gear+close cluster, putting the tab's geometric CENTRE on the gear, so a thumb aiming at the tab opened Session Options instead of switching (measured at 360/393/430px; only long names cleared it). ⚠️ **The floor is set by the 10th tab onward, not by the tabs you can see**: `.tab-number` renders only for `_tabIdx < 9`, so tab 10 loses 16px + a gap off its left and its centre sits 10px further right. The centre clears the icons when `reserved > icons + rightEdge - leftRunUp - gap` (= 50 + 9 - 17 - 4 = **38px**), hit-testing snaps to whole pixels so 39px still lands on the gear, and the practical floor is 40px — a NUMBERED tab clears it at 20px, which is exactly why reasoning from the tabs on screen would put the centre back on the gear. `test/mobile-tab-tap-zones.test.ts` recomputes that inequality from the stylesheet, so widening the gear or the padding fails there rather than on a phone. The guarantee is centre-off-the-ICONS, not centre-inside-the-label (on a numberless tab it lands in the gap between them, which still switches). Non-active tabs keep their icons hidden and stay tappable end to end. ⚠️ Mobile no longer hoists the active session to the front of the strip: that reordering ran on full renders only, so tab order flipped depending on which render path fired, and it renumbered the Alt+N badges. Scroll-into-view replaces it; do not reintroduce it. -**Session list layout: header strip or left sidebar** (`sessionListLayout`, App Settings → Appearance → Tabs, default `header`; per-device policy — it IS in `SettingsUpdateSchema` and persists server-side, but `displayKeys` makes a device keep its own value): with many sessions the horizontal strip stops being scannable, so the list can move into a vertical `