mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
+46
-12
@@ -3826,6 +3826,38 @@ class CodemanApp {
|
||||
return root.dataset.sessionList === 'sidebar' && root.dataset.sidebarDetail === 'rich';
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the VERTICAL TAB RAIL (tabOrientation 'vertical') is showing the
|
||||
* detailed rows: the same "created 3d ago · working 12m" line and status pill
|
||||
* the rich sidebar and both home screens carry.
|
||||
*
|
||||
* A docked column is not a tab strip — that was the argument for the rich
|
||||
* sidebar, and the rail is a docked column too, so it defaults to rich and
|
||||
* `tabRailDetail: 'simple'` is the opt-out.
|
||||
*
|
||||
* The compact carve-out is not cosmetic: below 240px the rail already drops
|
||||
* the row actions to a hover affordance, and three lines of stamps in a
|
||||
* ~208px column ellipsize into noise. `_setTabRailWidth()` re-renders the
|
||||
* tabs whenever that class flips, so this gate is re-read at the right moment.
|
||||
*/
|
||||
isTabRailRich() {
|
||||
const root = document.documentElement;
|
||||
return (
|
||||
root.getAttribute('data-tab-orientation') === 'vertical' &&
|
||||
root.dataset.tabRailDetail === 'rich' &&
|
||||
!root.classList.contains('tab-rail-compact')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The one gate the render paths ask: does THIS list draw detailed rows?
|
||||
* Either vertical surface can, and neither can be on at once (the sidebar
|
||||
* owns the tabs whenever it is active, which forces the rail off).
|
||||
*/
|
||||
isRichTabRows() {
|
||||
return this.isSessionSidebarRich() || this.isTabRailRich();
|
||||
}
|
||||
|
||||
/**
|
||||
* True where the sidebar is a MODAL off-canvas drawer over the terminal
|
||||
* instead of a docked column.
|
||||
@@ -3999,7 +4031,7 @@ class CodemanApp {
|
||||
this.showHomeSessions?.();
|
||||
}
|
||||
// Only the rich rows carry stamps that go stale with no event behind them.
|
||||
if (this.isSessionSidebarRich()) this._startSidebarRichClock();
|
||||
if (this.isRichTabRows()) this._startSidebarRichClock();
|
||||
else this._stopSidebarRichClock();
|
||||
}
|
||||
|
||||
@@ -4116,13 +4148,14 @@ class CodemanApp {
|
||||
}
|
||||
|
||||
/**
|
||||
* The per-row model for a rich sidebar row: which state the session is in,
|
||||
* when it was first created, and how long it has been in that state.
|
||||
* The per-row model for a rich row (detailed sidebar or vertical tab rail):
|
||||
* which state the session is in, when it was first created, and how long it
|
||||
* has been in that state.
|
||||
*
|
||||
* Classification is `_mobileOverviewState()` and the state duration is
|
||||
* `_mobileOverviewSince()` (both mobile-overview.js), NOT re-derived here —
|
||||
* the sidebar, the desktop home rail and the phone overview must never
|
||||
* disagree about what "working" means or about which stamp measures it.
|
||||
* the sidebar, the rail, the desktop home rail and the phone overview must
|
||||
* never disagree about what "working" means or about which stamp measures it.
|
||||
*
|
||||
* Guarded like every other cross-file consumer in this app: a stale cached
|
||||
* mobile-overview.js must degrade to a row with no meta line, not throw and
|
||||
@@ -4214,7 +4247,7 @@ class CodemanApp {
|
||||
_startSidebarRichClock() {
|
||||
if (this._sidebarRichClock) return;
|
||||
this._sidebarRichClock = setInterval(() => {
|
||||
if (!this.isSessionSidebarRich()) {
|
||||
if (!this.isRichTabRows()) {
|
||||
this._stopSidebarRichClock();
|
||||
return;
|
||||
}
|
||||
@@ -4431,7 +4464,7 @@ class CodemanApp {
|
||||
if (canIncremental) {
|
||||
// Read once for the whole pass, like the full-rebuild path: this touches
|
||||
// the DOM and the loop below runs for every session on every SSE tick.
|
||||
const richRows = this.isSessionSidebarRich();
|
||||
const richRows = this.isRichTabRows();
|
||||
// Incremental update - only modify changed properties
|
||||
for (const [id, session] of this.sessions) {
|
||||
const tab = container.querySelector(`.session-tab[data-id="${id}"]`);
|
||||
@@ -4740,9 +4773,9 @@ class CodemanApp {
|
||||
// into view replaces it.
|
||||
const parts = [];
|
||||
const tabOrder = this.sessionOrder;
|
||||
// Read once, not per session: isSessionSidebarRich() touches the DOM and
|
||||
// Read once, not per session: isRichTabRows() touches the DOM and
|
||||
// this loop runs for every tab on every full rebuild.
|
||||
const richRows = this.isSessionSidebarRich();
|
||||
const richRows = this.isRichTabRows();
|
||||
let _tabIdx = 0;
|
||||
for (const id of tabOrder) {
|
||||
const session = this.sessions.get(id);
|
||||
@@ -4786,9 +4819,10 @@ class CodemanApp {
|
||||
? (session.workingDir ? `${parsedName.prefix} (${session.workingDir})` : parsedName.prefix)
|
||||
: (session.workingDir || '');
|
||||
|
||||
// Rich sidebar rows only: the home screen's created/state stamps and a
|
||||
// status pill. richRow is null in every other layout, and both helpers
|
||||
// below collapse to '' — the header strip's markup is unchanged.
|
||||
// Rich rows only (the detailed sidebar OR the vertical tab rail): the home
|
||||
// screen's created/state stamps and a status pill. richRow is null in every
|
||||
// other layout, and both helpers below collapse to '' — the header strip's
|
||||
// markup is unchanged.
|
||||
const richRow = richRows ? this._sidebarRichRow(id, session) : null;
|
||||
const richMeta = this._sidebarRichMetaHTML(richRow);
|
||||
const richClass = richRow ? ` tab-state-${richRow.state}` : '';
|
||||
|
||||
Reference in New Issue
Block a user