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:
2026-09-12 11:13:32 -05:00
co-authored by Claude Fable 5.1
parent d0fb5b0f20
commit 0bed9ba3db
8 changed files with 99 additions and 4 deletions
+17 -1
View File
@@ -71,6 +71,7 @@ export function readSession(file) {
const lines = readFileSync(file, "utf8").split("\n");
let sessionId = null, cwd = null, lastTimestamp = null, lastMessage = null, lastAssistantText = null, lastError = null;
let firstUserText = null;
let model = null, provider = null;
let skippedLines = 0;
for (const line of lines) {
if (!line.trim()) continue;
@@ -85,8 +86,18 @@ export function readSession(file) {
if (entry.type === "session") {
sessionId = entry.id ?? sessionId;
cwd = entry.cwd ?? cwd;
} else if (entry.type === "model_change") {
// pi logs a model switch (/model, or the launch default) as its own
// entry: { provider, modelId }. The latest one is the model in use.
if (typeof entry.modelId === "string" && entry.modelId) model = entry.modelId;
if (typeof entry.provider === "string" && entry.provider) provider = entry.provider;
} else if (entry.type === "message" && entry.message) {
lastMessage = entry.message;
if (entry.message.role === "assistant") {
// Each assistant turn also names the model that produced it.
if (typeof entry.message.model === "string" && entry.message.model) model = entry.message.model;
if (typeof entry.message.provider === "string" && entry.message.provider) provider = entry.message.provider;
}
if (entry.message.role === "user" && firstUserText === null) {
const text = userText(entry.message.content);
if (text.trim()) firstUserText = collapse(text);
@@ -98,7 +109,7 @@ export function readSession(file) {
}
}
}
return { file, sessionId, cwd, lastTimestamp, lastMessage, lastAssistantText, lastError, firstUserText, skippedLines };
return { file, sessionId, cwd, lastTimestamp, lastMessage, lastAssistantText, lastError, firstUserText, model, provider, skippedLines };
}
// A user message's text: pi writes either a plain string or a list of blocks.
@@ -341,6 +352,11 @@ export function scanAgent(spec, { isAlive = tmuxInspect, now = () => new Date(),
sessionFile: file,
sessionId: session?.sessionId ?? null,
cwd,
// The model the seat is running, from the log's latest model_change entry
// or assistant turn, whichever came last. null when the log has neither
// (a seat with no log, or one that has not answered yet).
model: session?.model ?? null,
provider: session?.provider ?? null,
task,
taskSource,
workspace,