Skip to content

Vertical tab rail: detailed rows (created / working / status), plus a rename-cancel fix - #338

Merged
Ark0N merged 3 commits into
masterfrom
feat/vertical-rail-detailed-rows
Aug 25, 2026
Merged

Vertical tab rail: detailed rows (created / working / status), plus a rename-cancel fix#338
Ark0N merged 3 commits into
masterfrom
feat/vertical-rail-detailed-rows

Conversation

@Ark0N

Copy link
Copy Markdown
Owner

What this is

Codeman can put the session list in a vertical rail beside the terminal (App Settings → Appearance → Tabs → Tab Orientation: Vertical). Until now that rail showed a name per row and nothing else, even though the same information the home screen shows — when a session was created, how long it has been in the state it is in, and what that state is — already existed one surface over, on the rich left sidebar and on both home screens.

A docked column is not a tab strip: it has a row per session and width to spare, so a name is not the whole story it can tell. This PR gives the rail that row.

Before — name only:

1 rail-alpha
2 rail-beta
3 rail-gamma

After — the home screen's line, in the rail:

1 SH rail-alpha ⚙ × ⋯
📁 claudeman
CREATED 8m ago · WORKING 1m [working]

The setting

New per-device Vertical Rail Rows (tabRailDetail), directly under Tab Orientation:

ValueMeaning
Detailed (default)created stamp, state duration, folder, status pill
Simple (name only)exactly what the rail shipped with in 1.22.0

It only affects the vertical rail. The header tab strip is untouched, and the sidebar layouts keep their own sessionListLayout choice.

How it works

  • One gate, one row model.isRichTabRows() = isSessionSidebarRich() || isTabRailRich(). The row model, markup and 20s in-place clock are the rich sidebar's existing ones, and the state classification comes from _mobileOverviewState() / _mobileOverviewSince(). The rail, the sidebar, the desktop home rail and the phone overview therefore cannot disagree about what "working" means, or about which timestamp measures it (a working pane repaints about once a second, so its duration is anchored on the turn's last Enter, not on last-activity).
  • Detail rides on its own attribute (data-tab-rail-detail), the way the sidebar's does, so every existing html[data-tab-orientation='vertical'] rule keeps matching both variants with no edit. The pre-paint script stamps it, so a detailed rail does not flash through simple rows on load.
  • A detail flip alone forces a render. The stamps line is emitted by the row template, not toggled by CSS, so flipping the setting has to rebuild the rows — and re-run applyTabWrapSettings(), the one owner of the folder line, which is now rail-aware.
  • CSS twins are comma-grouped, never :is(). An :is() list takes its most specific argument's specificity, which would have lifted the sidebar arm from (0,3,1) to the rail's (0,5,1) and let those paint rules outrank things they never used to.

Width: three thresholds, all measured

At 256px the stamps line ellipsizes mid-word (CREA… · WORK…), which is the same reason the rich sidebar is 300px rather than 260px:

  • A rail that has never been sized now opens at 320px — the existing Wide preset, so the settings select shows a named choice rather than "Custom". A width the user has actually chosen is never overridden.
  • Below 288px the created stamp is dropped rather than truncated (tab-rail-tight, CSS only). What survives is the state duration, which is the number the list is ordered by, and its pill.
  • Below 240px the rail drops back to simple rows entirely (tab-rail-compact, the existing threshold, which already re-renders).

Bug fixed along the way: Escape wiped session names

Not rail-specific, and it predates the rail — it reproduces in the plain header strip too. The inline tab rename's Escape handler cleared the input and blurred it, and the blur handler commits:

}elseif(e.key==='Escape'){input.value='';input.blur();// → finishRename({ commit: true }) with an empty value}

So cancelling a rename renamed the session to an empty string. Measured against a live server before the fix: ["rail-alpha", "", "rail-gamma"], with the tab falling back to its folder name. Escape now calls cancelRename(), which invalidates the edit so the blur that follows the input's removal is a no-op.

Testing

  • npm test green (6101 passed) — includes new coverage in test/session-list-layout.test.ts for the rail-detail gate, the three ways it turns back off (simple / compact / horizontal), sidebar-wins, render-on-detail-flip, and the settings/schema/pre-paint/CSS plumbing.
  • npm run test:browser -- test/inline-rename.test.ts green, including the new Escape regression — pinned by running it against the old code first, where it fails with expected [ '/api/sessions/esc-cancel/name' ] to deeply equal [].
  • typecheck, lint, format:check, check:public-assets, check:frontend-syntax all clean.
  • Verified live against a real server on an isolated instance (own data dir and tmux socket): detailed / simple / compact / header / sidebar variants, click-to-select, the action menu, inline rename, Alt+N, the in-place stamp tick (same DOM node, new text), and a full settings-picker round-trip including a reload to confirm the pre-paint stamp.

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) <noreply@anthropic.com>
…ot skip the first render
Two review nits on the vertical rail's detailed rows.
1. The tab-rail-tight rule (below 288px) hides `.tab-meta-created`, and its
comment claimed the value "survives in the row's title attribute either way".
It did not: the only title carrying it lived ON that element, and a
`display: none` element has no hover target, so the created stamp was not
shrunk but gone with no way to ask for it. Rather than just correcting the
comment, `_sidebarRichMetaHTML()` now puts BOTH absolute stamps on the
`.tab-meta` line itself, so the pill and the gaps around the stamps remain as
hover targets. An item's own title still wins where the item is visible.
2. applyTabOrientation() decided whether applyTabWrapSettings() had already
re-rendered by comparing `_tallTabsEnabled` before and after. That reads an
UNDEFINED previous value as "it rendered", but applyTabWrapSettings()
deliberately renders nothing on its first call ever (it only establishes the
baseline: `prevTallTabs !== undefined && prevTallTabs !== showFolder`). So on
a first call that also flips the folder row, neither function rendered and the
rows stayed stale. Reachable when the pre-paint script throws and leaves the
layout attributes on their catch-branch fallbacks for applyTabOrientation() to
correct. The guard now mirrors applyTabWrapSettings()'s own condition.
Both new tests were run against the unfixed code first and fail there, which is
the only thing that makes them regression tests. (The third, "does not render
twice", passes either way by design: it pins that fix 2 did not introduce a
double rebuild.)
Verified in a real browser against a live server with two sessions, driving the
narrowing through _setTabRailWidth() the way the resize drag does: at the 320
default the row reads "CREATED 2m ago · IDLE <1m" with the created element
displayed; at 256 the tight class is on, the created element computes to
display:none, the visible text drops to "IDLE <1m", and the meta line's title
still reads "First created: ...". At 220 the compact threshold drops rich rows
entirely. Screenshots confirm no truncation artifacts in either state.
Full gate green (6104 passed), typecheck, lint, format, frontend-syntax and
public-assets all clean.
… compact wrap pass, rich-aware resets
Three review findings on the detailed-rows feature, all in its edge cases:
- The App Settings width select consulted the handheld defaults blob
(tabRailWidth: 256) BEFORE the rich-aware default, which the renderer
never reads — so a tablet's unsized rich rail rendered 320 while the
dialog said 256, and a routine Save persisted the 256 (below the 288px
tight threshold, permanently). The chain now mirrors
applyTabRailWidth()'s actual resolution.
- _setTabRailWidth() re-rendered on a compact flip but never re-ran
applyTabWrapSettings(), the one owner of the folder line, whose railRich
input reads the compact class this function just toggled. A rich rail
dragged below 240px kept emitting folder rows — persistently, for a
stored width < 240, since the boot wrap pass runs before the class is
first applied. The wrap pass now re-runs on the flip, with exactly one
render either way.
- Both reset affordances (handle dblclick, Enter on the handle) reset to
the hardcoded 256 even on a rich rail, landing it below the tight
threshold; both now resolve the rich-aware default (320), via a new
optional defaultWidth input on resolveTabRailKeyboardWidth().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ark0N
Ark0N merged commit ca5fe1a into masterAug 25, 2026
2 checks passed
@Ark0N
Ark0N deleted the feat/vertical-rail-detailed-rows branch August 25, 2026 17:12
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@Ark0N@claude