mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +02:00
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:
@@ -6,12 +6,14 @@
|
||||
* skip, per-session change-detection (dedup), rebroadcast on a displayed change,
|
||||
* NO rebroadcast on context-only drift, null-tolerance of Claude's undocumented
|
||||
* fields (the .nullish() schema — the project's recurring .optional()/null trap),
|
||||
* and 400 on a malformed body.
|
||||
* and 400 on a malformed body. Also the session's model (`displayModel`) the route
|
||||
* records off the same payload.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { createRouteTestHarness, type RouteTestHarness } from './_route-test-utils.js';
|
||||
import { registerStatusTelemetryRoutes } from '../../src/web/routes/status-telemetry-routes.js';
|
||||
import { SessionStatusTelemetry } from '../../src/web/sse-events.js';
|
||||
import { Session } from '../../src/session.js';
|
||||
|
||||
const SID = 'test-session-1'; // default id created by createMockRouteContext
|
||||
|
||||
@@ -100,3 +102,56 @@ describe('POST /api/status-telemetry', () => {
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/status-telemetry: the session model (displayModel)', () => {
|
||||
let h: RouteTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
h = await createRouteTestHarness(registerStatusTelemetryRoutes, { sessionId: SID });
|
||||
});
|
||||
|
||||
const post = (body: unknown) => h.app.inject({ method: 'POST', url: '/api/status-telemetry', payload: body });
|
||||
|
||||
/** A real claude session in the route's map, so displayModel is the shipped resolver's. */
|
||||
function realSession(mode: 'claude' | 'codex', id: string): Session {
|
||||
const session = new Session({ id, workingDir: '/tmp', mode } as ConstructorParameters<typeof Session>[0]);
|
||||
(h.ctx.sessions as unknown as Map<string, unknown>).set(id, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
it('records the model the statusline names, from the first render (no rate limits yet)', async () => {
|
||||
const session = realSession('claude', 'claude-1');
|
||||
const changed = vi.fn();
|
||||
session.on('displayModelChanged', changed);
|
||||
const res = await post({ sessionId: 'claude-1', data: { model: { display_name: 'Haiku 4.5' } } });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(session.toState().displayModel).toEqual({ model: 'Haiku 4.5', source: 'statusline' });
|
||||
expect(changed).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('follows a model switch, and says nothing for an unchanged one', async () => {
|
||||
const session = realSession('claude', 'claude-2');
|
||||
const changed = vi.fn();
|
||||
session.on('displayModelChanged', changed);
|
||||
await post({ sessionId: 'claude-2', data: REAL });
|
||||
await post({ sessionId: 'claude-2', data: REAL });
|
||||
expect(changed).toHaveBeenCalledTimes(1);
|
||||
await post({ sessionId: 'claude-2', data: { ...REAL, model: { display_name: 'Sonnet 4.6' } } });
|
||||
expect(session.toState().displayModel).toEqual({ model: 'Sonnet 4.6', source: 'statusline' });
|
||||
expect(changed).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('keeps a model name with markup as text, and a null name changes nothing', async () => {
|
||||
const session = realSession('claude', 'claude-3');
|
||||
await post({ sessionId: 'claude-3', data: { model: { display_name: '<b>Opus</b>' } } });
|
||||
expect(session.toState().displayModel?.model).toBe('<b>Opus</b>');
|
||||
await post({ sessionId: 'claude-3', data: { model: { display_name: null } } });
|
||||
expect(session.toState().displayModel?.model).toBe('<b>Opus</b>');
|
||||
});
|
||||
|
||||
it('takes no model from a CLI that has no statusline exporter', async () => {
|
||||
const session = realSession('codex', 'codex-1');
|
||||
await post({ sessionId: 'codex-1', data: { model: { display_name: 'Opus 4.8' } } });
|
||||
expect(session.toState().displayModel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user