control board: show the model each seat is running (#1503)
Jason asked for the running model (sonnet, opus, gpt-6-astra) on the board. readSession keeps model and provider from the log's latest model_change entry or assistant turn, whichever is later, so a /model switch shows on the next scan; scanAgent exposes both; the page shows the model under the agent name with the provider in the hover and a Model detail row. Blank when the log names none. Live check on a scratch board against the real data root: all 42 rows carried a model. Board 91/91. Sonnet review caught a double-escaped hover title; fixed. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -60,9 +60,11 @@ function writeFile(path, content) {
|
||||
function sessionLine({ id, timestamp, cwd }) {
|
||||
return JSON.stringify({ type: "session", id, timestamp, cwd });
|
||||
}
|
||||
function messageLine({ timestamp, role, stopReason, texts, blocks }) {
|
||||
function messageLine({ timestamp, role, stopReason, texts, blocks, model, provider }) {
|
||||
const message = { role };
|
||||
if (stopReason !== undefined) message.stopReason = stopReason;
|
||||
if (model !== undefined) message.model = model;
|
||||
if (provider !== undefined) message.provider = provider;
|
||||
if (texts) message.content = texts.map((text) => ({ type: "text", text }));
|
||||
if (blocks) message.content = (message.content || []).concat(blocks);
|
||||
return JSON.stringify({ type: "message", timestamp, message });
|
||||
@@ -190,6 +192,45 @@ test("readSession: extracts fields, collapses/truncates text, counts a truncated
|
||||
assert.equal(result.skippedLines, 1);
|
||||
});
|
||||
|
||||
test("readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them", () => {
|
||||
const root = makeRoot();
|
||||
const modelChange = (timestamp, provider, modelId) => JSON.stringify({ type: "model_change", id: "m", parentId: null, timestamp, provider, modelId });
|
||||
const switchedDir = join(root, "switched");
|
||||
writeSessionFile(switchedDir, "model.jsonl", [
|
||||
sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }),
|
||||
modelChange("2026-09-12T16:06:52.988Z", "zai", "glm-5.3-flash"),
|
||||
messageLine({ timestamp: "2026-09-12T16:07:00Z", role: "user", texts: ["hi"] }),
|
||||
messageLine({ timestamp: "2026-09-12T16:07:05Z", role: "assistant", stopReason: "stop", texts: ["hello"], model: "glm-5.3-flash", provider: "zai" }),
|
||||
modelChange("2026-09-12T16:08:52.233Z", "openai-codex", "gpt-6-astra"),
|
||||
]);
|
||||
const switched = readSession(join(switchedDir, "model.jsonl"));
|
||||
assert.equal(switched.model, "gpt-6-astra", "a /model switch after the last answer wins");
|
||||
assert.equal(switched.provider, "openai-codex");
|
||||
|
||||
const answered = writeSessionFile(root, "answered.jsonl", [
|
||||
sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }),
|
||||
modelChange("2026-09-12T16:06:52.988Z", "zai", "glm-5.3-flash"),
|
||||
messageLine({ timestamp: "2026-09-12T16:07:05Z", role: "assistant", stopReason: "stop", texts: ["hello"], model: "claude-sonnet-5", provider: "anthropic" }),
|
||||
messageLine({ timestamp: "2026-09-12T16:07:06Z", role: "user", texts: ["and?"] }),
|
||||
]);
|
||||
assert.deepEqual([readSession(answered).model, readSession(answered).provider], ["claude-sonnet-5", "anthropic"], "the assistant turn names the model that produced it");
|
||||
|
||||
const noneDir = join(root, "none");
|
||||
writeSessionFile(noneDir, "none.jsonl", [
|
||||
sessionLine({ id: "s", timestamp: "2026-09-12T16:06:52.940Z", cwd: "/x" }),
|
||||
messageLine({ timestamp: "2026-09-12T16:07:00Z", role: "user", texts: ["hi"] }),
|
||||
]);
|
||||
assert.equal(readSession(join(noneDir, "none.jsonl")).model, null);
|
||||
assert.equal(readSession(join(noneDir, "none.jsonl")).provider, null);
|
||||
|
||||
const rec = scanAgent({ agent: "a", project: "p", sessionsDir: switchedDir, tmux: {} }, { isAlive: () => true, now: GATE_NOW });
|
||||
assert.equal(rec.model, "gpt-6-astra", "scanAgent carries model through from the newest log");
|
||||
assert.equal(rec.provider, "openai-codex");
|
||||
const bare = scanAgent({ agent: "a", project: "p", sessionsDir: noneDir, tmux: {} }, { isAlive: () => true, now: GATE_NOW });
|
||||
assert.equal(bare.model, null);
|
||||
assert.equal(bare.provider, null);
|
||||
});
|
||||
|
||||
test("readSession: lastError carries the assistant errorMessage only when the last assistant turn errored", () => {
|
||||
const root = makeRoot();
|
||||
const errored = JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user