Merge #538: Tab Layout (by state, by case, ledger, classic) and three header stats styles (Discussion #426)

This commit is contained in:
Codeman maintainer
2026-10-08 19:44:22 +02:00
17 changed files with 2616 additions and 32 deletions
+24
View File
@@ -897,6 +897,30 @@ Tests: `test/terminal-touch-tap.test.ts`.
⚠️ Mobile no longer hoists the active session to the front of the strip: that reordering ran on full renders only, so tab order flipped depending on which render path fired, and it renumbered the Alt+N badges. Scroll-into-view replaces it; do not reintroduce it.
### Tab layouts (by state, by case, ledger)
**Tab layouts** (`tabArrangement`, per-device, default `state`; Discussion #426): App Settings → Appearance → Tabs → **Tab Layout**. Four values: `state` (option C), `case` (option A), `ledger` (option B) and `classic`, the strip as before. `<html data-tab-arrangement>` is stamped pre-paint and by `applyTabOrientation()`; the render paths read it through `isTabTriage()`, `isTabClusters()` and `isTabLedger()`. Named groups in the vertical rail (owner tab layouts, `_projectTabGroups()` non-null) win over both groupings, and the grouped tree renders as before.
**By state.** The list is split into four groups: **needs you** (a permission or question dialog; a failed session joins it), **waiting** (idle prompt pending), **working**, **idle** (also ended sessions, an agent that exited inside a live pane, and web tabs). Idle is `quiet` (`TAB_TRIAGE_GROUPS`): its heading element still exists, because it anchors the row's `order` band and, as the lead, holds the row's place beside the brand, but it draws no label or count (`.tab-triage-head--quiet`). `tabStateOrder` (`urgent-first` default, `urgent-last`) decides which end they start from; only the group order flips, never the rows inside a group. The desktop header strip draws a row per group with its label left-aligned in a column that `_sizeTabTriageGutter()` measures to the widest label on screen (re-measured only when the label text changes, and once on `document.fonts.ready`), so no row carries a fixed gutter's worth of empty space. The brand leaves the flow and sits over the strip's top-left corner (`.header:has(...)`), so the first row's heading (`.tab-triage-head--lead`, natural width) is pushed past it by `--tab-triage-brand` (a ResizeObserver keeps that width, so no render pass reads layout for it) and every later row starts at the edge, under "Codeman"; the flat rail and the sidebar draw a section per group; the tablet strip (600-767px) keeps its scrolling row with the headings as inline dividers; phones keep their chip row in group order with the headings hidden (mobile.css). Classification is `_mobileOverviewState()` + `_mobileOverviewExit()`, the home screens' own; the fold into four groups and the order bands are pure in `CodemanTabTriage` (constants.js, `computeTabTriageLayout()`).
⚠️ **By state is the flex `order` property, never a DOM reorder**, exactly like the sorted rail: `#sessionTabs` stays in `sessionOrder`, so the Alt+N badges, drag, the keyboard walk (which sorts by COMPUTED order) and the sidebar filter keep reading the list they always read. Each group owns a band of `TAB_TRIAGE_STRIDE` order values: its heading, its rows, its web tabs, then the row break that ends the header line. The headings and breaks are `aria-hidden` direct children of `#sessionTabs`, reconciled in place by `_syncTabTriageChrome()` after BOTH render paths, because a state change is an incremental pass and can still empty a group or fill a new one. Inside a header row tabs keep TAB order; a sorted rail ranks each section the way it ranks the flat rail. The header strip only wraps into rows on desktop, where `updateTabOverflowMode()` forces `tabs-auto-wrap`; the breaks only display in a wrapping strip, since a `flex-basis: 100%` break in a nowrap scroller would steal width.
**By case.** Each case's tabs sit in one `.tab-cluster` box (`_renderTabClusters()`), in the order the case first appears in the tab order; membership is `_mobileOverviewCaseFor()` (longest case path that prefixes the working directory), else the directory, else the session alone. A box with two or more tabs is labelled (swatch, name, count) and its generated `w<n>-<case>` names drop the `-<case>`, which stays in the DOM in a `.tab-name-case` span only `.tabs-clusters` hides; a one-tab case gets only the swatch in the header strip; a web tab gets a box of its own. The colour is `--session-<colour>` by a stable hash of the case key (`CodemanTabClusters.colorFor()`), so nothing is stored. The rail and the sidebar draw every case as a labelled section; phones dissolve the boxes (`display: contents`) into the one chip row.
⚠️ **The boxes ARE a DOM grouping**, unlike by state, which is safe only because a tab changes case far less often than it changes state: `_tabClusterLayout().key` is the whole structure, and the incremental path rebuilds whenever it differs from the last full render's. The keyboard walk keeps boxes together (box index first, then computed order, since a sorted rail orders rows inside each box). Drag only reorders inside one box, as it only reorders inside one state group (`_isTabDropAcrossGroups()`).
**Ledger.** CSS only, on `.tabs-ledger` inside `@media (min-width: 768px)` and the header host: the flat list on an auto-fill column grid with equal cells, mono type and a 3px status bar (`--ledger-bar`: yellow for a waiting alert, red for an action alert or an error, muted for stopped, exited and web tabs) instead of the dot. ⚠️ It must not change the markup (pinned: the ledger's innerHTML equals classic's); the rail, the sidebar and narrower strips show the plain list.
With `classic` nothing is left behind: no headings, no breaks, no inline order, no boxes, no name split, no arrangement class. A stale cached mobile-overview.js degrades every grouping to the flat strip. Tests: `test/tab-triage.test.ts`, `test/tab-clusters.test.ts`.
### Header stats styles
**Header stats styles** (`headerStatsStyle`, per-device, desktop only, default `tiles`; Discussion #426 option G): how the WS readout, CPU, MEM and the plan-usage windows are drawn. `tiles` gives each its own label-over-value tile with a bar along the bottom edge; `compact` is two pills, WS / CPU / MEM and the plan windows, with a ring beside every value (stat rings accent, red past 80%; plan rings green, yellow or red); `classic` is the bars and `5H · 7D` chip as before. `data-header-stats` on `<html>` (pre-paint, then `applyHeaderStatsStyle()` from `applyHeaderVisibilitySettings()`) drives every rule; narrower than 768px and in a solo window it resolves to `classic`, because the stats are hidden there anyway and the moves below would otherwise reach the phone header.
⚠️ The clustered styles need the three elements contiguous, and the template keeps the classic order, so `applyHeaderStatsStyle()` MOVES `#connectionIndicator` into `#headerSystemStats` (first child) and `#planUsageChip` right after it, and `classic` moves them back to comment anchors it left at the template positions. Ids are unchanged, so every writer still finds them. ⚠️ The indicator joins the pill only while System Stats is shown: the pill is hidden with `display: none`, and the WS readout must not disappear with it.
⚠️ The extra parts (`.stat-ring`, `.connection-tile`, `.pu-ring`, `.pu-meter`) are always rendered and hidden by default in styles.css, which is what keeps `classic` looking exactly as before. ⚠️ A tile is a three-row grid (9px label, 14px value, 2px bar) with pixel line-heights, 36px tall like the header, in the bundled JetBrains Mono. The first version laid the bar over the bottom of a fixed 28px tile, and a taller system mono (SF Mono) pushed the value into it; the bar is now a real row, so the height comes from the layout, not the font. The WS tile keeps its grid on the inner `.connection-tile` span, because `_updateConnectionIndicator()` writes the indicator's own `display` inline. The tile words come from `_connectionTileWords()`, DERIVED from the connection descriptor rather than added to it, because `test/connection-indicator.test.ts` pins the descriptor's exact shape. Plan rings and meters clamp their fill to 0-100 while the label keeps the real number. Tests: `test/header-stats-style.test.ts`.
### Phone overview home screen
**Phone overview home screen** (`mobile-overview.js`, phones only, per-device `mobileOverviewEnabled`, default ON): under 600px the "C" logo shows a session overview (NEEDS YOU / CURRENT SESSIONS / PAST SESSIONS) instead of the welcome overlay; tablet and desktop are unchanged. The branch lives in `showWelcome()`/`hideWelcome()` (terminal-ui.js) behind `shouldUseMobileOverview()`, which is **width-driven** (`getDeviceType() === 'mobile'`) because this is a layout decision, unlike the settings namespace which stays handheld-based.
+5 -1
View File
@@ -69,6 +69,8 @@ by folder** (per device, on by default) shows changed files under collapsed fold
window; off lists every file by its full path.
Most default to off. The stock desktop header is system stats, File Viewer, and the gear.
**Header Stats Style** picks how the system stats and plan usage are drawn: *Tiles*
(default; label over value with a bar underneath), *Compact* (two pills with a ring beside every value) or *As before* (the bars and the `5H · 7D` chip). Desktop only, per device.
New header controls never appear on phones. Split is desktop-only regardless of this
setting — the button and the feature both stay off below a ~1180px viewport, where two
resizable panes plus their divider have nowhere to go. **Tiles** is desktop-only the same
@@ -88,7 +90,9 @@ every session or only the active tab.
| Interface Language | English or Simplified Chinese. Per device. |
| Session List Layout | Header tab strip (default), a collapsible left sidebar, or the sidebar with detailed rows. See [The Dashboard](The-Dashboard#session-list-layout). |
| Tab Orientation | Keeps the header list but turns the strip vertical beside the terminal, resizable, with detailed rows by default. Desktop and tablet only. |
| Vertical Rail Order | *By activity* (default) sorts the rail the way the home screens are sorted; *Manual* keeps your tab order and drag-reordering. |
| Tab Layout | *By state* (default): a row each for needs you, waiting, working and idle, sections in the rail and sidebar. *By case*: one box per case. *Ledger*: an aligned column grid. *Classic*: the single list as before. See [The Dashboard](The-Dashboard#tab-layouts). |
| State Order | For *By state*: needs you on top (default) or at the bottom, right above the terminal. |
| Vertical Rail Order | *By activity* (default) sorts the rail the way the home screens are sorted; *Manual* keeps your tab order and drag-reordering. With *By state* or *By case* it orders the rows inside each section. |
| Tall Tabs | Taller tab strip. |
| Pop-out Button on Tabs | Adds the detach control to tabs, with a per-tab override. |
| Spawn Lineage Lines | Lines from each tab to the sessions it spawned; the selected tab's family is drawn thicker. Desktop only, on by default. |
+49 -2
View File
@@ -27,7 +27,7 @@ Session List Layout** can move it into a vertical sidebar on the left instead, a
| Layout | Behaviour |
| -------------------- | --------------------------------------------------------------------------------- |
| **Header tab strip** | The default. Wraps to a second row on desktop, scrolls sideways on a phone. |
| **Header tab strip** | The default. On desktop it is one row per state by default (see [Tab layouts](#tab-layouts)); it scrolls sideways on a phone. |
| **Left sidebar** | A vertical list with a filter box and a live session count. `Alt+B` collapses it to a narrow rail that keeps the status dots and task badges visible. On a phone it is an off-canvas drawer rather than a docked rail. A detailed variant adds the home screen's per-session line (`created 3d ago · working 12m`) and a status pill. |
| **Vertical rail** | The strip turned vertical beside the terminal, resizable, with detailed rows by default. **Vertical Rail Order** sorts it by activity (blocked on you first, then longest running, then most recently quiet), the same order as the home screens; pick *Manual* to get your own order and drag-reordering back. **Tab groups:** pick *Move to new group* from a row's ⋯ menu (or Shift+F10 on it) to make the first one; a group header's menu (right-click, Shift+F10 or its ⋯ glyph) renames it (also F2), reorders or deletes it, rows move between groups from their own menu or by dragging with a mouse or pen, and a collapsed group stays collapsed on that device. Desktop and tablet only. |
@@ -35,6 +35,40 @@ It is the same list either way, just re-hosted: tab order, drag-to-reorder, the
to `Alt+9` numbers and every status colour below behave identically in both. The setting is
per device, so a sidebar on your desktop does not force one onto your phone.
## Tab layouts
**App Settings → Appearance → Tabs → Tab Layout** picks how the tabs are arranged. Per device.
| Layout | What it does |
| ---------------------- | ------------------------------------------------------------------------------------ |
| **By state** (default) | Groups the tabs by what each session needs from you (below). |
| **By case** | One box per case, labelled with the case and its tab count. Inside a box, `w75-api-gateway` reads just `w75`. A case with one tab gets a box with a colour swatch. |
| **Ledger** | The same list on an aligned column grid: equal cells, monospace names, a coloured bar on the left of each cell instead of the dot (yellow waiting, red needs you). Desktop header only. |
| **Classic** | The single list in tab order, as before. |
**By state** groups the tabs like this, most urgent on top:
| Group | Who is in it |
| ------------- | ----------------------------------------------------------------------------------- |
| **Needs you** | Red: a question or permission 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, including ended sessions, agents that exited inside their pane, and web tabs. |
In the header each group is a row with its name and count on the left (Idle, the quiet
default, carries no label); a group with more tabs than fit on one line continues on the
next line. **State Order → Needs you at the
bottom** turns the rows the other way up, so the needs-you row sits right above the
terminal. Empty groups are not shown. These are the same states the phone overview and the
desktop home rail use, and tabs move between groups on their own as their state changes.
Both groupings also apply to the vertical rail and the left sidebar, as labelled sections.
Inside a group or a box tabs keep your tab order (on a rail sorted *By activity*, the
activity order), and the `Alt+1` to `Alt+9` numbers never change. Dragging reorders tabs
within a group or box. On a phone the strip stays a single scrolling row in group order,
without labels or boxes. If you have named tab groups in the vertical rail, those take
precedence there.
## Session tabs
One tab per session, in your order, and that order syncs across your devices.
@@ -107,7 +141,7 @@ The right side of the header. Almost all of these are off until you enable them
| ---------------------- | ------------------ | ------------------------------------------------------------------------------- |
| Connection dot | Always on | SSE connection health. Green is connected. |
| Font size `-` / `+` | Always on | `Ctrl +` / `Ctrl -` do the same. |
| CPU / MEM bars | On | Server resource use. |
| CPU / MEM | On | Server resource use. Drawn as tiles by default; see Header Stats Style below. |
| File Viewer | On | Toggles the file browser panel. |
| Settings gear | Always on | App Settings. |
| Plan usage chip | On, desktop only | Live Claude subscription usage. Claude-only, and needs its telemetry exporter, which the same setting installs. |
@@ -127,6 +161,19 @@ The right side of the header. Almost all of these are off until you enable them
| Tunnel indicator | When a tunnel runs | Cloudflare tunnel status. |
| Admin panel | Multi-user only | User administration. |
### Header Stats Style
The connection readout, CPU, MEM and the plan usage windows can be drawn three ways
(**App Settings → Header & Panels → Header Stats Style**, per device, desktop only):
| Style | Look |
| -------------- | -------------------------------------------------------------------------------------- |
| **Tiles** | The default. One small tile each (`WS live`, `CPU 22%`, `MEM 14.4G`, `5H 28%`, `7D 35%`): label over value, a thin bar underneath, no icons. |
| **Compact** | Two slim pills, `WS · CPU · MEM` and the plan windows, with a small ring beside every value. Hands the tabs back the most room. |
| **As before** | The bars and the `5H · 7D` chip, exactly as they were. |
Hiding System Stats or Plan Usage still hides them in every style.
New header controls never appear on phones. Phone layout is deliberately minimal and is
covered in [Mobile Guide](Mobile-Guide).