feat(tabs,header): group tabs by state and add header stats styles (#426)

Two directions from Discussion #426, each a per-device setting and the
new default.

Tab Grouping (tabGrouping: 'state' | 'none', default 'state', option C):
tabs are grouped needs you (red, plus failed sessions), waiting (yellow),
working and idle (also ended, exited panes and web tabs), most urgent on
top. The desktop header strip draws a row per group with its label and
count in a left gutter; the flat vertical rail and the sidebar draw a
section per group; tablets keep their scrolling row with inline dividers;
phones keep the chip row in group order without headings. Classification
is the home screens' own (_mobileOverviewState/_mobileOverviewExit), the
fold into four groups is pure in CodemanTabTriage (constants.js). It is
flex `order` plus aria-hidden heading/break elements reconciled in place
after both render paths, never a DOM reorder, so Alt+N, the keyboard walk
and drag keep reading tab order; a drop is refused across groups. Named
groups in the vertical rail take precedence.

Header Stats Style (headerStatsStyle: 'classic' | 'compact' | 'tiles',
default 'tiles', option G): tiles give WS, CPU, MEM and each plan window
a label-over-value tile with a bar underneath; compact is one WS/CPU/MEM
pill with sparklines plus a plan-ring pill; classic is the header as
before. Desktop only (classic below 768px and in solo windows). The
clustered styles move #connectionIndicator into #headerSystemStats and
#planUsageChip after it, and classic moves them back to comment anchors;
WS stays out of a hidden System Stats pill. The extra parts are always
rendered and hidden by default in CSS, so classic is unchanged.

Tests: test/tab-triage.test.ts, test/header-stats-style.test.ts; three
source pins in test/tab-rail-order.test.ts follow renamed lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-05 01:57:25 +02:00
parent ffaa5ee80c
commit 96f89718a7
16 changed files with 1543 additions and 26 deletions
+11 -10
View File
@@ -205,10 +205,11 @@ describe('vertical tab rail row order', () => {
it('keeps the Alt+N badge on the tab index while the cards are sorted', () => {
// `_tabIdx` counts the loop over sessionOrder, and only `style="order:…"`
// moves the card — so the badge names a shortcut, not a row position.
expect(appJs).toContain(
'const railSortOrder = this._tabRailSortOrder(tabOrder.filter((id) => this.sessions.has(id)));'
);
expect(appJs).toContain('` style="order:${railSortOrder.get(id)}"`');
// `listOrder` is the rail sort's map, or the state grouping's when that is
// on (test/tab-triage.test.ts); either way it only sets the inline order.
expect(appJs).toContain('const liveIds = tabOrder.filter((id) => this.sessions.has(id));');
expect(appJs).toContain('const railSortOrder = this._tabRailSortOrder(liveIds);');
expect(appJs).toContain('` style="order:${listOrder.get(id)}"`');
expect(appJs).toMatch(/_tabIdx < 9 \? '<span class="tab-number">' \+ \(_tabIdx \+ 1\)/);
// The loop itself still walks the user's order, which is what makes the
// badge, drag-and-drop and the arrow-key walk agree with each other.
@@ -219,12 +220,11 @@ describe('vertical tab rail row order', () => {
// A session going working→idle never adds or removes a tab, so the full
// rebuild is not reached — and a rebuild here would restart every card's
// animation on every SSE tick anyway.
expect(appJs).toContain(
'const railSortOrder = this._tabRailSortOrder(this.sessionOrder.filter((sid) => this.sessions.has(sid)));'
);
expect(appJs).toContain('const liveIds = this.sessionOrder.filter((sid) => this.sessions.has(sid));');
expect(appJs).toContain('const railSortOrder = this._tabRailSortOrder(liveIds);');
expect(appJs).toContain('if (tab.style.order !== railOrder) tab.style.order = railOrder;');
// An empty string is what clears the property when the rail stops sorting.
expect(appJs).toContain("const railOrder = railSortOrder?.has(id) ? String(railSortOrder.get(id)) : '';");
expect(appJs).toContain("const railOrder = listOrder?.has(id) ? String(listOrder.get(id)) : '';");
});
it('drops the drag affordance while sorting, so a card cannot snap back', () => {
@@ -280,9 +280,10 @@ describe('vertical tab rail sort setting', () => {
expect(html).toContain("document.documentElement.dataset.tabRailSort='activity';");
expect(settingsUi).toContain('root.dataset.tabRailSort = sort;');
// A sort flip leaves orientation on 'vertical' both times, so it has to
// count as a change of its own or nothing re-renders.
// count as a change of its own or nothing re-renders. The same holds for a
// grouping flip (tabGrouping, test/tab-triage.test.ts).
expect(settingsUi).toContain(
'const changed = orientationChanged || previousDetail !== detail || previousSort !== sort;'
'const changed = orientationChanged || previousDetail !== detail || previousSort !== sort || previousGrouping !== grouping;'
);
});
});