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
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-06 04:08:01 +02:00
parent ac94f339ac
commit 9b0d305223
16 changed files with 1543 additions and 26 deletions
+26
View File
@@ -6028,6 +6028,11 @@ Object.assign(CodemanApp.prototype, {
}
}
// Sparklines for the Compact header style. Always recorded (four numbers a
// poll), so switching styles shows history at once instead of empty bars.
this._pushStatSpark('cpu', stats.cpu);
this._pushStatSpark('mem', stats.memory?.percent);
if (memEl && memBar) {
const memGB = (stats.memory.usedMB / 1024).toFixed(1);
memEl.textContent = `${memGB}G`;
@@ -6045,6 +6050,27 @@ Object.assign(CodemanApp.prototype, {
}
},
/**
* Record one percentage sample and redraw that stat's sparkline: one bar per
* sample, newest on the right, as many samples as the template has bars.
* Bars stay 10% tall at minimum so an idle machine still draws a baseline.
*/
_pushStatSpark(key, percent) {
const history = (this._statSparkHistory ||= { cpu: [], mem: [] })[key];
if (!history) return;
const el = this.$(key === 'cpu' ? 'statCpuSpark' : 'statMemSpark');
const size = el?.children.length || 4;
const value = Number(percent);
history.push(Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0);
while (history.length > size) history.shift();
if (!el) return;
const offset = size - history.length;
for (let i = 0; i < el.children.length; i++) {
const sample = history[i - offset];
el.children[i].style.height = sample === undefined ? '' : `${Math.max(10, Math.round(sample))}%`;
}
},
// ─── Clipboard ──────────────────────────────────────────────────────────────
async _onClipboardWrite(data) {