Skip to content

feat: group tabs by state and add three header stats styles (Discussion #426) - #538

Open
Ark0N wants to merge 1 commit into
masterfrom
feat/tab-triage-header-stats
Open

Ark0N wants to merge 1 commit into
masterfrom
feat/tab-triage-header-stats

Conversation

@Ark0N

@Ark0N Ark0N commented Oct 4, 2026

Copy link
Copy Markdown
Owner

This builds two of the directions from the tab strip discussion (#426): C · Triage for the tabs and G · the right side of the header. Each lands as a setting next to the existing tab and header options, and each one is the new default.

Tab Grouping, new default "By state". Tabs are grouped by what each session needs from you, most urgent on top:

Group Who is in it
Needs you Red: a permission or question prompt is blocking the agent. A failed session too.
Waiting Yellow: the agent finished its turn and is waiting for your next prompt.
Working A turn is running.
Idle Everything quiet: idle, ended, an agent that exited inside its pane (#446), and web tabs.

On desktop the header strip becomes one row per group, with the group name and count on the left. The vertical rail and the left sidebar show the same groups as sections. Empty groups are not drawn. App Settings → Appearance → Tabs → Tab Grouping → None brings back the single list in tab order.

Header strip grouped by state, tiles on the right

Header Stats Style, new default "Tiles". App Settings → Header & Panels → Header Stats Style picks how the WS readout, CPU, MEM and the plan usage windows are drawn. These are the three variants from the G mockup:

As before Compact Tiles (default)
classic compact tiles

There are no server behaviour changes. The settings schema gains two optional enum keys (tabGrouping, headerStatsStyle), and both are per-device display keys like the other tab options, so a phone and a desktop can differ. Because both are new defaults, every desktop picks them up on its next load; None and As before are the way back.

How the grouping works

  • Same states as the home screens. A tab's group comes from _mobileOverviewState() and _mobileOverviewExit(), the classifier the phone overview, the desktop home rail and the activity-sorted rail already use, so the strip cannot disagree with them about who is waiting. The fold from six states into four groups is pure and lives in CodemanTabTriage (constants.js).
  • Visual order only, never a DOM reorder. Like the sorted rail, the grouping is the flex order property plus one heading and one row break per group (aria-hidden, so the tablist still holds only tabs). #sessionTabs stays in tab order, so the Alt+1..9 numbers never change, the arrow-key walk follows what you see, and drag still works. A drop is only accepted inside the same group; across groups the tab would stay in its own group anyway and land somewhere you did not put it.
  • A state change does not rebuild the strip. It is an incremental render pass: the tab element survives, only its order and the heading counts move. Headings are reconciled in place, so an SSE tick that changes nothing writes nothing.
  • Inside a header row, tabs keep tab order, so a tab only moves when its state changes. On a rail sorted By activity, each section is sorted by activity, as the whole rail was before.
  • Named groups win. In the vertical rail, owner tab groups (feat(tabs): grouped vertical rail from owner tab layouts #517) take precedence and render exactly as before.
  • Narrow screens. Phones keep the single scrolling chip row from fix(mobile): make the phone header tab strip read as live tabs #504, in group order, without the headings (the phone overview is where the labelled sections live). Tablets between 600 and 767px keep their scrolling row and show the headings as inline dividers.

Vertical rail with state sections

How the header styles work

  • data-header-stats on <html> drives every rule. It is stamped by the pre-paint script and then by applyHeaderStatsStyle() on load and on save. Below 768px and in a detached solo window it is always classic; the stats are hidden there anyway.
  • The template keeps the classic order, so the two clustered styles move #connectionIndicator into #headerSystemStats and #planUsageChip right after it. Switching back to As before returns both to comment anchors left at their template positions. Ids are unchanged, so every writer still finds them. The WS readout only joins the pill while System Stats is shown, so hiding System Stats never hides WS.
  • The extra parts (sparklines, rings, meters, the two tile words for WS) are always rendered and hidden by default in CSS. That is what keeps As before pixel-identical to today. The WS tile words are derived beside the connection descriptor rather than added to it, because test/connection-indicator.test.ts pins the descriptor's exact shape.
  • Light skins work from the existing control tokens:

Light skin

Testing

  • npm test (the CI gate): 458 files and 8899 tests pass (12 skipped).
  • typecheck, lint, format:check, check:public-assets, check:frontend-syntax, check:browser-excludes and check:lockfile pass, and both stylesheets parse with PostCSS.
  • New: test/tab-triage.test.ts (23 tests: the pure layout, plus the shipping app.js and mobile-overview.js in JSDOM, covering incremental moves, heading reconciliation, exited and failed sessions, none leaving no trace, the sorted rail, named groups winning, the drag guard, and a stale mobile-overview.js degrading to the flat strip) and test/header-stats-style.test.ts (13 tests: run on the real <header> from index.html, covering the moves, the round trip back to the template order, WS with System Stats hidden, rings and meters, tile words and sparklines).
  • Updated: three static pins in test/tab-rail-order.test.ts that matched source lines I renamed (railSortOrder is now fed through listOrder). The behaviour they guard is unchanged.
  • Browser suites, which the gate does not run, compared against a clean origin/master worktree: tab-activation, session-sidebar-ux, tab-rail-resize and inline-rename show the same single failure as master (the rail rename clamp test that fix(tabs): grouped rail interaction fixes for inline rename #526 addresses). test/mobile/tabs.test.ts matches master (one failure there too, in keyboard focus). One swipe test failed once on the branch and passed on two reruns.
  • Live, on an isolated beta instance (CODEMAN_INSTANCE) with Playwright at 1440, 700 and 390px wide, dark and light skins: a permission prompt sent through the real hook route moved a tab into Needs you over SSE and back out on stop. Changing both settings in the App Settings modal applied them without a reload and persisted them.

Reviewing the diff

About 1,540 added lines, but most of them are not logic:

  • Tests, about 610: the two new test files.
  • styles.css +370: two self-contained blocks appended at the end of the file (tab grouping, then header stats styles). No existing rule is edited.
  • Docs, about 70 lines: docs/wiki/The-Dashboard.md, docs/wiki/Settings-Reference.md, two sections in docs/architecture-invariants.md and two entries in CLAUDE.md.
  • The logic, about 490 lines including their comments: constants.js (the pure grouping helper, mostly comments), app.js (the gate, the per-pass layout, heading reconciliation, the drag guard, plan rings and meters, WS tile words), settings-ui.js (load, save, apply) and panels-ui.js (sparkline history).

Not in this PR

The screenshots use invented session names.

)

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>
Sign up for free to 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