A model chip beside every agent name

Task 12 recorded which model each launch rode, but it surfaced only at
the tail of the session-detail metadata line, where nobody's eye lands.
With work and review models diverging per intent, "which brain did this"
is a routine review question, so the answer now sits beside the name that
did it.

One component, modelChip(), rendered wherever a name identifies a run:
the sessions list row, the session-detail header, the Focus header and
the working card's agent line. It borrows the session-id hash's register
— small, mono, dim — and takes no colour, because a model is not a state.
The vendor prefix a name repeats is dropped for the chip (claude-opus-4-8
→ opus-4-8, anthropic/x → x) with the whole string on hover; an
unfamiliar name is shown as recorded rather than guessed at.

A launch that never knew its model wears no chip at all — an inherited
default, or a session replayed from disk after a restart. The two
metadata lines that used to carry the model keep only what the chip
cannot say: "model inherited", and only when it genuinely is.

Tests lift the two functions out of the page and run them in node (skipped
where node is absent) for the shortening, escaping and silence; placement
and register stay source-level invariants, as board.html has no frontend
test runner.
This commit is contained in:
istos
2026-07-30 12:04:36 +02:00
parent 39ab96d81b
commit 053e1d7d71
3 changed files with 220 additions and 7 deletions
+41 -7
View File
@@ -101,6 +101,17 @@
.livechip[hidden]{display:none}
.dot{width:7px;height:7px;border-radius:99px;background:var(--idle);flex:none}
.dot.live{background:var(--accent);animation:breathe 2.4s ease-in-out infinite}
/* the model chip: which brain did this, beside the name that did it.
Machine-produced, so mono; a model is not a state, so it borrows no
colour — it lives in the session-id hash's register wherever names
appear, tracking each site's hash size. cursor:help because the
unshortened string is on the title. */
.mchip{font-family:var(--mono);font-size:10.5px;font-weight:400;color:var(--dim);white-space:nowrap;cursor:help}
/* the card's row is the tight one: there the chip gives way like the
mono fact beside it rather than pushing the row wider */
.card .whorow .mchip{font-size:11px;overflow:hidden;text-overflow:ellipsis}
.f-head .s-title .mchip{font-size:12px}
.refline .mchip{font-size:11.5px}
/* ── kanban ── */
main{flex:1;display:flex;min-height:0}
@@ -697,6 +708,24 @@ function isLiveSession(m) {
}
function allTasks() { return (S.state?.board.stages || []).flatMap(s => s.tasks); }
/* Which model a run rode, shortened for the chip: the provider path an
opencode-style id carries (anthropic/model-x) and the vendor word a
claude one repeats (claude-opus-4-8) are both redundant beside a board
that already knows its vendor. Nothing else is touched — an unknown
name is shown as recorded rather than guessed at. */
function shortModel(model) {
return String(model).split('/').pop().replace(/^claude-/, '');
}
/* One chip, every place a name identifies a run. A launch that never knew
its model (it inherited the vendor default, or the session was replayed
from disk after a restart) gets nothing at all: no chip is the honest
answer, a placeholder would read as a model named "unknown". */
function modelChip(agent) {
if (!agent || !agent.model) return '';
return `<span class="mchip" title="${esc(agent.model)}">${esc(shortModel(agent.model))}</span>`;
}
function connectStream() {
const es = new EventSource('/api/stream');
let hadError = false;
@@ -1060,6 +1089,7 @@ function cardFor(task) {
`<div class="toprow">${top.join('')}</div>` +
`<div class="title">${esc(task.title)}</div>` +
`<div class="whorow"><span class="initial">${esc(initial)}</span><span class="who">${esc(who)}</span>` +
modelChip(agent) +
`<span class="meta">${esc(meta)}${extras.length ? ' · ' + extras.join(' · ') : ''}</span></div>` +
chipRow + driveWell + liveLine;
el.querySelectorAll('a.chip2').forEach(a =>
@@ -1474,7 +1504,7 @@ function renderFlight() {
return `<div class="sess-row${m.id === sid ? ' sel' : ''}" data-sid="${esc(m.id)}" role="button" tabindex="0">` +
`<span class="top"><span class="dot${active ? ' live' : ''}"></span>` +
`<span>${esc((m.label || m.id.slice(0, 8)).split(' · ')[0])}</span>` +
`<span class="sid">${esc(m.id.slice(0, 8))}</span></span>` +
`<span class="sid">${esc(m.id.slice(0, 8))}</span>${modelChip(agentFor(m.id))}</span>` +
`<span class="sub">${m.task ? 'on ' + esc(m.task) + ' · ' : ''}${m.count || 0} events · ${m.last ? ago(m.last) + ' ago' : ''}</span>` +
(m.lastSummary ? `<span class="sub">${esc(m.lastSummary)}</span>` : '') +
`</div>`;
@@ -1498,12 +1528,14 @@ function renderFlight() {
const stopBtn = agent && agent.status === 'running'
? `<button id="stopagent" class="stopbtn" data-aid="${esc(agent.id)}">Hold</button>` : '';
const branch = agent && agent.branch ? ` · <span class="mono">${esc(agent.branch)}</span>` : '';
// Honesty about what the run actually rode: the configured model, or
// the vendor default it inherited. Interactive sessions say nothing.
const model = agent ? ` · <span class="mono">${agent.model ? esc(agent.model) : 'model inherited'}</span>` : '';
// Honesty about what the run actually rode. A known model is the chip's
// job now, beside the name; the line keeps only what the chip cannot
// say — that this launch inherited the vendor default. Interactive
// sessions say nothing.
const model = agent && !agent.model ? ` · <span class="mono">model inherited</span>` : '';
$('#fsession').innerHTML =
`<div><div class="s-title">${esc((meta.label || sid).split(' · ')[0])}` +
`<span class="sid">${esc(sid.slice(0, 8))}</span></div>` +
`<span class="sid">${esc(sid.slice(0, 8))}</span>${modelChip(agent)}</div>` +
`<div class="s-line">${meta.task ? 'on ' + esc(meta.task) + ' · ' : ''}` +
`started ${fmtShort(meta.started)} · ${meta.count || 0} events · ` +
`${files.size} files edited · ${checks} check runs${branch}${model}</div></div>` +
@@ -1690,9 +1722,11 @@ function renderFocus() {
const refBits = [];
if (task) {
refBits.push(task.number ? '#' + esc(task.number) : esc(task.file));
refBits.push(`<span class="acc">${esc((meta.label || '').split(' · ')[0])}</span>`);
refBits.push(`<span class="acc">${esc((meta.label || '').split(' · ')[0])}</span>` +
modelChip(agent));
if (agent && agent.branch) refBits.push('worktree ' + esc(agent.branch));
if (agent) refBits.push(agent.model ? esc(agent.model) : 'model inherited');
// the chip says which model; the line is left saying only what it can't
if (agent && !agent.model) refBits.push('model inherited');
refBits.push(esc(task.stage) + '/' + esc(task.file));
} else {
refBits.push(esc(sid.slice(0, 8)), 'no task attached');