mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
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>
This commit is contained in:
@@ -484,6 +484,30 @@ also pure decoration: it confers no permission, and a child is unaffected by its
|
||||
parent exiting. It appears on session state as `parentSessionId` (absent when
|
||||
unresolved) and survives a server restart.
|
||||
|
||||
## Session model (`displayModel`)
|
||||
|
||||
Session state (`GET /api/v1/sessions`, the `session:updated` event) carries the model a
|
||||
session runs as far as the server knows it, for the web UI's session headers:
|
||||
|
||||
```json
|
||||
"displayModel": { "model": "qwen3.8-27b", "source": "screen" }
|
||||
```
|
||||
|
||||
`source` is where it came from, strongest first:
|
||||
|
||||
| `source` | Meaning |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `custom-endpoint` | The session is pointed at a Custom Model Endpoint Profile; its `modelId` answers, whatever the CLI prints. |
|
||||
| `statusline` | Claude's statusLine exporter reported it (`model.display_name`); follows an in-session `/model`. |
|
||||
| `screen` | Read off the CLI's own footer (`capabilities.modelDetect`, today dsh and codex); follows a switch. |
|
||||
| `launch` | What the session was launched with (`--model`, the app-wide default, `<cli>Config.model`); nothing has reported since. |
|
||||
|
||||
Between `statusline` and `screen` the newest report wins. The field is absent when no
|
||||
model is known (a shell, a CLI that reports none and was launched without one). `model`
|
||||
is display text from a pane or a CLI report: control characters are stripped and it is at
|
||||
most 64 characters, but treat it as untrusted text. A `statusline` or `screen` value is
|
||||
persisted and restored after a server restart until the next report replaces it.
|
||||
|
||||
## Approvals Inbox
|
||||
|
||||
Cross-session queue of prompts waiting on a human (permission dialogs,
|
||||
|
||||
@@ -815,6 +815,8 @@ Further detail: with many sessions the horizontal strip stops being scannable, w
|
||||
|
||||
⚠️ **Chrome.** The header is a fixed 28px sibling of the body (its buttons are 26px targets with 16 to 19px glyphs, the app header's own icon-button size), refreshed in place on every tab render (`_renderSessionTabsImmediate` wrapper), never by rewriting the tile (that would take its xterm along) and never growing (#464: the body holds the xterm). Header buttons stop pointerdown, so acting on an unfocused tile does not focus it. × removes the tile only; killing stays behind the session menu's Close session. The `needs` pulse animates `box-shadow` only. Dividers are their own 6px grid tracks (`gap: 0`), tiles placed explicitly; a drag uses pointer capture, reflows the affected tiles locally per animation frame and sends ONE `fit()` (one PTY resize) per affected tile at pointer-up; closing the grid or removing a tile mid-drag tears it down. A tab dragged onto a tile replaces it (or swaps two tiles); tiles and empty slots handle `dragover`/`drop` in the CAPTURE phase and stop it, because the drag carries the session id as text and xterm's helper textarea would type it into the PTY.
|
||||
|
||||
⚠️ **Harness and model.** Every session header (a tile's, both split panes') names the session's CLI with PR #532's `run-mode-dot <cliId>` logo slot and the model it runs as text, painted by ONE function, `_paintSessionHarness` (terminal-split.js), from the pure `describeSessionHarness` (constants.js). The id is data (the logo's class, the CLI catalog's label), never a branch: `test/frontend-cli-no-id-branching.test.ts` scans constants.js, terminal-split.js and tile-grid.js. The model is `SessionState.displayModel` (src/session-display-model.ts): the custom endpoint's `modelId`, else the newest report from the CLI itself (claude's statusline `model.display_name` via `POST /api/status-telemetry`, or the CLI's own footer read with `capabilities.modelDetect` off the capture the idle/working probe already takes), else the launch model, else nothing: the logo alone, never a placeholder. ⚠️ The model is untrusted, pane-derived text: sanitized and capped at 64 characters on the server, rendered with `textContent` inside a `data-i18n-skip` span; the tooltip (on the logo and on the model's box, which the translator may reach) says where a model the CLI did not report came from ("set at launch", "custom endpoint"), and the logo's accessible name carries harness and model so the model box is `aria-hidden`. ⚠️ The painter diffs against what it last wrote (kept on the header's parts), never the DOM, so an unchanged session writes nothing on a tab render. ⚠️ On a narrow header the model gives way first, then the name: the name does not shrink at all and is capped at its box (`max-width: 100%`), because any shrink factor takes a subpixel from a name that fits and ellipsizes it. ⚠️ Pane A's strip exists only while the split is open, as the first child of `.terminal-wrap`; it takes height from the main terminal, so opening and closing fit through `sendResize` / `syncTerminalGeometry` (#464), never a bare `fitAddon.fit()`, and the partial-history banner moves below it.
|
||||
|
||||
⚠️ **Attach.** A tile whose session has no PTY (`pid === null`) or whose socket closed because it exited (4009) shows an Attach overlay (absolute, the body keeps its size): `POST /interactive` (or `/shell`) with NO body, one in flight per session, a tripped PTY-exit breaker only through the same confirm as the single view, then the tile is remounted (a stopped socket cannot reconnect). The routes report a refusal in the ENVELOPE of a 200, so the response body is read, not `res.ok`. An agent that exited in a live pane (`paneExit`) cannot be started again in place (both routes refuse while the pane's tmux client runs): its tile shows the exit and points at Close session.
|
||||
|
||||
⚠️ **Persistence and joining.** `codeman:tile-grid` holds `{ v: 1, open, ids, focused, zoomed, colFr, rowFr }`, ids only, written as the grid changes; closing keeps it as `open: false` (the Tiles toggle, the picker and Ctrl/Cmd+click bring it back), the last tile leaving forgets it, a solo window never reads or writes it, an automatic zoom is not stored. Sessions created by THIS tab's Run join the open grid (`_joinTileGridFromRun`, called from session-ui.js `_ensureCreatedSessionVisible`; a wrapper from tile-grid.js would be overwritten by session-ui.js's later `Object.assign`); sessions created elsewhere arrive only by `session:created` and never join. ⚠️ A tile that joins that way connects BEFORE Run starts its pane: its resize reaches a session with no PTY, which `Session.resize()` only records (`_lastDesktopDims`) while the spawn uses a fixed 120x40, and Run's own resize step measures the parked main terminal (`display: none`, so `proposeDimensions()` is NaN and the step is skipped). Measured live: a 97x17 tile over a 120x40 pane. So `_renderTileChrome` remembers each tile's last-seen pid and calls `tile.paneStarted()` when it appears or changes (keyed on the sessions map, so a `handleInit` after an SSE drop counts too); `paneStarted()` forgets the sent size and sends it, and a hidden tile (a zoomed neighbour) sends nothing but keeps the size forgotten, so its next `fit()` sends it. A plain `fit({ force: true })` would lose that case. Tests: `test/tile-grid-*.test.ts` over the shared vm harness `test/mocks/tile-grid-vm.ts`.
|
||||
|
||||
@@ -49,6 +49,8 @@ interface CliEntry {
|
||||
// .workDetect?: { promptGlyph, workingLine, watchingLine?, watchingLines?, awaitingLine? }
|
||||
// — how this CLI's pane shows work, work it started in the background, and a turn
|
||||
// that ended waiting for workers it will resume from
|
||||
// .modelDetect?: { screenLine, screenLines? }
|
||||
// (where this CLI's own chrome names the model it runs: SessionState.displayModel)
|
||||
overlays: CliOverlays; // remote-SSH / Docker pane commands, credential store
|
||||
}
|
||||
```
|
||||
@@ -57,10 +59,12 @@ interface CliEntry {
|
||||
|
||||
### Regexes that come from config
|
||||
|
||||
Four capability fields carry a regular expression an override file can set: `discovery.version.regex`, `capabilities.workDetect.workingLine`, `capabilities.workDetect.watchingLine` and `capabilities.workDetect.awaitingLine`. All four go through `compileVersionRegex()`, which caps the source at 200 characters, refuses the nested-quantifier shapes that cause catastrophic backtracking, and returns `null` rather than throwing so every caller degrades instead of crashing.
|
||||
Five capability fields carry a regular expression an override file can set: `discovery.version.regex`, `capabilities.workDetect.workingLine`, `capabilities.workDetect.watchingLine`, `capabilities.workDetect.awaitingLine` and `capabilities.modelDetect.screenLine`. All five go through `compileVersionRegex()`, which caps the source at 200 characters, refuses the nested-quantifier shapes that cause catastrophic backtracking, and returns `null` rather than throwing so every caller degrades instead of crashing.
|
||||
|
||||
`workingLine` is the one that matters most, because it is compiled once per session and then run against every accumulated PTY chunk and every pane capture. A nested quantifier there is a ReDoS against the event loop for the whole server, not just that session. The guard therefore runs in two places, and neither is redundant: `schema.ts` rejects the entry at LOAD time so a bad pattern never reaches a session, and `_workingLinePattern()` in `session.ts` compiles through the same helper so the runtime cannot end up with a pattern the schema would have refused.
|
||||
|
||||
`modelDetect.screenLine` names the model a session runs, for the tile grid's and the split pane's headers (`SessionState.displayModel`). It must have exactly ONE capture group, the model, which `schema.ts` checks at LOAD time, and it runs over the last `screenLines` (1 to 4, default 1) non-blank rows of the capture the idle/working probe already takes, joined with newlines so a pattern can anchor on the row above. Like `watchingLine`, the rows are pane text the agent writes most of, so a pattern must anchor on chrome only that CLI draws. The two stock ones, measured on live panes: dsh-TUI's status line on the row under its composer's rounded border (`╰─+╯\n ?(<model>)`, three rows), and codex's ` <model> <effort> · ` footer on its last row. A screen that does not match keeps the last model the session reported; a CLI without the field shows its launch model, if any. Claude needs none: its statusLine exporter reports `model.display_name` on every render.
|
||||
|
||||
`watchingLine` reads a different row of the same screen. A CLI draws it while work the agent
|
||||
itself started is still running — Claude prints `⏵⏵ bypass permissions on · 1 monitor · ← for
|
||||
agents` while a monitor, a backgrounded shell or a cloud session is live. Codeman turns that
|
||||
|
||||
@@ -48,6 +48,13 @@ or settled a question the spec left open. The invariants as built are in
|
||||
the one cap every limit reads; the layout table keeps 7 to 9 (`TILE_LAYOUT_MAX`), unreachable,
|
||||
so going back to nine is that one line. A stored grid with more ids comes back as its first
|
||||
six. Where this spec says nine, read six.
|
||||
- **Each header names the harness and the model** (owner request): the tile header is
|
||||
`● [logo] name · model ……… ⋯ ⤢ ×`. The logo is PR #532's `run-mode-dot <cliId>` slot
|
||||
(the id is data), the model the session's `displayModel` (custom endpoint, else what the
|
||||
CLI itself reports: claude's statusline, a footer read with `capabilities.modelDetect`
|
||||
for dsh and codex; else the launch model; else nothing, the logo alone). Both split
|
||||
panes carry the same strip: Pane B's header, and Pane A's while the split is open. Pane
|
||||
B's close is a tile button (26px).
|
||||
- **No + in the tile header** (owner decision 9): the header is `● name ……… ⋯ ⤢ ×`. The
|
||||
+ menu and its "New session in this case" are gone; tiles are added from the Tiles
|
||||
button (and its right-click picker), Ctrl/Cmd+click on a tab, a dragged tab, "Open group
|
||||
@@ -206,7 +213,8 @@ animation frame, and sends one resize per affected tile at pointer-up.
|
||||
|
||||
### Tile header
|
||||
|
||||
`● name ……… ⋯ ⤢ + ×` (as built: `● name ……… ⋯ ⤢ ×`, owner decision 9)
|
||||
`● name ……… ⋯ ⤢ + ×` (as built: `● [logo] name · model ……… ⋯ ⤢ ×`, owner decision 9 and
|
||||
the harness/model request; see "As built")
|
||||
|
||||
- **●** status dot from the existing six-state classifier
|
||||
(`app._sidebarRichRow(id, session)`, built on `_mobileOverviewState`):
|
||||
|
||||
@@ -39,12 +39,14 @@ window is too small for six; the picker says which limit applies.
|
||||
|
||||
## A tile
|
||||
|
||||
Each tile has a small header: `● name ......... ⋯ ⤢ ×`
|
||||
Each tile has a small header: `● [logo] name · model ......... ⋯ ⤢ ×`
|
||||
|
||||
| Part | What it does |
|
||||
| ------ | ------------------------------------------------------------------------------------------------ |
|
||||
| `●` | The session's state: working, idle, waiting on you, needs you (red, and the tile's border pulses), error, ended. Hover the header for how long. |
|
||||
| logo | Which agent runs in the tile (Claude Code, Codex, DeepSeek, Shell, ...). Hover it for the agent and the model by name. |
|
||||
| name | Double-click to rename the session. |
|
||||
| model | The model the session runs, when Codeman knows it: what the agent itself reports (it follows a `/model` switch), else the model it was started with. Nothing when unknown. |
|
||||
| `⋯` | The session menu: options, open in a new window, close the session. |
|
||||
| `⤢` | Zoom: the tile fills the grid; press it again (or `Alt+Shift+Enter`) to get the grid back. |
|
||||
| `×` | Remove the tile. The session keeps running; close it from `⋯` if you want it gone. |
|
||||
@@ -85,6 +87,8 @@ same. Narrowing the window below the desktop width also returns to the single vi
|
||||
The grid is saved on this device and comes back when you reload the page, with its focus,
|
||||
zoom and column widths. A session that was closed in the meantime is simply left out.
|
||||
|
||||
Split shows the same logo, name and model above both of its panes.
|
||||
|
||||
The grid and Split are never open together: opening the grid turns an open split into two
|
||||
tiles, and Split is unavailable while the grid is open.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user