feat(sessions): a dsh session shows its route config's model while its screen names none

displayModel gains a `config` source, ranked below any report from the running
CLI and above the launch model: custom endpoint, then statusline or screen,
then config, then launch, then nothing. The screen still wins whenever it
names a model, since that is what the running TUI uses.

- Registry data: capabilities.modelDetect gains `configResolver`, a NAMED
  reader (src/model-config-resolvers.ts), like a launcher profile; dsh names
  'deepseek-route' (the reader from the previous commit). `screenLine` becomes
  optional; the schema refuses a modelDetect naming nothing, an unknown
  reader, or screenLines without a screenLine.
- Session: the reader runs from _withPaneLifecycle's finally, so at every pane
  start, attach and relaunch, with the session's own launch config
  (legacyConfigForMode) and env (its clamped overrides, then the server's), so
  a per-session DSH_HOME is the home read. Async; a read that lands after a
  newer one or after the session stopped is dropped; a remote or docker
  session reads nothing locally. A change emits displayModelChanged
  (broadcast and persist). Not restored after a restart: the next attach
  reads it again, and a restored screen value outranks it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 19:41:56 +02:00
parent 284f86b740
commit 661fe3dc13
9 changed files with 402 additions and 22 deletions
+14 -2
View File
@@ -15,7 +15,7 @@
import { z } from 'zod';
import { compileVersionRegex, countCaptureGroups, TOKEN_PATTERNS } from './patterns.js';
import { isKnownLauncherProfile, isKnownSetenvProfile } from './profiles.js';
import type { McpConfigFormat } from './types.js';
import type { McpConfigFormat, ModelConfigResolverName } from './types.js';
/** A bare CLI id: lowercase, starts with a letter, at most 24 chars. Also used as a CSS/URL token. */
const cliId = z
@@ -382,12 +382,24 @@ const capabilitiesSchema = z
.refine(
(src) => compileVersionRegex(src) !== null && countCaptureGroups(src) === 1,
'screenLine must be a regex compileVersionRegex() accepts (at most 200 characters, no nested quantifiers) with exactly one capture group'
),
)
.optional(),
// Bounded hard, like watchingLines: every row it adds is one more row the agent
// itself may be able to write.
screenLines: z.number().int().min(1).max(4).optional(),
// A NAMED reader (src/model-config-resolvers.ts), never code in config.
configResolver: z.enum(['deepseek-route'] as const satisfies readonly ModelConfigResolverName[]).optional(),
})
.strict()
// Typos rather than configurations, refused at LOAD time like watchingLines.
.refine(
(v) => v.screenLine !== undefined || v.configResolver !== undefined,
'modelDetect declares nothing to read'
)
.refine(
(v) => v.screenLines === undefined || v.screenLine !== undefined,
'screenLines has nothing to bound without a screenLine'
)
.optional(),
privilegedParams: z
.array(
+3
View File
@@ -1243,6 +1243,9 @@ const DEEPSEEK: CliEntry = {
modelDetect: {
screenLine: String.raw`╰─+╯\n ?([A-Za-z0-9][\w.:/@+-]{0,79})(?= · |\n|$)`,
screenLines: 3,
// With the status bar's model field off (or before it paints), the route the
// session's profile pins, read the way dsh-TUI resolves it: src/deepseek-route-config.ts.
configResolver: 'deepseek-route',
},
// Only-if-sent, like codex/antigravity/grok: an ABSENT permissionMode means the
// launcher's own default, `workspace-write`, which already asks. Clamping to
+11 -1
View File
@@ -93,6 +93,9 @@ export interface CliVariant {
/** The newline chord a CLI's composer reads as "insert a line break" (see `CliCapabilities.newline`). */
export type NewlineSequence = 'line-feed' | 'esc-enter';
/** The config readers `capabilities.modelDetect.configResolver` may name (src/model-config-resolvers.ts). */
export type ModelConfigResolverName = 'deepseek-route';
/** The MCP config dialects `src/mcp-sync.ts` has an adapter for. */
export type McpConfigFormat = 'claude-json' | 'gemini-json' | 'codex-toml' | 'opencode-json' | 'antigravity-json';
@@ -477,8 +480,15 @@ export interface CliCapabilities {
* own footer format), never on a shape the agent could print in its transcript. Measured
* on a live pane per CLI; absent means the CLI's screen is never read for a model and
* the session shows its launch model, if any.
*
* `configResolver` names a reader (src/model-config-resolvers.ts) that resolves the
* model the CLI's own config pins, the way that CLI resolves it for the session, for
* while the screen names none (its status line switched off, or not drawn yet). Read
* once per pane start, attach or relaunch, bounded and read-only; the screen still
* wins whenever it names a model. A NAMED reader, like a launcher profile, so the
* per-CLI behaviour stays data here and code in one module.
*/
modelDetect?: { screenLine: string; screenLines?: number };
modelDetect?: { screenLine?: string; screenLines?: number; configResolver?: ModelConfigResolverName };
/**
* Params a non-granted multi-user owner may not set freely, and what they are forced to.
* Data-driven so a CUSTOM CLI's bypass flag is clampable exactly like codex's.