From 053e1d7d7135ca8dbec0de7df9da0ed08794f977 Mon Sep 17 00:00:00 2001 From: istos Date: Thu, 30 Jul 2026 12:04:36 +0200 Subject: [PATCH] A model chip beside every agent name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- AGENTS.md | 8 ++ manager/core/board.html | 48 +++++++++-- tests/test_model_chip.py | 171 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 tests/test_model_chip.py diff --git a/AGENTS.md b/AGENTS.md index 98b2230..dfa61da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -253,6 +253,14 @@ Basil, …) — picked per launch, never shared by two running agents, shown as are held in memory, so a restarted board falls back to plain "Agent" for sessions that predate it. +Beside that name, wherever it identifies a run — the sessions list, the +session and Focus headers, the working card's agent line — sits the model +the launch rode: a small mono chip in the id hash's dim register +(`opus-4-8`, the vendor's whole string on hover). Which brain did the work +is a review question, not a state, so the chip takes no colour. A launch +that inherited the vendor default, and a session replayed from disk, wear +no chip at all — the board says nothing rather than guessing. + **▸ start work** launches a headless `claude -p` on the task. It exists only on `in-progress/` cards: moving a card to in-progress is the commitment, and only then does work start — the server refuses launches from anywhere else. diff --git a/manager/core/board.html b/manager/core/board.html index cd403e3..a6026b6 100644 --- a/manager/core/board.html +++ b/manager/core/board.html @@ -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 `${esc(shortModel(agent.model))}`; +} + function connectStream() { const es = new EventSource('/api/stream'); let hadError = false; @@ -1060,6 +1089,7 @@ function cardFor(task) { `
${top.join('')}
` + `
${esc(task.title)}
` + `
${esc(initial)}${esc(who)}` + + modelChip(agent) + `${esc(meta)}${extras.length ? ' · ' + extras.join(' · ') : ''}
` + chipRow + driveWell + liveLine; el.querySelectorAll('a.chip2').forEach(a => @@ -1474,7 +1504,7 @@ function renderFlight() { return `
` + `` + `${esc((m.label || m.id.slice(0, 8)).split(' · ')[0])}` + - `${esc(m.id.slice(0, 8))}` + + `${esc(m.id.slice(0, 8))}${modelChip(agentFor(m.id))}` + `${m.task ? 'on ' + esc(m.task) + ' · ' : ''}${m.count || 0} events · ${m.last ? ago(m.last) + ' ago' : ''}` + (m.lastSummary ? `${esc(m.lastSummary)}` : '') + `
`; @@ -1498,12 +1528,14 @@ function renderFlight() { const stopBtn = agent && agent.status === 'running' ? `` : ''; const branch = agent && agent.branch ? ` · ${esc(agent.branch)}` : ''; - // Honesty about what the run actually rode: the configured model, or - // the vendor default it inherited. Interactive sessions say nothing. - const model = agent ? ` · ${agent.model ? esc(agent.model) : 'model inherited'}` : ''; + // 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 ? ` · model inherited` : ''; $('#fsession').innerHTML = `
${esc((meta.label || sid).split(' · ')[0])}` + - `${esc(sid.slice(0, 8))}
` + + `${esc(sid.slice(0, 8))}${modelChip(agent)}
` + `
${meta.task ? 'on ' + esc(meta.task) + ' · ' : ''}` + `started ${fmtShort(meta.started)} · ${meta.count || 0} events · ` + `${files.size} files edited · ${checks} check runs${branch}${model}
` + @@ -1690,9 +1722,11 @@ function renderFocus() { const refBits = []; if (task) { refBits.push(task.number ? '#' + esc(task.number) : esc(task.file)); - refBits.push(`${esc((meta.label || '').split(' · ')[0])}`); + refBits.push(`${esc((meta.label || '').split(' · ')[0])}` + + 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'); diff --git a/tests/test_model_chip.py b/tests/test_model_chip.py new file mode 100644 index 0000000..73c8931 --- /dev/null +++ b/tests/test_model_chip.py @@ -0,0 +1,171 @@ +"""The model chip beside every agent name (task 24). + +Task 12 recorded which model each launch rode; this puts it where eyes +land — one chip, in the session-id hash's register, beside the name that +identifies a run. + +Two halves. The chip's behaviour (shortening, escaping, and the silence +that means "this launch never knew") is exercised for real: the two +functions are lifted out of board.html and run in node, skipped where +node is absent. The placement — which four render sites wear it, and +that they all wear the same one — is a source-level invariant, board.html +being a single file with inline JS and no frontend test runner. + + python3 -m unittest discover -s tests -v +""" + +from __future__ import annotations + +import json +import re +import shutil +import subprocess +import unittest +from pathlib import Path + +BOARD = Path(__file__).resolve().parents[1] / "manager" / "core" / "board.html" + +# The pieces the chip is made of, lifted from the page as written. +PARTS = ( + r"const esc = \(s\) =>.*?\}\[c\]\)\);", + r"function shortModel\(model\) \{.*?\n\}", + r"function modelChip\(agent\) \{.*?\n\}", +) + + +def _harness() -> str: + html = BOARD.read_text(encoding="utf-8") + out = [] + for pattern in PARTS: + m = re.search(pattern, html, re.S) + if m is None: + raise AssertionError(f"board.html no longer defines {pattern!r}") + out.append(m.group(0)) + return "\n".join(out) + + +class ChipBehaviour(unittest.TestCase): + """What the chip actually renders, run as the browser would run it.""" + + @classmethod + def setUpClass(cls): + cls.node = shutil.which("node") + if not cls.node: + raise unittest.SkipTest("node not available — chip behaviour unrun") + cls.src = _harness() + + def chips(self, agents: list) -> list: + """modelChip(a) for each given agent record.""" + script = (self.src + "\nconsole.log(JSON.stringify(" + + json.dumps(agents) + ".map(modelChip)));") + out = subprocess.run([self.node, "-e", script], + capture_output=True, text=True) + self.assertEqual(out.returncode, 0, out.stderr) + return json.loads(out.stdout) + + def test_a_launch_that_never_knew_its_model_shows_nothing(self): + """No chip is the honest answer for an inherited launch, a session + with no agent record (yours, or one replayed from disk after a + restart), and a pre-task-12 record: a placeholder would read as a + model actually named that.""" + self.assertEqual( + self.chips([None, {}, {"model": None}, {"model": ""}, + {"name": "Wren"}]), + ["", "", "", "", ""]) + + def test_the_vendor_prefix_is_dropped_and_kept_on_hover(self): + """claude-opus-4-8 → opus-4-8 on the face, whole on the title.""" + chip, = self.chips([{"model": "claude-opus-4-8"}]) + self.assertIn(">opus-4-8", chip) + self.assertIn('title="claude-opus-4-8"', chip) + self.assertIn('class="mchip"', chip) + + def test_a_provider_path_is_dropped_the_same_way(self): + """The opencode adapter's ids are provider/model.""" + chip, = self.chips([{"model": "anthropic/claude-opus-4-8"}]) + self.assertIn(">opus-4-8", chip) + self.assertIn('title="anthropic/claude-opus-4-8"', chip) + + def test_an_unfamiliar_name_is_shown_as_recorded_not_guessed_at(self): + """Shortening only removes what this board knows is redundant — + it never eats the first word of a name it does not recognise.""" + for model in ("gpt-4o", "some-model", "opus-4-8"): + chip, = self.chips([{"model": model}]) + self.assertIn(f">{model}", chip) + + def test_the_model_string_is_escaped_on_both_face_and_title(self): + chip, = self.chips([{"model": 'a"${esc(m.id.slice(0, 8))}', + "the session-detail header": + '${esc(sid.slice(0, 8))}', + "the Focus header": + '${esc((meta.label || \'\').split(\' · \')[0])}', + "the working card's agent line": + '${esc(who)}', + } + + def test_every_name_that_identifies_a_run_wears_the_chip(self): + for where, anchor in self.SITES.items(): + pos = self.html.find(anchor) + self.assertNotEqual(pos, -1, f"{where} no longer renders as expected") + window = self.html[pos + len(anchor):pos + len(anchor) + 40] + self.assertIn("modelChip(", window, + f"{where} lost the model chip beside its name") + + def test_there_is_one_chip_component_not_four(self): + """One component means one register: a second inline copy is how + the four drift apart.""" + self.assertEqual(self.html.count('class="mchip"'), 1, + "the chip's markup must live only in modelChip()") + self.assertEqual(self.html.count("function modelChip("), 1) + + def test_the_chip_is_the_id_hashs_register_and_means_no_state(self): + """Mono because a model name is machine-produced, dim because it + is a footnote to the name — and no state colour, because a model + is not a state.""" + rule = re.search(r"\n \.mchip\{([^}]*)\}", self.html) + self.assertIsNotNone(rule, "the .mchip rule is gone") + self.assertIn("font-family:var(--mono)", rule.group(1)) + self.assertIn("color:var(--dim)", rule.group(1)) + for state_colour in ("--accent", "--calm", "--alarm"): + self.assertNotIn(state_colour, rule.group(1), + f"the chip took on {state_colour}: colour " + "would start meaning 'model' as well as state") + + def test_the_chip_never_wraps_a_line_it_joins(self): + """It joins flex rows carrying names and timestamps; a wrapping + chip would move them.""" + rule = re.search(r"\n \.mchip\{([^}]*)\}", self.html) + self.assertIn("white-space:nowrap", rule.group(1)) + + def test_the_metadata_lines_no_longer_repeat_a_known_model(self): + """The chip says which model; the two lines that used to carry it + keep only what the chip cannot say — that a launch inherited the + vendor default — and say it nowhere else.""" + for m in re.finditer(r"model inherited", self.html): + window = self.html[max(0, m.start() - 200):m.start()] + self.assertIn("!agent.model", window, + "'model inherited' must be reached only when the " + "model is genuinely unknown") + self.assertEqual(self.html.count("model inherited"), 2, + "the session-detail line and the Focus refline are " + "the two places that say it") + + +if __name__ == "__main__": + unittest.main()