feat(sessions): publish the model each session runs (displayModel)

A session header can only name the model a session runs if the server knows
it, so SessionState gains `displayModel: { model, source }`, resolved in a pure
module (src/session-display-model.ts), strongest first:

- custom-endpoint: a Custom Model Endpoint Profile's modelId answers the
  session, whatever alias the CLI prints;
- statusline / screen: the newest report from the running CLI itself.
  Claude's statusLine exporter already posts model.display_name on every
  render; the status-telemetry route now records it (only for a CLI with
  capabilities.statusLineTelemetry). A CLI whose registry entry declares the
  new capabilities.modelDetect has its footer read off the pane capture the
  idle/working probe already takes (no extra tmux call), so an in-session
  /model switch is followed at the next transition;
- launch: the model the session was launched with (claude's --model or the
  app-wide default, another CLI's <cli>Config.model), read where the registry
  says the model param lives;
- nothing known: no field, never a placeholder.

modelDetect is registry data, measured on live panes: dsh-TUI's status line
on the row under its composer (qwen3.8-27b on the owner's route) and codex's
`<model> <effort> ·` footer on its last row. Both anchor on chrome only that
CLI draws, over the last rows of the screen only; a transcript line shaped like
the footer is never taken (fixture tests). The pattern goes through
compileVersionRegex() with exactly one capture group, checked at load time.

An unreadable or covered footer keeps the last model (unlike the watching
label: a model does not stop running when something covers its row). Model
text is untrusted: escape sequences and control characters are stripped and it
is capped at 64 characters. A change emits displayModelChanged, broadcast
(session:updated) and persisted; a restart restores a CLI-reported model until
the next report.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 13:31:59 +02:00
parent d044406f8f
commit 8392854619
14 changed files with 794 additions and 5 deletions
+25
View File
@@ -641,6 +641,22 @@ export interface CustomModelSelection {
label?: string;
}
/**
* Where a session's {@link DisplayModel} came from (src/session-display-model.ts):
* - `custom-endpoint`: the Custom Model Endpoint Profile's model, which wins.
* - `statusline`: the CLI reported it (claude's statusLine exporter), follows a switch.
* - `screen`: read off the CLI's own footer (`capabilities.modelDetect`), follows a switch.
* - `launch`: what the session was launched with; nothing has reported since.
*/
export type DisplayModelSource = 'custom-endpoint' | 'statusline' | 'screen' | 'launch';
/** The model a session runs as far as the server knows, for a session header. */
export interface DisplayModel {
/** Display text: sanitized (no control characters) and at most 64 characters. */
model: string;
source: DisplayModelSource;
}
/**
* The full custom-model selection a session keeps: the public selection plus the
* bookkeeping `Session.setCustomModel()` needs to UNDO it later without guessing what
@@ -847,6 +863,15 @@ export interface SessionState {
* written) is {@link CustomModelBookkeeping}, persisted disk-only like `__envOverrides`.
*/
customModel?: CustomModelSelection;
/**
* The model this session runs, as far as the server knows it, and where that came from
* (src/session-display-model.ts): the custom endpoint's model, else the newest report
* from the CLI itself (statusline or its own footer), else the launch model. Absent when
* none is known; a session header then shows the harness alone. Untrusted display text
* (pane-derived for `screen`): render it as text. Persisted, and a `statusline`/`screen`
* value is restored after a restart until the next report replaces it.
*/
displayModel?: DisplayModel;
/** Sanitized per-session attachment history. */
attachmentHistory?: SessionAttachmentHistoryItem[];
/**