Reopening the editor over a rename still in flight filled it from the name
the server had not replaced yet, so dismissing it (blur commits) queued the
old name behind the new one and undid the rename. The queue now records the
newest queued name per session (_inlineRenamePending, cleared with the queue
entry), and a reopened editor takes its prefix, input and "unchanged"
comparison from it. An untouched confirm sends nothing more.
A failed write only toasted while its editor was still current. The queue
reports the failure itself now, and the editor only puts its label back.
One rejected task blocked every later rename of that session until reload.
Each task now chains from a settled predecessor, the local apply after a
successful PUT is guarded, and the queue entry is cleaned up on either
outcome.
The rail and sidebar editor's 4rem floor moves from a stylesheet
`!important` into the inline min-width startInlineRename already writes per
layout (0 in the header strip, 4rem in the rail and sidebar).
Tests: the reopened-editor case now expects only "First" to be sent; new
cases cover a 500 answered after the editor is gone and a throw in
updateSubagentParentNames; the long-prefix check runs in the sidebar and
detailed sidebar too and asserts the inline floor; the header strip editor
keeps min-width 0.
Two problems with renaming a tab in the vertical rail, both easier to hit now
that the grouped rail has its own inline editor beside the session one.
Writes. A committed rename PUT its name and only applied the answer if the
same editor was still open when it came back. Reopening the editor before the
PUT answered (F2 or right-click again, or starting a group rename, which
cancels the session editor) threw the confirmed name away, so the tab kept
showing the old name until an SSE frame happened to repaint it. Two quick
renames also raced as two concurrent PUTs. Inline renames now go through a
per-session queue: one PUT at a time in the order they were made, the
confirmed name applied to app.sessions whatever happened to the editor, and
the "already that name" check made when the write runs rather than when Enter
is pressed, so confirming the name still on screen over a write in flight is
a real write.
Layout. The editor (a flex row) could not shrink below the input's intrinsic
width, so a long w<n>-<case> prefix pushed the label past its row: the prefix
slid out of view in the detailed rows and the input was clipped mid-word in
the compact rail. The label now has min-width 0, the prefix gives way first
(down to 2rem, with an ellipsis), the input keeps 4rem, and in the compact
rail the row's adornments step aside while the name is edited. The detailed
rows' three-line clamp also outranked the shared unclamp rule, which is what
the existing "unclamped editor" browser test caught; it is restated there.
Header strip, sidebar and flat-rail markup are unchanged.
Tests (test/inline-rename.test.ts, browser suite): the unclamp check runs for
simple and detailed rows; a write-ordering describe covers ordering, a
reopened editor cancelled over a confirmed write, a re-sent unchanged name and
a group rename taking over; a long-prefix describe drives real rows from a
live session in simple, detailed and compact rails.
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>
Renaming a tab appeared to do nothing: the new name only showed after a full
page reload. The PUT always succeeded; what was broken is how the tab strip
learns the result. `finishRename()` re-renders the strip from the client-side
`app.sessions` map, and nothing wrote the new name into that map, so the rename
depended on the `session:updated` SSE frame to carry its own write back. On a
page whose stream has gone quiet without erroring, that frame never lands and
the re-render repaints the stale label.
- `_applyLocalSessionName()` writes the confirmed name into `this.sessions` and
refreshes cached subagent parent names, mirroring `_onSessionUpdated`.
- `_putSessionName()` returns the stored name or null. `_apiPut` turns a network
error into a null Response and an API failure into a non-ok status, so a
rejected rename previously read as success and silently dropped the edit (the
old try/catch could never fire).
- Both surfaces use them: `startInlineRename()`'s `finishRename` and
`saveSessionName()`.
Two regression tests: the commit applies the name with no SSE frame dispatched,
and a 500 restores the old label, leaves the map untouched, and toasts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two independent ways a tab description could be typed in and silently lost.
1. Session Options modal (deterministic). The Session Name input saves on
blur, and every autosave handler in the modal bails on a null
editingSessionId. closeSessionOptions() cleared that id BEFORE hiding the
modal, and hiding it is what blurs the input, so the save always ran too
late and returned early. Escape and backdrop-click lost the name with no
PUT at all; only the X button worked, because mousedown blurs the input
before the click handler runs. Fix: blur the focused modal field first,
then clear the id. That also covers the auto-compact prompt, which saves
on change and had the same fate.
2. Right-click inline rename (racy). The _inlineRenameActive guard from #81
sits in renderSessionTabs() (the scheduler) and _fullRenderSessionTabs(),
but not in _renderSessionTabsImmediate() (the debounced executor). A
render queued in the ~100ms before the rename opened still fires and the
incremental branch rewrites .tab-name's innerHTML, destroying the input
mid-keystroke: it commits a truncated name, or, if it lands before the
first keystroke, closes the rename so everything typed after goes
nowhere. Fix: guard the executor too. finishRename() re-renders on both
commit and cancel, so a render dropped there is picked back up.
Verified end-to-end against a live server on an isolated instance: all three
modal close paths now persist the name, and the rename input survives a
render mid-typing. Both regression tests were checked to fail with their fix
reverted; the render one was vacuous at first because the synthetic tab sat
on <body> instead of inside #sessionTabs, so it now builds the tab in the
real container.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three follow-up fixes to the inline rename input introduced in #81:
1. IME composition guard. Pressing Enter to confirm a Chinese pinyin
candidate (or any IME composition) was committing the half-composed
text as the session name. Skip the keydown handler when isComposing
is true or when keyCode is the legacy 229 sentinel that older
Safari/Edge versions report on the Enter that triggers compositionend.
2. Ghost tab on mid-rename deletion. If a session was deleted via SSE
while its tab was being renamed, the render-skip flag suppressed
_renderSessionTabs() and the orphaned <input> stayed on screen until
blur — at which point the rename PUT 404'd against the dead session.
Replace the boolean _inlineRenameActive with a _activeRename
{sessionId, cancel} object so _cleanupSessionData can abort an
in-flight rename targeting the deleted session, and finishRename
skips the API call when the session is gone.
3. Stuck-flag risk. Move the settle-once guard into a closure-local
`settled` boolean so blur / Enter / Escape / external cancel all
converge to a single idempotent path. Register _activeRename only
after the input is fully wired so a throw earlier in setup can't
strand state.
Adds test/inline-rename.test.ts with 7 Playwright tests that drive
startInlineRename via page.evaluate() against a stubbed session and
synthetic .tab-name node — no real PTY/tmux needed, runs in ~1.3s.
Also fixes test/mobile/helpers/server.ts which imported the WebServer
via a path one directory short of the repo root, breaking the entire
mobile test suite under the main vitest config.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>