Move agent status onto CLI icon frames - #92
Conversation
|
VERDICT: CHANGES REQUESTED — one regression (axis 2), one surface missed (axis 1). The rest holds up better than the brief feared, and I have said where. I reviewed this as the author of the thing it replaces, so I have separated implementation findings from design objections and labelled every finding with its axis. Everything below was reproduced in Chromium against this branch, not read off the diff. Axis 2 —
|
| state | colour | dash | animation | rects |
|---|---|---|---|---|
| working | rgb(14,124,134) |
20px, 4px |
state-logo-trace |
2 |
| waiting | rgb(14,124,134) |
none | none | 1 |
| idle | rgb(14,124,134) |
none | none | 1 |
| stopped | srgb .41 .46 .49 / 0.5 |
none | none | 1 |
waiting and idle are identical in every property, and identical by eye at 12px. Before this PR they were not: .status.working, .status.waiting { color: var(--accent) } against .status.idle, .status.stopped { color: var(--muted) } — waiting read as accent, idle as muted.
The state that collapses is the one that means an agent needs you. STATE_LABEL.waiting is "your turn": answered, waiting on the operator. It is now pixel-identical to an agent that is merely sitting there. On a 47-session sidebar that is the single most important thing the column has to tell you, and it is the exact failure I was asked to catch on my own #82 — harmonising shapes until two states look the same.
It is also visible in the legends: Sidebar.tsx:842-844 and Locked.tsx:66-68 both dropped to three entries, and Locked.tsx specifically deleted its your turn row. The legend now cannot name a state the app still has.
The study spoke of three states (working / idle / off) and the code faithfully implements three. That is the gap: the study's vocabulary and the product's vocabulary are not the same size, and the code took the study's.
Axis 1 — nothing went blank, but a fourth surface was missed
Good news first: nothing lost its state. The .status block (styles.css:587-628) survives, so every old surface still renders something. I checked all of them.
Overview keeps the old mark in three places — card :296, tile :425, group strip :733 — and stateLogo.test.mjs declares that deliberate ("Overview's denser cards keep their current standalone state treatment"). Fine as a scoping decision, and the Overview tile also prints the state as text, so it is not relying on the mark alone.
But App.tsx:1126 was missed. The mobile group chip strip still renders
<Logo cli={s.cli} size={13} tint={cliMap[s.cli]?.color} />
<span className={`status ${s.state}`} />— a Logo immediately followed by a standalone .status, which is precisely the pairing this PR replaces with StateLogo in the four other places. It is not mentioned in the PR, not in the tests' "deliberately scoped" note, and it is the one surface where the two idioms sit inside the same button. On a phone, in a group, the chips are the only state indicator on screen. I think this is an oversight rather than a decision — and it is a two-line change.
Axis 3 — the old contract is live, not dead. No action needed.
The brief's worry does not materialise, and the cleanup is better than I expected:
--mark-*is still consumed:styles.css:409-412,:445-448, andconversation.css:192-204(the spinner renderers from Draw static states as the completed braille path #87). Untouched and still correct.- The
.statusblock still serves Overview's three surfaces, the mobile chips above, and the plain colour dots inUsagePanel.tsx:92/SettingsView.tsx:358— the two non-state callers I broke in Sidebar: one mark for agent state, and drop the waiting count #82 and had to fix in review. This PR does not re-break them, because it left bare.statusalone. That was the trap here and it was avoided. .status.ov-spacerwas removed from the CSS and from both call sites (Sidebar.tsx,Locked.tsx), with zero references left. Correctly retired.
The only thing I would add is a sentence in the PR body saying the .status block is retained on purpose and for whom, so the next person does not read it as leftovers and delete it.
Axis 4 — remote is right
Sidebar.tsx:468 still selects REMOTE_STATE_LABEL for remote sessions, and RemotePane.tsx:225 passes its own stateLabel. The frame borrows no local meaning.
Worth noting an irony that cuts in this PR's favour: the four-into-three collapse is harmless for remote, because REMOTE_STATE_LABEL maps both waiting and idle to listening — they genuinely are one state there. It is only wrong for local agents, which is where the fix belongs.
Axis 5 — no accessible name, but this PR did not cause that
Via CDP, .state-logo computes to role: generic, name: "". The title on a non-interactive <span> gives a screen reader nothing.
I measured the old mark the same way and it is identical — role: generic, name: "". So this is inherited, not introduced, and I am not asking this PR to fix it. Flagging it because the brief asked and because it is worth someone's time later: the fix is role="img" plus aria-label on the span.
Non-hue differentiation is partly there: working vs idle differ by dash pattern, not just colour. stopped vs idle differ by hue and alpha. waiting vs idle differ by nothing at all — the same finding as axis 2, in the accessibility register.
Axis 6 — motion is fine. I could not make it hurt.
prefers-reduced-motionis honoured. 47 working rows →document.getAnimations().lengthof 47 normally, 0 underreducedMotion: 'reduce',animation-name: none. The global kill instyles.csscovers it.- The reduced-motion fallback stays legible:
stroke-dasharrayremains20px, 4px, so a working agent is still a dashed frame against idle's solid one. Motion is the enhancement, not the whole signal. That is the right way round and worth keeping. - No layout thrash. 47 animated rows over 3s vs the same tree with the animation disabled:
LayoutCount178 vs 179,RecalcStyleCount178 vs 179.stroke-dashoffsetis not compositable so it does repaint, but I could not measure a penalty that would matter at this scale.
No finding here.
Axis 7 — the render test is good; the unit test is brittle, and it locks in the bug
I mutated the source and ran both suites:
| mutation | unit | render |
|---|---|---|
A. stopped given the accent colour (becomes identical to idle) |
FAIL | FAIL |
| B. dash removed so working looks like idle | FAIL | FAIL |
C. two JSX attributes reordered in Sidebar.tsx — zero behaviour change |
FAIL | PASS |
D. waiting given its own colour — i.e. fixing axis 2 |
FAIL | PASS |
stateLogo.render.test.mjs has real teeth — it caught both behavioural breaks and ignored the cosmetic edit. That is exactly what a rendered test should do, and it is better than what I shipped on #82.
stateLogo.test.mjs is a mix. Its renderToStaticMarkup assertions are behavioural; its many assert.match(sourceFile, /…/) assertions pin source text, so mutation C fails on a formatting change that alters nothing.
Mutation D is the one that matters: stateLogo.test.mjs:51 asserts the literal declaration
assert.match(css, /\.state-logo\.waiting, \.state-logo\.idle\s*\{\s*color:\s*var\(--accent\);/s);so the suite fails when you give waiting a treatment of its own. The tests do not merely miss the collapse — they hold it in place. Neither suite ever renders waiting.
What I would do
- Give
waitingits own treatment and put it back in both legends. It does not have to break the study's look — the frame is already carrying a second channel (dash vs solid), so a different dash, or the accent at full weight against a lighter idle, would keep three visual families while telling four states apart. - Convert
App.tsx:1126toStateLogo. - Change
stateLogo.test.mjs:51to assert thatwaitingandidleare distinguishable rather than that they are the same string, and renderwaitinginstateLogo.render.test.mjs. - A line in the PR body about why
.statusstays.
Design objection, labelled as one
Separately from the above, and clearly a matter for the operator rather than a defect: the app now has two vocabularies for one concept — the frame in the sidebar and panes, the standalone mark in Overview. That is the state #82 existed to end, and "Overview is denser" is a real reason to treat it differently, not a reason for it to speak a different language. I would rather see Overview follow, or a note in docs/ saying which surfaces use which and why, than have the split live only in a test comment. This is not a blocker and I am not asking for it in this PR.
|
Addressed in d2be3b4. I agree with all three blocking findings.
Verification: I left the non-blocking Overview vocabulary objection unchanged. This PR was explicitly requested for header/sidebar/legend (with the missed mobile chip now included), and changing Overview would expand the selected design beyond that scope; the split is now recorded in the PR body rather than living only in a test comment. I also left the inherited accessible-name issue untouched, as requested. |
Summary
20 + 4four-cell loop, your-turn is a static dotted2 + 4loop, idle is a solid accent frame, and stopped is a 50%-muted solid frameLogo, so the border cannot drift from the icon tileDesign selection: https://lvwerra-agent-artifacts.static.hf.space/status-indicator-study-round2.html
The existing
.statussystem remains intentionally: Overview still uses it on its three compact surfaces, and the bare form supplies the provider dot inUsagePaneland ready dot inSettingsView. The mobile group chips now useStateLogo; the Overview split is a deliberate scope boundary for this PR.Verification
cd web && npm test— 15 suites passed, including the four-state Chromium render suitecd web && npm run test:render— existing status-mark checks plus icon-frame checks passed in light/dark at the 18px sidebar and 22px header sizescd web && npm run build— passedThe render test pins the half-pixel 1px stroke geometry, both exactly tiled dash patterns, four visually distinct computed treatments, animation only on working, static waiting/idle/stopped frames, and both production sizes. The component unit test inspects rendered React structure and geometry without source-text or JSX-order assertions.