Commit Graph
209 Commits
Author SHA1 Message Date
Codeman maintainer 1568e489eb fix(tabs): never size the state label column from hidden headings
#538's by-state header strip lines its labels up in a column measured into
--tab-triage-gutter by _sizeTabTriageGutter(). Under 600px mobile.css hides
the headings (the phone row keeps its chips, no labels), but the sizer
measured them anyway: every part was 0 wide, yet `width + 5 * (parts - 1)`
made each labelled heading 5, so the "nothing to size" guard never tripped,
the column became 15px and the group key was cached as if measured. A phone
turned to landscape (>= 768px) or a foldable opened (Find N5: folded under
600, unfolded 1124 wide) then crossed into the wrapping strip with no tab
render behind it (the resize handler only calls updateTabOverflowMode()),
the key had not changed, and "WAITING 2" sat in a 15px column on top of the
first tab of its row.

Only the wrapping strip reads the column, so the sizer now measures nothing
and forgets its key while the strip does not wrap, counts only parts that
are laid out (a hidden heading sizes and keys nothing), and
updateTabOverflowMode() calls it right after deciding the wrap. Phones and
tablets therefore never measure (no layout read per render pass), and every
flip into the wrapping strip, the 768px breakpoint included, measures
afresh. The 600px breakpoint only matters below 768px, where nothing is
measured.

Tests in test/tab-triage.test.ts pin that hidden headings size and key
nothing, that the real updateTabOverflowMode() measures on the unfold with
no render, and that wrapping again re-measures with the counts unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 21:36:06 +02:00
Codeman maintainer 4ee382832a fix(tabs): keep the active tab in view when it changes state band on a phone
On phones and 600-767px tablets the header strip stays one horizontally
scrolling row. #538 (Tab Layout, default "by state") orders that row in one
flex `order` band per state, so when the ACTIVE session changes state (a
prompt sent: idle to working; a permission prompt: needs you) its chip
moves to another band while scrollLeft stays put, and the tab in use left
the screen (measured at 390px: x 165 to -870). #257's reveal rules only
covered a CHANGED active tab: _updateActiveTabImmediate reveals on a
switch, and _fullRenderSessionTabs restores scrollLeft and re-reveals only
when _lastRenderedActiveTabId changed, while a state change is an
incremental pass that never reveals at all.

_noteActiveTabBand() records the active tab's band (read off the element,
so it is what is on screen) and reports when it moved while the tab stayed
active. Both render paths reveal on that, in the single scrolling row only
(_isScrollingTabRow: not wrapping, not a vertical list). It is keyed on the
band, not the raw order value, because another tab entering or leaving the
active tab's band shifts that value by one and another tab's move must not
yank a strip the user is browsing. _updateActiveTabImmediate records too,
so the pass right after a switch still counts (viewing a waiting tab spends
its alert and drops it into the idle row). The incremental path reveals
after updateTabOverflowMode(), and only from the branch that reached
_syncTabTriageChrome(), so a pass that falls through to a full rebuild
mid-loop leaves the record for the rebuild to compare.

Tests in test/tab-triage.test.ts pin the reveal on both paths and after a
switch, and that another tab's band change, a wrapping strip, a vertical
list and an active web tab never scroll.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 21:36:06 +02:00
Codeman maintainer d630e62114 Merge #538: Tab Layout (by state, by case, ledger, classic) and three header stats styles (Discussion #426) 2026-10-08 19:44:22 +02:00
Codeman maintainer b1def488c4 Merge #544: lineage lines as routed trees, every family shown, the selected one emphasized 2026-10-08 19:44:20 +02:00
Codeman maintainer fcec77131c fix(tiles): the count menu closes when the keyboard leaves it
Found live: closing the grid with a click starts the single view's
selection, which focuses its terminal when its replay lands, a few
hundred milliseconds later. A right-click on Tiles in between opened the
count menu with the keyboard in it, and the late focus then moved the
keyboard into the terminal while the menu stayed open (3 of 3 tries), so
the arrows, Enter or Escape meant for the menu went to the session's
PTY instead (an Escape arrived there as an ESC byte).

The menu now closes when the keyboard leaves it for another element, as
any menu does. A focus going nowhere (a click on a button in Safari,
which does not focus it) does not count, so a click on a count still
picks it. Live afterwards: the menu either closes as the terminal takes
the keyboard, or keeps it when the replay landed first; never open with
the keyboard elsewhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 05:13:43 +02:00
Codeman maintainer 4ad283c647 docs(tiles): the count menu, the open/close animation and the paced open
- docs/tile-grid-plan.md: owner decision 10 (right-click Tiles is a
  2 / 4 / 6 count menu, default 6, remembered per device; the session
  picker is gone; decision 8's "picker on right-click" and "exactly the
  stored set" superseded) with the owner's answers on the details; three
  as-built bullets (the count menu, the animation, painting first); the
  Tiles-button bullet rewritten for the count; the entry points, the
  capacity note, the multi-user row and the Escape invariant follow.
- docs/architecture-invariants.md: the Opening paragraph rewritten (the
  count, the stored grid in its cells, the menu owning its Escape, the
  paced connect and focusOnConnect, the motion rules); the z-index list
  names the count menu and the closing grid's still copy.
- CLAUDE.md: the tile grid paragraph and the z-index line.
- Wiki: Tile Grid (the click, the count menu, the animation, reduced
  motion) and Keyboard Shortcuts (right-click Tiles).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 04:53:02 +02:00
Codeman maintainer 45ca9c347b feat(tiles): the grid is cells, and an empty cell can be any cell
Owner feedback 1: "so the empty tab doesnt always have to be the last
one! so I can move freely around and the empty tab can also be tab nr
4 or 3". This replaces the slot refusal of aecada8c.

grid.cells (a session id or null per cell) is now the one source of
truth; grid.ids is a getter deriving the tiles in reading order, so
everything that only wants the tiled sessions (focus neighbour,
cycling, the load queue's order, the picker, closeSession) is
unchanged. The shape still comes from the tile count and the cap
counts tiles, never empty cells.

- A tile dragged onto an empty cell moves there and leaves its own
  cell empty, nothing else moving (_moveTileToCell, through
  _reorderTiles: no remount, reconnect or reload; only a tile whose
  cell size changed fits). A tiled session's tab does the same; a tab
  of a session not tiled yet joins in the cell it is dropped on. Each
  slot knows its cell and reads "Drop a tab or a tile here".
- Move Tile goes to the adjacent cell: into it when empty, a swap when
  a tile is there (tileCellInDirection).
- Removing a tile leaves its cell empty; adding one takes the first
  empty cell. A shape change goes through fitTileCells: each tile keeps
  its row and column when all fit (2x2 growing to 3x2), else the tiles
  pack in reading order.
- Focus never lands on an empty cell: Alt+Shift+Arrows run over the
  cells, Ctrl+Tab and Alt+[ ] over the tiles.
- codeman:tile-grid stays ids only: its ids are the cells with null for
  an empty one. A reload brings the holes back when the shape is the
  same (a session gone since leaves its cell empty), another shape
  packs, the old packed format reads unchanged, and a followed
  #session= link keeps the holes.

Docs: the spec's as-built bullet (rewritten in place), the wiki's Tile
Grid page and Keyboard Shortcuts, the invariants and CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 03:08:14 +02:00
Codeman maintainer aecada8c56 fix(tiles): no tile onto an empty slot, header focus on click, arrow chords skip text fields
The owner's answers on moving tiles:

- "dont move the tile": an empty slot no longer takes a tile. A slot is
  always the last cell, so a move there shifted every tile after it.
  Each drop target now says what it accepts (_acceptTabDrops'
  `accepts`): a tile takes any session but its own, an empty slot only a
  session not tiled yet. A refused drag is still held (dropEffect none,
  no highlight), and dropSessionOnSlot refuses a tiled session too, its
  tab included. A session not tiled yet still joins on a slot.
- A cancelled drag changes nothing, focus included (best practice): the
  header focuses its tile on click, never on press, so a drag that ends
  with Escape or outside leaves focus and the idle alert alone. The body
  keeps press-to-focus, so focus still moves before a press reaches
  xterm. The rename input stops its own clicks.
- A tiled tab dropped on the zoomed tile stays refused (confirmed).
- The Alt+Shift+Arrow focus chords skip a text field too (best
  practice), as the move chords already did: shifted arrows select
  there. Toggle and zoom are not text-editing keys and are unchanged.

Docs: the wiki, the spec's as-built bullet (with the owner's answers),
the invariants and CLAUDE.md say so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 01:28:20 +02:00
Codeman maintainer adeb22d7b2 docs(tiles): moving tiles, by the header and by Ctrl+Shift+Arrows
The wiki's Tile Grid page gets a "Moving tiles" section and the move
chords in its keys table (with what they leave to a text field and
take from a terminal editor inside a tile); Keyboard Shortcuts lists
the chords and the header drag. The spec records moving as an owner
request in its as-built list, with the reasoning behind the default
keys. The invariants and CLAUDE.md say every move goes through
_reorderTiles (no remount, reconnect or reload; only a tile whose cell
size changed fits) and that the header drag carries its own type and
is not draggedTabId.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 00:13:32 +02:00
Codeman maintainer 9209922ea4 fix(deepseek): reject the dsh footer's non-model words instead of requiring a digit
The digit rule from 21ae48a5 hid the official DeepSeek ids (`deepseek-chat`,
`deepseek-reasoner` carry no digit), so a session on the official route with
the model field on showed the logo alone (its bundle row pins a provider
alone, so the config had nothing either). It also still misread a folder name
with a digit when every field before it was off.

Now the captured field is rejected when it is what the field can be when it is
NOT the model, and read otherwise:

- capabilities.modelDetect.rejectWords (registry data, single tokens, compared
  ignoring case; the schema bounds them and requires a screenLine). dsh lists
  every effort id its adapters offer (pi-ai THINKING_LEVELS plus the DeepSeek
  adapter's off/low/high/max) and the shipped mode ids, from dsh 0.1.1-rc.2 /
  dsh-TUI 0.10.0-beta.1. A mode's drawn label (`plan mode`, `full access`,
  CJK) can never be one captured field.
- In the shared screen reader, for every CLI: a field equal to the session's
  own working-directory basename is the folder, never the model.

Fixtures: `deepseek-chat` and `deepseek-reasoner` with the model field on are
read; every effort id, `default`, `plan mode`, and the folder name first (with
and without a digit) are not; the live qwen footer still reads `qwen3.8-27b`.
Known gaps, all off by default, are named in stock.ts: a custom mode id drawn
raw, a git branch or a one-word session title first, and the non-compact
footer layout (nothing read there; the route config applies).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 21:16:53 +02:00
Codeman maintainer 2e25bfa9e0 docs: the dsh route config as a displayModel source
- api-reference: the `config` source in the displayModel table, read again at
  every pane start, attach and relaunch rather than restored.
- cli-registry: `modelDetect.configResolver` (a named, read-only, bounded
  reader), the stock `deepseek-route` reader and its rules, and why the dsh
  footer pattern needs a digit.
- deepseek-integration §4: Codeman reads the route for display only, the way
  dsh-TUI resolves it; the catalog check it cannot see.
- architecture-invariants (tile grid), tile-grid-plan "as built" (owner
  feedback 1), the wiki's model row, CLAUDE.md's source order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 20:39:30 +02:00
Codeman maintainer c481bf4f47 docs: session headers name the harness and the model (displayModel)
- api-reference: the new `displayModel` session field, its sources in order
  (custom-endpoint, statusline, screen, launch) and that it is untrusted
  display text, persisted and restored when the CLI reported it.
- cli-registry: `capabilities.modelDetect` (one capture group, the last rows
  of the probe's capture, anchored on chrome only that CLI draws), the two
  stock patterns (dsh-TUI, codex) and the fifth config regex.
- architecture-invariants (tile grid): the header painter, the id as data, the
  untrusted model text, no writes for an unchanged session, the truncation
  order, Pane A's strip and its fits through syncTerminalGeometry.
- tile-grid-plan "as built", the wiki's Tile Grid page (logo and model rows,
  Split's strips), and CLAUDE.md's tile grid and CLI registry paragraphs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:32:00 +02:00
Codeman maintainer 7990249e2d docs(tiles): the replay pace, one refit per resize, the SSE filter, the line bound
The invariants for this performance pass: a tile's replay holds the load
queue only while xterm parses it, and destroy() settles a replay in
progress; the main terminal's resize timer refits the split's Pane B
only, leaving grid tiles to the grid's observer; the page's SSE filter
names TILE_GRID_SSE_FILTER while tiles own the terminal; grid tiles send
lines= on their full captures. Plus an "As built" note in the spec, whose
parking section still says the subscription stays [activeSessionId].

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 08:46:08 +02:00
Codeman maintainer 00440c02e1 docs(tiles): no + in the tile header, owner decision 9
The spec records decision 9 and an as-built entry replacing the "+ / New
session in this case" one, and marks the target picture, the header line
and the + bullet as built without it. CLAUDE.md's header list, the
invariants' z-index line (the + menu's layer) and the wiki's Tile Grid
page (the header string, its table and the cap sentence) drop the +.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 02:49:12 +02:00
Codeman maintainer 5da25f782b feat(i18n): the Run button family, and the Help modal and shortcut overlay leftovers, in Chinese
Owner request: translate "Run SH" and the rest of the Run family, plus
the two leftovers from the last report.

Run: one pattern turns "Run <code>" (Run CC, Run SH, Run OC, Run CX ...,
and any registry CLI's shortBadge) into "运行 <code>"; the mode codes and
product names stay, and exact entries still win ("Run Shell" was already
运行 Shell, "Run OMP" 运行 OMP, "Run PI" takes the "Run Pi" entry). New
entries: "Terminal / Shell" (the run menu's shell item) and "Send Enter"
(the phone toolbar's Enter button title). The phone overview's Run button
already showed 运行 beside a mode word kept as typed.

Help modal and shortcut overlay: "Tabs", "Toggle Session Sidebar", both
"Copy Selection" rows, "Focus Tabs", and "Wheel" (滚轮, a mouse input like
Click). Key names stay English: the Help modal's Home key, and every key
the overlay renders, now carry data-i18n-skip, because "Home" is also a
dictionary word (the Home button) and showed as 主页 in the key column.

The invariants' paneExit section gains the badge's translation rule
(from the previous commit): its updates compare with the remembered
English, never the DOM.

Tests: i18n-exit-run-help covers every Run label _applyRunMode can show
(its hard-coded ones and Run <shortBadge> for every stock CLI), the
toolbar titles, the Help modal through the real translator in JSDOM (no
English outside the key column, the Home key kept while the word Home
elsewhere still translates, Wheel translated), every shortcut registry
group and label, and that the overlay's keys are skipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 02:11:01 +02:00
Codeman maintainer 107e87e457 feat(tabs): draw every lineage family, emphasize the selected tab's
Drawing only the selected tab's family hid every other connection until you
clicked into one. All families are drawn again (still as routed trees); the
selected tab's families (what it spawned, and the family it was spawned into)
get .lineage-family--focus: 2.5px instead of 1.5px, full opacity instead of
0.75, a larger end dot, and drawn last so nothing covers them.

Lanes follow strip order (cycling through CodemanLineage.MAX_LANES, 3), never
the selection, so a family's lines never move when you click a tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 01:35:42 +02:00
Codeman maintainer c848e7cf27 feat(i18n): the tile grid in Chinese (zh-CN)
Owner request: with App Settings language set to 简体中文, the grid reads
fully in Chinese. Every string the grid puts on screen gets its own
ZH_CN entry, so none reaches the generic leading-verb fallback: the Tiles
button (both states, with the right-click hint), the Tiles and Split
chips, the grid region, the Help modal's Tiles rows, the shortcut
registry's Tiles group and labels (overlay and App Settings list, and
"not bound"), "Open group as tiles", the picker, a tile's +, the header
buttons, the Attach overlay (not attached, attaching, exited, ended, the
hint), the empty slot, the dividers, the Split button while tiles are
open, and the toasts. Strings with a count, an exit code or a duration
are translateDynamic patterns: the cap texts (both wordings, with and
without ": the new session opens on its own"), "This window fits N
tile(s)", the auto-zoom hint, "The agent exited (N)" / "(signal N)", the
crash-restart confirm (the existing confirm wrapper runs it through t();
the session name passes through untranslated, in the single view too),
and the tile header tooltip ("idle 3m"), which requires the duration so
bare state words stay out of the table (they collide with other
surfaces, see mobile-overview.js).

Wording: 平铺 for the feature, 窗格 for one tile, 附加 for attach, 智能体,
案例, as the table already has them. Key names stay; Click, Right-click
(mouse actions) and Arrows in the Help modal's key column are translated.
English reads exactly as before (only additions to the table).

test/tile-grid-i18n.test.ts drives the real tile code through every
state that writes text, harvests each string and requires Chinese with
no Latin word left beyond key names and durations, and the same English
in en; plus the markup through the real translator in JSDOM, and session
and group names (also when they equal a UI word) staying untranslated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 01:15:43 +02:00
Codeman maintainer 09daf0fe49 feat(tabs): draw lineage lines as one tree for the selected tab's family
Lineage lines used to draw one glowing dashed bezier per parent/child pair,
hanging below the tab strip, for every family at once. With a parent on row 3
of a wrapped strip and ten children below it, the curves crossed every lower
row's tab names and ran through the terminal text.

- One rounded orthogonal tree per spawning tab: every route starts at the
  parent, so siblings share a trunk. 1.5px, solid, 1px dark outline; only a
  working child's branch is dashed.
- Routes run only through the gaps between tab rows, joined by a spine left of
  every row (computeLineageTree/computeLineageRows in constants.js), so they
  never cross a tab or reach the terminal. The vertical rail gets the same tree
  on its existing left track.
- Only the selected tab's family is drawn (what it spawned, plus its parent and
  siblings). Selection redraws, and the strip's size transitionend redraws once
  more, since the active tab widens for ~150ms after the selection redraw.
- The routing room is reserved by .session-tabs.lineage-tree, keyed on whether
  any lineage exists, never on the selection, so a tab switch never resizes the
  header or the PTY.
- Colours stay per spawning tab, now claimed in strip order for every family so
  selection order never decides who gets the skin blue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 00:03:37 +02:00
Codeman maintainer 39d87e363f style(tiles): bigger tile header buttons, the app header's icon size
Owner feedback: the tile header's ⋯ ⤢ + × read as tiny next to the
session name. The buttons inherited the header's 12px font. They are now
26px click targets (min-width, so a wider glyph still fits) with a 16px
glyph, the same as the app header's own icon buttons (.btn-icon-header);
the thin ellipsis and cross get 19px so all four read at one visual
size. The header grows from 24 to 28px to hold them, the inline rename
input to 22px. Checked live at DSF 1 on a dark (daylight-blue) and a
light (paper-gray) skin, focused and unfocused tiles, and a zoomed tile.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 23:27:08 +02:00
Codeman maintainer b7fafb1c16 feat(tiles): the Tiles button opens the grid at once; the picker is on right-click
Owner decision 8 ("when I hit the tiles button, open the tiles already!").
A click with the grid closed now opens it straight away, and Ctrl+Shift+G
runs the same function (toggleTileGrid), so the two cannot drift. What
opens comes from one pure helper, tileGridOpenSet (constants.js):
  a. the grid this tab last had, if any of its sessions survive, opened
     exactly (an open split closes and its sessions do not join);
  b. else an open split's two sessions, Pane A focused;
  c. else the open sessions in tab order (the picker's list: no detached
     ones), up to what the grid takes here (the cap of 6, fewer when the
     window fits fewer), the active session always among them and focused.
A click with the grid open still closes it.

The picker moved to right-click (oncontextmenu, browser menu suppressed).
With the grid open it is preselected with the current tiles, and Open
replaces them. Ctrl/Cmd+click on a tab with the grid closed opens the
toggle's set plus that session. The button's title, the Help modal and
the wiki say right-click chooses which sessions.

Docs: decision 8 and an as-built entry in the spec (Entry points too),
CLAUDE.md, the invariants (#tile-grid, Opening), the wiki's Tile Grid and
Keyboard Shortcuts pages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 23:23:55 +02:00
Codeman maintainer d2e72143e8 feat(tiles): the grid holds at most 6 tiles (owner decision 7)
Six was tested smooth on the owner's desktop; nine missed the headless
frame bar and is untested on real hardware. TILE_GRID_MAX (constants.js)
is now 6 and stays the one cap every limit reads; the layout table gets
its own bound, TILE_LAYOUT_MAX = 9, so the 7 to 9 layouts keep working
(unreachable) and going back to nine is that one line.

Every way in stops at the cap: opening, addTile, a tile's +, a session
Run makes, Ctrl/Cmd+click, the picker, "Open group as tiles", and a stored
grid with more ids (it comes back as its first six, focus kept only if it
survives, a dropped zoom cleared, row fractions that no longer match the
3x2 reset). The limits now go through one helper, _tileGridLimit(), whose
texts say which limit binds: "Up to 6 tiles" / "The grid holds at most 6
tiles" when it is the cap, "This window fits N" when it is the window.

Docs: decision 7 and an as-built entry in the spec, CLAUDE.md, the
invariants, the wiki's Tile Grid and Dashboard pages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 23:18:29 +02:00
Codeman maintainer 0d1b91188d docs(tiles): Ctrl+Shift+G inert while the Tiles setting is off is the owner's decision
The spec listed it as the default applied while the owner's answer was
pending. The owner has decided: with showTileGridButton off the chord is
inert and passes through like any unbound key; on, it toggles the grid.
Recorded as decision 6 in the spec (with a line under Gating), and as an
owner decision in CLAUDE.md and the invariants.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 20:47:13 +02:00
Codeman maintainer fa9d5879b3 fix(tiles): a tile that joined before its pane existed resends its size
A session Run makes while the grid is open joins as a tile right away, so
the tile connects and sends its size before Run starts the pane. The
server only records a resize for a session with no PTY and spawns the pane
at 120x40, and Run's own resize step measures the parked main terminal
(display: none, so nothing). Measured live for Shell and Claude: a 97x17
tile over a 120x40 pane, for good (#464).

The chrome refresh now remembers each tile's last-seen pid and calls
TerminalTile.paneStarted() when it appears or changes. paneStarted()
forgets the sent size and sends it; a hidden tile (a zoomed neighbour)
sends nothing and keeps it forgotten, so its next fit() sends it, which a
plain fit({ force: true }) would lose. Keyed on the sessions map, so a
handleInit after an SSE drop counts too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 19:46:04 +02:00
Codeman maintainer f1e5b82ecc docs(tiles): the tile grid in CLAUDE.md, the invariants, the wiki and the Help modal
- CLAUDE.md: a Tile grid paragraph beside the split-pane one (parking, the
  one load queue, the selection and close rules, chords, dividers, auto-join,
  the exited-agent case), tile-grid.js (7.6) in the load order, the
  desktop-gated header markers and the Tiles picker in the z-index stack.
- docs/architecture-invariants.md#tile-grid: the mechanisms and the reason
  behind each rule; the split section now says where a waiting grid load
  differs and that every capture carries a deadline.
- docs/wiki/Tile-Grid.md: the user manual page (turning it on, the ways in, a
  tile's header, keys, leaving, persistence, Split), linked from the sidebar,
  The Dashboard, Keyboard Shortcuts and Settings Reference.
- The Help modal lists the tile chords (pinned in help-modal-shortcuts.test).
- docs/tile-grid-plan.md: status updated, and an "as built" list of where PR 2
  went another way than the spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 19:34:25 +02:00
Codeman maintainer bcdccd14c4 fix(shortcuts): Ctrl+W no longer closes a session
Close Session was bound to Ctrl+W by default. Ctrl+W is delete-word in
every shell, readline prompt and agent CLI, so muscle memory killed the
session (its tmux pane and CLI, with no confirm) mid-sentence, and with
the split pane open it was not even the pane being typed in.

Close Session now has no default key: the capture-phase handler lets
Ctrl+W through and xterm sends ^W to whichever pane is focused. The
action stays in the registry and can be bound in App Settings ->
Shortcuts; the shortcut overlay shows it as not bound. The Help modal,
CLAUDE.md, the split and tile-grid specs and three wiki pages stop
advertising Ctrl+W as kill. Owner decision (tile-grid decision 5).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 15:46:49 +02:00
Codeman maintainer 497711a05e docs: Pane B is a TerminalTile (reconnect, exactly-once input, focus)
CLAUDE.md, architecture-invariants#split-pane-sessions and the split-pane
spec described Pane B as having no reconnect, seq-less input and xterm's
own Ctrl+V. Updated for TerminalTile (terminal-tile.js, load order 7.4):
reconnect and stop codes, the input-socket map and which input is kept
out of the persisted queue, image paste, the geometry rules, and
_focusedPane() with its Ctrl+W exception. The tile-grid spec now records
PR 1 as built (no key handler factory; scheduleLoad and scrollback move
to PR 2 with their first user).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:46:33 +02:00
Codeman maintainer 310f20b288 feat(header): rings for CPU and MEM in the Compact style (#426)
Compact now draws every reading the same way, ring then label then
value: CPU and MEM get an accent ring (red past 80%, like their value)
in place of the sparklines, the plan windows keep their green, yellow
or red rings, the dot separators go, and both pills share one label and
one value style. The sparkline markup, history and CSS are removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 08:44:20 +02:00
Codeman maintainer 55cafc282f feat(tabs): no label on the idle row (#426)
Idle is the default state of a tab, so naming it only adds noise. The
idle group is now quiet: its heading element stays (it anchors the row's
order band and, as the first row, holds its place beside the brand) but
draws no label or count. Needs you, Waiting and Working are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 06:29:40 +02:00
Codeman maintainer bb5fd5ee97 fix(tabs): state rows start under the brand, labels left-aligned (#426)
In the desktop header strip grouped by state, the brand leaves the flow
and sits over the strip's top-left corner, so every row after the first
starts at the left edge under "Codeman" instead of leaving that corner
empty. Labels are left-aligned in the measured column; the first row's
heading takes its natural width beside the brand (--tab-triage-brand,
kept by a ResizeObserver so no render pass reads layout for it).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 06:25:26 +02:00
Codeman maintainer e3dfbf6591 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>
2026-10-06 04:08:02 +02:00
Codeman maintainer 9b0d305223 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>
2026-10-06 04:08:01 +02:00
Codeman maintainer 88f5a43a9f fix(cases): bounded path probe landing fixes (#516)
- hooks-config: a probe the bulk cap refused gets ONE bounded re-probe past the
  cap (probeBeforeTouching), and whatever is still unknown is skipped. The
  per-spawn hook and statusLine helpers used to fall back to an unbounded
  lstat/readFile there, which on a dead workspace never settled and could take
  the last threadpool workers (and hang the boot hook sweep). New test: cap
  engaged, stat/lstat/readFile hanging on two more paths; both helpers return.
- describeUnknownPath()/unknownPathReason(): POST /api/sessions, quick-start and
  GET /api/cases/:name now say a folder was not checked (other mounts are still
  not answering) instead of blaming a healthy folder at the stall ceiling.
  errorCodes unchanged.
- #535 x #516: Create in a custom folder probes the parent through the bounded
  probe before realpath/stat/lstat/readdir touch it; an unknown parent is 422
  OPERATION_FAILED (UNREACHABLE) within the probe timeout. New test.
- Docs: MAX_STALLED default is 2 (follows UV_THREADPOOL_SIZE), CaseInfo
  .unreachable covers a refused probe, the boot sweep skips an unanswering
  workspace, a CLAUDE.md gotcha for bounded probes, verbs.md documents the 422
  (plugin mirror synced), api-reference documents the custom-folder 422.
- Tests: the launcher case-lookup describe is no longer nested in the Grok
  block, and the cap-below-ceiling test no longer depends on an inherited
  UV_THREADPOOL_SIZE / CODEMAN_PATH_PROBE_MAX_STALLED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 20:00:30 +02:00
Aamer Akhter bd4a1e9886 fix(tabs): grouped rail editing review fixes
- Pointer drag: a press released outside the rail no longer lingers. The
  release is heard on window while a press is pending, a move with the
  primary button up cancels it, a new press cancels any previous drag, and
  an existing Escape listener is removed before another is added, so no
  orphaned capture listener can swallow Escape before the terminal.
- Inline group rename: a commit by blur leaves focus where the user put it;
  Enter and Escape still return focus to the header.
- A failed layout read while edits are pending keeps the held layout and the
  editor and re-reads once the write settles, so a 409 is still rebased.
  Dropping unsaved work now always says so in a toast.
- "Move to <group>" quotes the group name (with a matching zh-CN pattern), so
  a group named "New group" or "ungrouped" no longer reads or translates like
  the fixed entries.
- The group menu glyph stays visible under (hover: none).
- The sessionStorage replay copy carries { owner, baseVersion, savedAt } and is
  ignored for another owner, after 60 s, or against an older layout. A move
  with no anchor carries no index, so a replay keeps the row last.
- A 400 that survives the re-read is reported as "Could not save tab groups."
- closeTabRailActionMenu() no longer removes the group menu's DOM.
- Cancelling "Delete group" returns focus to the header.
- Stale comments updated.
2026-10-04 20:21:44 -04:00
Aamer Akhter 97cb5b5799 feat(tabs): edit groups in the vertical rail
The grouped vertical rail can now be edited from the browser: groups are
created, renamed, reordered and deleted, and tabs are moved between them, by
menu, keyboard or pointer drag. Every edit is saved through the existing
PUT /api/tab-layout; there are no server changes.

Saving (tab-layout-browser.js, pure):
- Edits are named operations (createGroup, renameGroup, deleteGroup,
  reorderGroup, moveRef) applied to the rail at once, mirroring the server
  model: a moved session takes the sessions that still follow it, and a
  hand-moved child is marked placement 'manual'. normalizeLayout now keeps
  placement and updatedAt, since whole layouts are written back.
- createEditCoordinator keeps ONE PUT {baseVersion, layout} in flight. Edits
  made in the same turn share a write; edits made while one is in flight go
  out on the version it returns. A 409 replays the operations onto the
  layout the server returned and retries (bounded); an operation that no
  longer applies is dropped and reported. A 400 re-reads first; any other
  failure reports and re-reads.
- dropOperation maps a finished drag to one operation, or null for a drop
  that changes nothing.

Wiring (app.js, tab-rail-resize.js):
- The session row menu gains Move up/down, Move to <group>, Move to
  Ungrouped and Move to new group in the vertical rail. Before the first
  group exists it offers only "Move to new group", which is how a flat rail
  becomes grouped; the header strip's menu is unchanged.
- A group header opens its menu with Shift+F10 / ContextMenu, right-click or
  a hover glyph (a non-focusable aria-hidden span, so the treeitem still
  holds no interactive child): Rename, New group, Move group up/down,
  Delete. F2 renames inline. A web tab row's Shift+F10 opens its settings
  plus the same moves.
- The menu closes on Escape (consumed before the global Escape handler, focus
  back to its row or header), a pointer outside, Tab, focus leaving it, a
  resize, a second open and any full re-render.
- Inline group rename shares the session rename's ownership handle, so only
  the current editor releases the render guard. Enter or blur commits,
  Escape cancels, IME composition keys are left to the IME, and the label
  becomes a flex slot so the editor gets the full width while typing.
- Pointer drag (mouse and pen) in the grouped rail only: rows before/after a
  row or into a group, a header drag reorders groups. Escape cancels; the
  click that ends a drag neither selects nor toggles. The flat rail and the
  header strip keep their HTML5 drag untouched.
- A tab:layoutChanged read is deferred while a write is in flight and run
  once it settles; a read otherwise rebases unsaved edits. On pagehide,
  unconfirmed edits go out in a keepalive PUT and into sessionStorage, and
  replay after reload (a no-op when the keepalive landed).
- New strings have zh-CN entries; group names reach the DOM only as text.

Unchanged: the flat rail's markup when no group exists, the tree semantics
and single roving tab stop, sessionOrder and Alt+N.

Tests: test/tab-layout-editing.test.ts (operations, coordinator, drop
mapping, menus, rename, dismissal, SSE deferral, reload recovery, flat-rail
identity) and test/tab-layout-editing.browser.test.ts (real pointer drags,
editor paint, menu Escape), listed in BROWSER_TEST_GLOBS.
2026-10-04 20:10:32 -04:00
Codeman maintainer 7917273188 fix(tabs): collapsed-header alerts, quieter layout reads, tree key and touch fixes (#517, #519 review)
Maintainer merge-time fixes for the grouped vertical rail (#517) and its
tree semantics (#519), from the two PR reviews.

#517 minors
- A collapsed group hid rows that need the user with no signal on its
  header. The header now takes the most urgent alert among the session
  rows its collapse hides, in the tab alert language (tab-alert-action
  red ring, tab-alert-idle yellow ring, the existing ::before rules
  extended to the header). New pure hiddenGroupAlerts() over a per-section
  `hidden` list; _syncTabGroupHeaderAlerts() patches it on BOTH render
  paths, since alerts change without a rebuild. The kept selection draws
  its own ring and is not counted.
- Every layout read rebuilt the whole tab strip, and failed reads retried
  every 5 s forever. _applyTabLayout() now rebuilds only when the
  structure key changed. The key drops the layout version (bumped on
  every session create/close and order PUT) and instead carries group
  names and the rows each collapse hides, so a version bump that moves
  nothing costs nothing and a rename still rebuilds. The load coordinator
  backs off (5, 10, 20, 40 s, capped at 60 s) and stops after 4 retries;
  the next SSE init or tab:layoutChanged tries again, a success resets.
- A malformed stored collapse value disabled collapse on that device for
  good. A parse or shape error now reads as nothing collapsed and is
  rewritten to []; ok:false stays reserved for a store that throws.
- Ctrl+Shift+{ / } still reordered across groups, where the server
  re-ranks per group, sends no session:orderChanged and leaves this
  client's sessionOrder and Alt+N targets diverged. The move is now a
  no-op unless the neighbour is in the active session's own section
  (_canSwapActiveTabWith, reading the projection's new sectionByRef, which
  also covers rows a collapse hides). Within a group the swap still works
  and the server agrees with it; the flat rail and the strip are
  unchanged.

#517 nits
- Keyboard group toggle dropping focus: already fixed by #519's
  focus-by-identity; the Enter toggle test now pins focus on the header.
- Header <button> inside role=tablist: moot, #519 made the header a
  treeitem inside role=tree.
- Byte-identity test not comparing against master: skipped in the suite
  (a test cannot read another revision's files portably). Checked by
  hand instead: the flat strip and flat rail markup of this branch before
  and after this commit are identical in all 16 cases (both orientations,
  manual and activity sort, no layout and zero groups, full and
  incremental paths).
- Doubled blank line in docs/architecture-invariants.md: removed.

#519 minors
- A tap on a tree header or unselected row dismissed the touch keyboard:
  the roving tabindex parks those at -1, so the [tabindex] arm of
  MOBILE_KEYBOARD_DISMISS_EXEMPT_SELECTOR missed them. The selector now
  lists [role="treeitem"].
- The tree key handler acted on keys pressed on a focused control inside
  a row (Enter on the overflow button re-selected and reloaded the active
  session instead of reopening its menu). It now returns unless the key
  landed on the treeitem itself.

#519 nits
- aria-posinset/setsize went stale when the activity-sorted grouped rail
  re-sorted rows on the incremental path. The position pass is extracted
  (_applyTabTreePositions) and re-run, with aria-selected and the header
  alerts, at the end of the incremental branch while the rail is a tree.
- An expanded group with no open rows was announced as an expanded parent
  owning an empty group. A group with no open rows is now a tree leaf: no
  aria-expanded, no aria-owns, its rows container presentation; Left and
  Right do nothing on it, and its chevron keys off the section's
  collapsed class instead of aria-expanded.

Tests: tab-layout-browser (malformed storage, backoff with a bounded
drain, structure key, hidden alerts, leaf groups, sectionByRef),
tab-layout-rail (header alerts on both paths, render-on-change, backoff
without rebuilds, malformed storage, Ctrl+Shift section gate, in-row
control keys, leaf header keys, posinset after an incremental re-sort,
the dismiss selector matching tree items), and three new Chromium tests
in tab-activation.browser (Enter on a focused overflow button, the touch
keyboard staying up on tree taps, the collapsed header's red ring). Every
new test fails on the pre-fix sources. Docs: architecture-invariants
owner-tab-layouts and keyboard-dismissal sections, one clause in
CLAUDE.md's dismissal rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 23:52:41 +02:00
Codeman maintainer 52267f8617 fix(terminal): drive the shipped Shift+Enter handlers and the Key tester cap in their browser tests (#520, #522 review)
- Minor: the Key tester "14 lines" test never pressed a key into the
  tester (the previous test blurred it, so the presses landed on <body>
  and the cap was never exercised). It now refocuses the field, asserts
  the focus, clears the log, checks 2 presses accumulate to 6 lines, then
  4 presses of another key cap the log at exactly 14 with the oldest 4
  lines evicted in order, and the readonly field stays empty. Verified to
  fail with the cap changed to 20.
- Nit: the split-pane invariant implied Ctrl+Enter could use the CLI's
  declared newline chord. Reworded after checking the send-key route:
  Ctrl+Enter is always a real 0x0a, Shift+Enter is the declared
  capabilities.newline chord (0x0a unless the CLI declares another), sent
  on keydown only. The same imprecision in the auto-named sessions
  paragraph is corrected too.
- Nit: docs/wiki/Settings-Reference.md now lists the Key tester row in
  the Terminal & Input table.

- Nit: test/shift-enter-keypress.browser.test.ts exercised a hand-copied
  predicate named `shipped`. It now loads the real app from a real
  WebServer and presses real keys into the handlers terminal-ui.js
  (app.terminal, recording the real _sendInputAsync send path) and
  terminal-split.js (a real SplitTerminalPane) attach, recording the
  send-key POSTs through a fetch wrapper. It asserts no \r reaches either
  send path for Shift/Ctrl+Enter, exactly one send-key per press for the
  right session, and that Enter and Alt+Enter are untouched. The old
  keydown-only gate stays as a labelled reproduction of xterm's keypress
  behaviour on a bare Terminal. Verified to fail on both panes with the
  gate narrowed back to keydown.
- Nit: the keypress trap is now written down beside the other key-gate
  rules (Command palette and shortcut registry): xterm runs the custom
  handler for keydown, keypress and keyup and drops only Ctrl/Alt/Meta
  keypresses, so a gate on a chord that can carry Shift alone must
  swallow every event type. The smart-copy keydown-only rule points at it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 23:52:41 +02:00
Codeman maintainer 36af183f97 fix(split-pane): leave the owed marker to a trailing refresh, and say what a pull's request phase holds (#524 review)
- Stale second marker above a trailing refresh's replay: xterm parses
  write() on a later tick while clear() is synchronous, so a marker stamped
  in a load's finally, just before _endBufferLoad() starts the trailing
  refresh, landed in the freshly cleared buffer above that refresh's replay.
  _stampMarkerIfOwed() now returns early while a refresh is pending; that
  refresh re-owes the marker on a closed socket and writes the one copy
  below its own replay. Pinned by marker-count assertions on the two
  existing trailing-refresh tests plus a new async-parse fake (writes
  parsed on a later tick, clear() synchronous) for back-to-back refreshes
  and a pull with a queued refresh and a close mid-pull; all four fail
  without the guard. Also checked against a real @xterm/headless 6.0.0.
- Marker withheld for up to the 45 s request budget: kept the behaviour and
  made the comment and the docs truthful. The pull's request phase holds no
  live output, but it holds the single-flight flag, so a coalesced {t:'r'}
  refresh and a close's owed marker wait for the response. Writing the
  marker at once during that phase would need a separate "awaiting
  response" state and, with a refresh pending, reopens the same
  write-vs-clear() race as above; a Codeman restart resets the in-flight
  request along with the socket, so that pull fails at once and stamps.
- Stale comments: _onSocketClosed() now says the deferral covers any load,
  _writeDisconnectedMarker() points at _stampMarkerIfOwed(), and the pull's
  finally comment describes the hand-off to a trailing refresh.
- Invariants doc: dropped "the initial load" from the loads a close can land
  in (connect() awaits it before creating the socket), reworded the
  "nested refresh stamps its own" sentence to describe the guard, and noted
  what the request phase holds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 23:52:27 +02:00
Codeman maintainer c029cea620 Merge pull request #499 from aakhter/pr/mobile-ime-preview
feat(terminal): preview IME composition text on iOS Safari

# Conflicts:
#	CLAUDE.md
2026-10-04 23:26:30 +02:00
Codeman maintainer e082d8e438 Merge pull request #515 from irisitymichaelgrundberg/feat/codex-reasoning-effort
feat(codex): start a codex session at a chosen reasoning effort
2026-10-04 23:24:39 +02:00
Codeman maintainer fa53a5751e Merge pull request #519 from aakhter/pr/grouped-rail-tree
feat(tabs): full-row activation and tree semantics for the grouped rail
2026-10-04 23:24:39 +02:00
Codeman maintainer 23d145b121 Merge pull request #517 from aakhter/pr/grouped-vertical-rail
feat(tabs): grouped vertical rail from owner tab layouts
2026-10-04 23:24:39 +02:00
Codeman maintainer ec2a036543 Merge pull request #524 from timkjr/fix/split-pane-live-queue-timing
fix(split-pane): open Pane B's live queue after the response, keep the disconnected marker last

# Conflicts:
#	docs/architecture-invariants.md
2026-10-04 23:24:08 +02:00
DevvynandClaude Sonnet 5.5 2cf37529e9 fix(terminal): address review: Key tester isolates shortcuts, Codex stays on line feed
- app.js: the shortcut dispatcher returns early for events aimed at a data-raw-keys
  field, so Ctrl+W / Ctrl+L / Escape / Alt+1 / Ctrl+K pressed in the Key tester no
  longer kill the session, clear the terminal or close Settings
- stock.ts: drop Codex's esc-enter (a line feed works); no stock CLI declares a chord.
  The esc-enter path is tested through a clis.json override
- tests: unused port (3194), Ctrl+Enter asserts no keypress, shortcut-isolation test
  (verified to fail without the guard)
- docs/comments point at capabilities.newline; set-input class, trailing whitespace

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 11:19:42 +08:00
timkjrandClaude Sonnet 5.5 df398c5c68 fix(split-pane): open Pane B's live queue after the response, keep the disconnected marker last
Follow-ups from the #506 review.

The live-frame queue opened before the fetch, freezing Pane B for the
whole round trip. It now opens beside capturedAt; the request uses the
shared terminal fetch deadline and the body read a 10 s one.

A {t:'r'} refresh queued behind a pull ran its clear() after the
disconnected marker was written and wiped it, and a close during a
refresh load wrote the marker above the replay. The marker is now an
owed flag (_markerOwed) that each load settles in its own finally.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-02 10:00:11 -05:00
Aamer Akhter 98c6c1881d feat(tabs): full-row activation and tree semantics for the grouped rail
The grouped vertical rail is now an ARIA tree with a tree keyboard model,
and tab rows are pinned as full-row activation targets whose controls keep
their own actions and stable hit targets.

Tree semantics (grouped vertical rail only):
- #sessionTabs becomes role=tree while grouped and returns to its shipped
  role=tablist and label when grouping ends. The header strip, sidebar and
  flat rail keep role=tablist / role=tab exactly as before (the flat rail's
  markup is unchanged byte for byte).
- A named group's header is a level-1 treeitem with aria-expanded that
  aria-owns its rows' role=group (rows are level 2). Ungrouped rows and the
  row a collapsed group keeps showing are level-1 items; a collapsed header
  owns nothing, and the "Ungrouped" heading is a visual divider hidden from
  assistive tech. aria-level, aria-setsize and aria-posinset are set on every
  item, and aria-selected follows the selection without a rebuild.
- Exactly one treeitem carries tabindex=0 (roving). Controls inside rows
  leave the tab order, so Shift+F10 / ContextMenu open a row's actions
  (session action menu, web tab settings).
- Up/Down walk visible items, Home/End jump, Right expands a header or enters
  it, Left collapses a header or climbs from a row to its header, Enter/Space
  select a row or toggle a header. With the activity sort on, the walk follows
  painted order within each group; the flat list keeps its whole-list walk.
- Focus survives a full re-render by identity (a row a collapse just hid hands
  focus to its header), but a render never pulls focus into the rail.
- The group header is the treeitem itself (no nested button), still toggled by
  click through the same onclick and still the lineage proxy anchor.

Full-row activation:
- Clicking a row's status dot, mode chip, name or padding already selected it
  upstream; that is now pinned in real Chromium for the strip, the flat rail
  and the grouped rail, together with every control (gear, detach, close,
  overflow, web tab gear and close) running only its own action.
- The close control now shows a pointer like its siblings instead of the
  default arrow.
- Enter/Space on a focused web tab in the flat list opens it; it used to call
  selectSession(undefined).
- The action controls are pinned to stay under the pointer when a row is
  hovered (no reflow-on-hover moving the gear out from under a click).

New Chromium suite test/tab-activation.browser.test.ts is listed in
BROWSER_TEST_GLOBS (run with npm run test:browser).
2026-10-01 22:21:45 -04:00
Aamer Akhter 7cbce5bf6c feat(tabs): grouped vertical rail from owner tab layouts
The vertical tab rail now reads the owner's tab layout (GET /api/tab-layout)
and draws its groups as collapsible sections. This is the first frontend
consumer of the tab-layout backend and it is read-only: nothing in the
browser writes the layout yet.

- tab-layout-browser.js (new, pure, loaded before app.js): projects the
  layout onto the live sessions and open web tabs, renders the grouped
  markup, stores collapse per device, and sequences loads newest-wins with
  a bounded retry on failure.
- app.js: loads the layout on init and on tab:layoutChanged, renders the
  grouped rail from the same per-row markup the flat rail uses, falls
  through to a full render whenever the grouping structure changes, and
  withholds drag-reorder in the grouped rail.
- Grouping is opt-in by construction. With no layout, a failed read, a
  layout without groups, or a horizontal strip, the rail renders exactly
  as before (byte-identical markup).
- Grouping is a render layer only: sessionOrder, Alt+N, Ctrl+Tab and the
  palette keep reading the server-projected order, and row badges keep
  their Alt+N slot.
- A collapsed group still shows the active row; lineage arcs to a hidden
  session anchor to its group header.
- webview-tabs.js: renderWebviewTab() extracted so a single web tab can be
  placed into its group with unchanged markup.
2026-10-01 14:14:30 -04:00
Michael GrundbergandClaude Opus 5.5 0ae39cdd94 test(codex): pin reasoning effort through quick-start and the multi-user clamp
Both create schemas now refuse an unknown level, and a non-granted owner's
codexConfig keeps its reasoningEffort when the clamp forces bypass off.
docs/architecture-invariants.md lists the two --config values codex now
takes from codexConfig.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 17:16:18 +02:00
Codeman maintainer 73c0bfccc4 fix(files): keep attachment markdown refs from resolving into the workspace, and render files without chat line breaks (#503 review)
- A markdown preview opened by attachment id under a bare file name
  (attachment cards, history drawer) no longer resolves relative refs
  against the workspace root: filePreviewText carries attachmentId, and
  the rebase pass turns those images into their alt text and unwraps
  those links. Absolute-path and workspace previews are unchanged.
- _renderMarkdown(text, { breaks = true } = {}): the File Viewer passes
  breaks: false, so a hard-wrapped paragraph renders as one paragraph;
  the Response Viewer keeps a <br> per newline.
- Absolute paths linkified inside a rendered document now carry the
  preview's data-session-id.
- CLAUDE.md, architecture-invariants and the Working-With-Files wiki page
  now say that only an in-workspace path clicked in the terminal keeps
  the tail viewer.
- Tests in test/file-preview-markdown.test.ts for all three fixes,
  including an end-to-end run of the shipping app.js + marked + DOMPurify.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:19:46 +02:00
Codeman maintainer 3af1ff6fae fix(split-pane): keep Pane B's disconnected marker last when the socket closes mid-pull (#506 review)
- terminal-split.js: move the socket's close into _onSocketClosed(), which
  defers the marker while a history pull holds live output (_liveQueue);
  _pullHistory() records closedBefore and its finally writes the marker
  after the queue flush when the socket closed during the pull, replayed
  or not, so it never lands above held frames or between replay chunks
- tests: drive the real close path for a close mid-fetch ending in a skip,
  a downgrade or a failed fetch, a close during the chunked replay, and a
  close with no pull running; pin the onclose wiring in the static guard;
  describe the mid-fetch case on its own
- CLAUDE.md: turn the plain-text split-pane pointer into a link
- architecture-invariants.md: describe the deferred marker

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:18:57 +02:00
Codeman maintainer 988f111cd0 fix(ui): keep a focus the row menu moved when closing the Session Manager (#509 review)
- _restoreOverlayFocus(key, modal) now leaves focus alone when something
  outside the overlay already holds it (not <body>, not inside the modal).
  The Session Manager's "Switch to session" and "Open folder" call
  selectSession() before closeSessionManager(), and the restore was pulling
  focus back from the terminal to the header button. Both close methods pass
  their modal; a regression test drives that order.
- Test harness: focusHarness() routes getElementById through a local binding
  instead of leaking globalThis.__els, and its modal stubs report their own
  search box as contained, as the real DOM does.
- CLAUDE.md and docs/architecture-invariants.md: record that the global
  Escape handler calls every close method on every Escape (capture phase),
  so a close method with side effects must return early when not open.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 11:18:56 +02:00