mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
feat(tabs): Tab Layout setting with by case (A) and ledger (B), reversible state order, cleaner tiles (#426)
Tab Grouping becomes Tab Layout (tabArrangement: 'state' | 'case' | 'ledger' | 'classic', default 'state'), the first row of App Settings, Appearance, Tabs, so the old and the new strip are one choice apart. - By case (option A): each case's tabs sit in one .tab-cluster box in first-appearance order, labelled with the case and its count, coloured by a stable hash into the session palette. Membership is _mobileOverviewCaseFor(), the home screens' own match. Inside a box with company a generated w75-api-gateway reads w75; the -<case> stays in the DOM in a .tab-name-case span only .tabs-clusters hides. The incremental render path rebuilds only when the cluster structure key changes. The rail and the sidebar get a labelled section per case; phones dissolve the boxes into the chip row. Drag stays inside one box. - Ledger (option B): CSS only on .tabs-ledger, desktop header strip: an auto-fill column grid of equal cells in mono type with a 3px status bar. Its markup is identical to classic's. - State Order (tabStateOrder: 'urgent-first' | 'urgent-last'): flips the by-state groups so needs you can be the bottom row. - By state: the label column is measured to the widest label on screen and the labels are right-aligned in it, instead of a fixed 92px gutter that left short labels far from their tabs. - Tiles: a three-row grid (label, value, bar) with pixel line-heights in the bundled JetBrains Mono, 36px like the header. The bar used to lie over a fixed 28px tile, and a taller system mono (SF Mono) pushed the value into it. The WS tile's grid moved onto an inner .connection-tile span because JS writes the indicator's display inline. Compact uses the same font and a matched WS size. Tests: test/tab-clusters.test.ts (new), plus the rename and the reversed order in test/tab-triage.test.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -867,15 +867,21 @@ 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 grouping by state
|
||||
### Tab layouts (by state, by case, ledger)
|
||||
|
||||
**Tab grouping by state** (`tabGrouping`, per-device, default `state`; Discussion #426 option C): the tab list is split into four groups, most urgent first: **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). 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; 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, and the fold into four groups is pure in `CodemanTabTriage` (constants.js, `computeTabTriageLayout()`).
|
||||
**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.
|
||||
|
||||
⚠️ **It 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 already 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 (no tab added or removed) and can still empty a group or fill a new one. An unchanged pass writes nothing.
|
||||
**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). `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 right-aligned in a left gutter 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 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()`).
|
||||
|
||||
⚠️ Inside a group a header row keeps TAB order, so the strip only moves a tab when its state changes; a sorted rail ranks each section the way it ranks the flat rail (`_tabRailSortOrder()` feeds `pos`). Drag stays on in the strip, but `_isTabDropAcrossTriageGroups()` refuses a drop on a tab in another band: the dragged tab would stay in its own group and land where the user did not put it.
|
||||
⚠️ **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.
|
||||
|
||||
⚠️ **Named groups win**: in the vertical rail with owner tab layouts (`_projectTabGroups()` non-null) triage is off and the grouped tree renders as before. With `tabGrouping: 'none'` nothing is left behind: no headings, no breaks, no inline order, no `tabs-triage` class. A stale cached mobile-overview.js degrades to the flat strip. The header strip only wraps into rows on desktop, where `updateTabOverflowMode()` forces `tabs-auto-wrap` while `tabs-triage` is on; the breaks only display in a wrapping strip, since a `flex-basis: 100%` break in a nowrap scroller would steal width. Tests: `test/tab-triage.test.ts`.
|
||||
**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
|
||||
|
||||
@@ -883,7 +889,7 @@ Tests: `test/terminal-touch-tap.test.ts`.
|
||||
|
||||
⚠️ 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-spark`, `.connection-tile-label`/`-value`, `.pu-ring`, `.pu-meter`) are always rendered and hidden by default in styles.css, which is what keeps `classic` looking exactly as before. 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`.
|
||||
⚠️ The extra parts (`.stat-spark`, `.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
|
||||
|
||||
|
||||
@@ -89,8 +89,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. |
|
||||
| Tab Grouping | *By state* (default) splits the tabs into needs you, waiting, working and idle: rows in the header, sections in the rail and sidebar. *None* keeps one list in tab order. See [The Dashboard](The-Dashboard#tab-grouping-by-state). |
|
||||
| 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 grouping by state it orders the rows inside each section. |
|
||||
| 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 | Arcs from a parent tab to sessions it spawned. Desktop only, on by default. |
|
||||
|
||||
+22
-14
@@ -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. On desktop it is one row per state (see [Tab grouping by state](#tab-grouping-by-state)); it 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,9 +35,18 @@ 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 grouping by state
|
||||
## Tab layouts
|
||||
|
||||
By default the tabs are grouped by what each session needs from you, most urgent on top:
|
||||
**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 |
|
||||
| ------------- | ----------------------------------------------------------------------------------- |
|
||||
@@ -47,18 +56,17 @@ By default the tabs are grouped by what each session needs from you, most urgent
|
||||
| **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; a group with more
|
||||
tabs than fit on one line continues under its own tabs. The vertical rail and the left
|
||||
sidebar show the same groups as sections. Empty groups are not shown. These are the same
|
||||
states the phone overview and the desktop home rail use.
|
||||
tabs than fit on one line continues under its own tabs. **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.
|
||||
|
||||
Tabs move between groups on their own as their state changes. Inside a group they 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. On a phone the strip stays a
|
||||
single scrolling row: the tabs come in group order, without the headings. If you have named
|
||||
tab groups in the vertical rail, those take precedence there.
|
||||
|
||||
Turn it off with **App Settings → Appearance → Tabs → Tab Grouping → None** to get one list
|
||||
in tab order. Per device.
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user