refactor(herdr): three-row sidebar layout with shared alert ranking - #582
Merged
Conversation
…sion Control 把「這個 pane 現在最重要的事是什麼」的排序收斂成單一來源 `PANE_CONCERN_TIERS`, 並讓 Mission Control 的 action 鏈改讀它。原本兩處排序互相矛盾:`contextSignal` 把 context 排在所有 tool failure 之上,而 MC action 鏈把 `fail >= 2` 排在 context 之上、`cache dropped` 排在 `fail == 1` 之上——同一個 pane 因此在 sidebar 讀到 "near full"、在 Mission Control 讀到 "inspect last error"。這是 ADR 0005 存在要 防的失效形狀,而為 row 3 的 `$alert` 再寫第三套排序會讓它變成三方分歧。 The merged order interleaves the owner-signed-off $alert tiers (quota-refused > stale > fail > cache-dropped) with the tiers only a single-row surface renders (no-telemetry, blocked, ctx, ready). `sidebarOwned` marks the latter so the three-row sidebar can skip what rows 1 and 2 already show; Mission Control renders every tier. The MC action chain's ONLY behaviour change is `fail == 1` moving above `cache dropped`, which the signed-off order requires. `quotaRefusalCount` reads `status === 429` off the index — an observed refusal, never the forecast in usage-status/*.json, which is #571's surface. fail-on-old (docs/verification-principles.md): asserted through the pre-existing missionControlSnapshot API so the old code answers it rather than throwing on a missing export. Reverting only the MC chain (helper left exported, restoration proved by probe) turns exactly one test red — old 'inspect prompt/tool diff' vs new 'inspect failed tool' — while the fixture guards (cacheDropped/failures/ ctxPct) still pass on the old code, so the red is the ordering and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nstead of overwriting `launch-agent stamps the workspace id into the pane identity header` fails on main (57e5b8f) for any developer whose own shell was launched by this plugin. `pluginEnv()` strips `HERDR_*`, `CCXRAY_*` and `PROXY_PORT` but not `ANTHROPIC_CUSTOM_HEADERS`, which joined the plugin's env surface in #575. The reason it leaks where `ANTHROPIC_BASE_URL` does not is the merge, and that distinction is now written down: `launchEnvVars` OVERWRITES the base url but deliberately PREPENDS the user's existing headers, so an ambient value survives into the spawned plan and the test's exact-equality assertion sees three headers instead of one. Production behaviour is correct and unchanged — the prepend is the documented #575 decision that stops ccxray dropping a user's own `X-Existing: foo`. 差異證據(環境相依,已量測兩次):在本機 pristine tree(origin/main 零差異、 單獨執行)該測試紅,actual 為 `X-Ccxray-Agent-Id: herdr:wY:…, X-Ccxray-Auth: …, X-Ccxray-Agent-Id: herdr:w1:…`; 剝除後綠。CI 以空環境跑,所以它在 CI 一直是綠的——這正是它出貨時沒被發現的原因。 plugins/herdr full suite: 200/200 (CCXRAY_HOME=$(mktemp -d)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…per refresh Sidebar row 3 becomes two tokens with one meaning each — `$facts` (grey, `$42.08 · 60m`) and `$alert` (warning colour) — of which exactly one carries content per refresh while the other is returned in `clearTokens`. That is the mechanism `ctx_bar_*` has shipped with; a single token holding either a fact or a warning would have to change colour with its meaning, the Channel Discipline violation docs/design-principles.md exists to stop. Row height never changes, only content, which design-principles.md:60 permits explicitly. `$alert` reads the shared ranking from the previous commit, so context pressure and process state never reach row 3 — rows 2 and 1 already render them, and a 95% session showing "full" on row 3 would be the FOURTH encoding of one fact (percentage + sparkline + colour band + text). `summarizeTurnGroup` now exposes the raw signals (`failures`, `cacheDropped`, `refusedCount`), all read off `anchor`, so a subagent's failure cannot raise an alert about the main conversation. A pane whose session cannot be located fills NEITHER token: row 1's state_labels already says "not linked", and `$facts` would put the detail's honest-but-useless `n/a · ?` beside it. This is asserted as a forward test, NOT a fail-on-old — `$facts` did not exist before, so the old code would fail it for a missing token rather than for a behaviour difference. Alert tiers gain a sidebar `brief`: the spelled-out 'cache dropped after prompt change' is 33 columns against a 18-40 column row, and truncation is one of the symptoms this layout removes. Mission Control keeps the full sentence. Anything still too long is clipped against the measured width by us rather than cut by Herdr. Also removes a loaded landmine in refresh-badges: `tokens.turns` fell back to `usage.data.meta.totalEntries`, every session's line count, in a per-agent row. Dead today (no path returns a nullish `detail.turns`) so it ships without a differential test — it is a landmine, not a live defect. plugins/herdr suite: 205/205 (CCXRAY_HOME=$(mktemp -d)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`state_labels` replaces a pane's native `state_text`, and the plugin set it on
every refresh to the ccxray summary. Row 1 (`state_icon · agent · state_text`)
therefore read `claude · ccxray: traced · claude` — the agent name twice, and
the model a third time once row 3 carries the cost. Herdr's own idle/working is
the right content for a located pane; it is the one thing on that row Herdr
knows better than we do.
The label is now set only for the states Herdr cannot know ("not linked",
"no hub") and actively CLEARED otherwise. Clearing rather than omitting is
load-bearing: a label Herdr already holds survives a report that does not
mention it, so a pane that recovered from "not linked" would keep showing it
forever. `--clear-state-labels` is the herdr CLI's own affordance for this.
`paneMetadataUnchanged` counts a pending clear as a change, mirroring what it
already does for `clearTokens`. Without it the skip-write optimisation reads
"no token moved" and suppresses the very write that hands row 1 back.
fail-on-old: reverting the conditional (restoration proved by probe) turns
'clears the state labels instead of overwriting the state text' red while
'still labels a pane whose session it cannot locate' stays green — the second
behaviour is unchanged, which is what the pair is there to show.
plugins/herdr suite: 207/207 (CCXRAY_HOME=$(mktemp -d)).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ctx_bar`'s tail carried whichever alert ranked highest. Two problems, both visible in the reported screenshot (`▁▁▁▁▁▁▁▁ 21% · stal…` on one row, `21% stale · claude-opu… · $10.01` on another): - 'full' / 'near full' is a FOURTH encoding of the percentage sitting right beside it — percentage, sparkline, colour band, then the word. - the stale text is now row 3's `$alert`, so keeping it here prints the same reason twice. Row 2 keeps the one context fact no other row shows: how much of the context came from cache. That was already the tail whenever nothing was wrong, so the healthy rendering is unchanged; only the alert-shaped tails go. `contextSignal` is removed rather than left unreferenced — it held the second of the two contradicting orderings, and an unused ranking function is something a future reader reuses. Four existing tests pinned the removed tail. They are RE-POINTED, not made green: each protected property still exists and now has a channel, so the assertions moved to it. Notably `doesNotMatch(detail.ctxBar, /fail/)` (the #438 per-turn-failure guard) had become vacuously true for every input — it now asserts `detail.failures` and `paneAlert`, so a regression to `turnToolFail || toolFail` still turns it red. Known consequence: a config rendering ONLY `$ctx_bar` loses the stale WORD from this tail and keeps the withdrawn colour. The reason is not lost from the token set — `$ctx` still carries `21% stale` and `$alert` carries it in full — but such a config must be migrated to see it as text. This deviates from the handoff's literal instruction to have contextSignal read the shared helper: doing that would have row 2 print the same string row 3 does, which is the duplication the three-row layout exists to remove. plugins/herdr suite: 207/207 (CCXRAY_HOME=$(mktemp -d)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… layout
The installer could only APPEND. A real config had eight rows accumulated across
three generations — the atomic `["$ctx","$model","$cost"]` row, a dead
`$tg/$ty/$tr` row this plugin has never provided tokens for, `$summary`, and the
four `$ctx_bar_*` colour rows — rendering the model three times, the cost twice
and the percentage twice, with four truncations. Running the old installer on it
stacked the new rows on top and made the card WORSE, so shipping row 3 without
this would not have fixed the reported screenshot at all.
The migration mechanism is not new: `OLD_CTX_BAR_ROW_RE` has been migrating the
single-`$ctx_bar` generation to the four colour variants since they shipped. This
widens it to the generations that shipped around it, and deletes conservatively —
only inside this one rows array, and only lines whose COMPLETE token set matches
a superseded generation. A row the user extended is kept, because we cannot know
which half they wanted. Every deletion is printed.
Result on the reported shape: 8 rows → 7, which Herdr renders as three lines
(it skips a row whose tokens are all empty — the four ctx_bar colour rows have
relied on that in production since they shipped).
Three drift sites are collapsed into one while here, two of which were already
broken by this change:
- the ctx_bar colours were declared twice, so a repaired config could disagree
with a fresh one → one MANAGED_ROW_BY_TOKEN map.
- remove-sidebar-summary re-typed the skeleton it deletes → it now derives from
the installer's DEFAULT_ROWS. Row 1 changed shape here, so uninstall would
have silently left an empty table behind.
- onboarding.js spelled out its own "installed?" regex over `$summary` → it now
calls configHasManagedRows. Quick Start reported SETUP · optional for a
correctly installed sidebar until this was fixed, and its regex did not strip
comments, so a commented-out example row counted as installed.
install-sidebar-summary gains ADR 0015's two-mode shape (`require.main === module`)
so the other two can import those constants without running an install.
fail-on-old: reverting main() to append-only (restoration proved by probe) turns
3 of the 5 new tests red — the migration, idempotency, and the deletion report.
The other two stay green on purpose: conservative deletion and the installed
predicate hold under both behaviours.
plugins/herdr suite: 212/212 (CCXRAY_HOME=$(mktemp -d)).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The standalone dashboard action ran a bare `ccxray open`, so invoking it from a pane threw away the one thing its caller knew — which pane they were looking at. `ccxray open --session <id>` has existed since server/index.js:748 and Mission Control's `d` has passed it since it shipped; only this action did not. The pane→session resolution is EXTRACTED from refresh-badges rather than rewritten, as `resolvePaneSessionId`. The badge and the deep link have to agree about which session a pane is, or `prefix+m` → `d` and this action open different sessions for the same pane. It keeps the `agent_session_known` short-circuit: that flag means the context author already asked and got "none", so re-listing the agents could only repeat the answer more slowly. NOT DONE, deliberately: the handoff asked for a `prefix+d` keybinding. `lib/keybindings.js:12-17` records a decision that two keys is the ceiling other Herdr plugins observe and that "grabbing more of a shared namespace for one plugin is the antisocial choice". A third default binding contradicts that, and the convenience already exists at two keystrokes via `prefix+m` → `d`. Adding it is the user's call, not something to slip in against an in-tree decision — the env-override pattern in that file is the shape to use if they want it. open-dashboard gains ADR 0015's two-mode shape so a test can assert the argv without opening a browser on the developer's machine. fail-on-old: restoring the bare `open` call (proved by probe) turns 'passes the focused pane session to ccxray open' red while the no-session fallback stays green — that path was already correct. plugins/herdr suite: 215/215 (CCXRAY_HOME=$(mktemp -d)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…severity 三條 codex 二審 finding 的修正: P1 (blocker): migrateRowsArray 現在辨識並替換舊安裝器寫的 DEFAULT_ROWS (`state_icon workspace tab` + `agent`) 為新的 `state_icon agent state_text`。 舊列不帶 `$` 前綴所以 rowLineTokens 回 `[]`,改用 regex 比對。少了這個, 用上一代安裝器建的 config 遷移後會顯��四行而非三行。 P2a (major): remove-sidebar-summary 現在認得新舊兩種 managed skeleton, 從安裝器的常數衍生而非手寫——這兩個檔案已經因為 row 1 換形狀而不同步了, codex 正確抓到這個漂移。 P2b (major): 429 quota refusal 現在更新 MC 的 severity (→red) 與 reasons, 不只更新 action。少了這個,一個被拒的 pane 在 MC 上看起來是綠的,且 attention filter 會把它排除。 plugins/herdr suite: 218/218 (CCXRAY_HOME=$(mktemp -d)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…N_MARKER codex round 2 P1: a user-authored table (no SECTION_MARKER) that happens to contain `["agent"]` had it silently replaced. The marker is proof the plugin created the table, so without it the row is the user's own. Rejected findings (recorded here per codex-loop rules): - P2a (multi-row same line): TOML rows arrays in Herdr always put each `[...]` element on its own line. `herdr config check` validates this convention. Theoretical. - P2b (shared-row custom token): same granularity as P2a. The installer and uninstaller have operated at line granularity since day one, and a mixed token/user row has never been produced or documented as a supported shape. - P2c (WS 429): a correct observation that Codex WS turns carry status 101, but this is an existing-scope limitation — `quotaRefusalCount` reads Anthropic HTTP turns only, and the WS transport needs `ws-proxy.js` changes to surface upstream close codes, which is another PR's scope. Recorded as a known limitation in PANE_CONCERN_TIERS's quotaRefusalCount comment. plugins/herdr suite: 219/219 (CCXRAY_HOME=$(mktemp -d)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Herdr caps pane metadata at 16 unique token names (set + clear combined). The plugin was sending 12 tokens + 5 clears = 17 unique names, and the two new row-3 tokens pushed it to 19. Every badge refresh failed with `invalid_metadata_token` — a silent failure because the badge write is fire-and-forget from the event handler's perspective, so the sidebar froze on whatever the last successful write left behind. This was invisible in unit tests because `reportPaneTokens` is mocked. Only running `refresh-badges.js` against a real `herdr pane report-metadata` surfaced it. Fix: whitelist the tokens the config actually renders (xray, 4 ctx_bar colours, facts, alert = 7). The ~10 internal tokens (ctx, model, cost, age, turns, cache, fail, summary, ctx_bar, ctx_band) are kept in the `tokens` object for stateLabels, notifications, and the console log, but are NOT sent to `report-metadata`. Un-migrated configs that still reference `$ctx` etc. keep the last value Herdr retained for them — herdr does not evict tokens a report does not mention — and the installer migration removes those rows. `summary`, `ctx_band`, and `ctx_bar` (superseded names) are explicitly cleared as a one-time cleanup. Worst-case unique names per report: 3 set (xray + 1 active ctx_bar colour + facts OR alert) + 7 clear = 10, well within the 16 cap. plugins/herdr suite: 219/219 (CCXRAY_HOME=$(mktemp -d)). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… value Node's http.IncomingMessage joins duplicate HTTP headers with ', '. A ccxray-launched shell that launches another agent (via launch-agent.js) prepends its own ANTHROPIC_CUSTOM_HEADERS — the documented #575 behaviour that preserves a user's existing headers — so the server sees: x-ccxray-agent-id: herdr:wY:old, X-Ccxray-Auth: ..., X-Ccxray-Agent-Id: herdr:w1D:new Before this fix, the raw comma-joined string was passed to clientIdentityFromMessage, which stored it verbatim as the entry's agentId. The badge then searched for `herdr:w1D:p3` and never matched the polluted string, so every pane launched from a ccxray-managed shell showed "not linked" permanently — the auto-refresh cycle was broken. Fix: split on comma, strip header-name prefixes (`X-Ccxray-Agent-Id: `), take the last segment that starts with `herdr:`. The innermost (newest) launch is always appended last. Discovered during live e2e verification of the sidebar three-row layout: the badge rendered correctly when given a session id manually, but auto-refresh (via pane.agent_status_changed) never matched because the persisted agentId was the entire comma-joined string. This is a #575 regression that only manifests when the LAUNCHING shell itself was launched by ccxray. npm test: 2302/2302 (CCXRAY_HOME=$(mktemp -d)). Live e2e: workspace w1D, proxy on 5605 with fixed forward.js, agentId stored as clean `herdr:w1D:herdr-mt2pvxpz-588`, badge auto-refresh matched via launchId, three-row rendering confirmed on herdr pane get. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lis186
added a commit
that referenced
this pull request
Aug 22, 2026
…e2e (#587) * test(hub): wait for a live agent before closing the pane under test `unregisters immediately when a Herdr pane closes with SIGHUP` fired SIGHUP the instant the client appeared in `hub status`, which is the earliest possible moment — the top of the ~1ms window between `hub.registerClient()` resolving and `spawnAgent()` installing the signal handlers. Inside that window SIGHUP still has its OS default disposition, so the client dies without unregistering and the hub keeps it until the 30s dead-client sweep, far past the test's 3s budget. Measured: 8/20 red on this file alone (2/10 in the first sample); red on CI twice in two days, runs 32361079591 (#575) and 32473160514 (#582). The mechanism was confirmed by instrumenting the client — on a failing run stderr stops after `spawnAgent` is entered, `T-handler` never prints, and the process reports `signal=SIGHUP` rather than an exit code. The test never established its own premise either. `agent.pid` was absent on EVERY probed run, passing ones included: the fake agent was killed before its first line, so the green path was "child dies by signal → code null → finish(1) → unregister". It verified that a client killed AFTER the handlers are armed unregisters — never that a pane running an agent was closed. Waiting for a live agent pid fixes both: the premise now holds, and the kill lands after the handlers exist. 20/20 against the unchanged server, so this stands alone as the CI unblock; the window itself is a separate commit. Two details the wait depends on: - the fake agent installs its trap BEFORE publishing its pid, so "pid file exists" implies "ready to be closed". The other order leaves a window where SIGHUP kills the shell outright and the client reports 1 instead of the agent's own 0. - the client's exit code is asserted (0, passed through from the agent). This is half of a pair with the no-agent path; the other half arrives with that fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(hub): arm client shutdown before registering, and gate the agent spawn on it A hub client registered with the hub before it installed any SIGHUP/SIGTERM handler. Until such a listener exists those signals keep their OS default disposition and kill the process outright, skipping `hub.unregisterClient()` — and the hub only prunes by pid liveness every 30s, so a pane closed inside that window left a phantom client blocking hub idle shutdown for half a minute. The window is the statusline check plus the spawn itself, about 1ms. `armClientShutdown` (server/client-shutdown.js) is armed BEFORE `hub.registerClient()`, and `spawnAgent` no longer installs the pair itself when a caller owns them — it publishes its child through `opts.onChild` instead, so one set of handlers covers both sides of the window. With an agent running the handler still just forwards; the graceful unregister keeps living where it did. Two things this needed beyond arming early. The spawn must be gated on the shutdown. `exit()` fires on a hub socket round trip, so a spawn racing it leaves the agent running with its parent gone. Measured before the gate, with the window widened to 300ms and the unregister to 800ms: the client exited 129 while the agent it had just spawned was still alive (`ORPHAN: true`); with the gate, `agent spawned: no`. The gate and the spawn are synchronous from the same point, so no signal can be delivered between them. The exit code is 0, not the OS default's 128+signum. The two sides of the window can only agree in the common case, not by construction: with an agent running the client PASSES THROUGH the agent's own code, and here there is no agent to pass through. An agent that handles SIGHUP exits 0, so the client exits 0 — measured, and now asserted on both paths. Reporting 128+signum would have kept the window visible through `$?` for exactly the case the arming exists to hide. SIGINT is deliberately not armed: spawnAgent installs a no-op so the terminal reaches the foreground child, and a no-op before a child exists would leave Ctrl-C unable to abort startup. test/client-shutdown.test.js drives the module through its injection seams, and caught one defect in doing so: the repeat-signal branch called `exit()` without setting the once-only flag, so a later-settling unregister exited a second time. Invisible in production only because `process.exit` does not return — but `exit` is injectable, so it is not guaranteed terminal. All exit paths share one guard now. npm test: 2315/2315 (CCXRAY_HOME=$(mktemp -d)). test/hub-client-signal.e2e.test.js: 20/20. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(hub): close the shutdown races codex found, and test the window for real Five review rounds against the previous commit; findings went 6 → 4 → 3 → 1 → 0, and every round after the first was in code the previous round's fix had added. Two findings were rejected with reasons, both of which the reviewer subsequently accepted. What changed: **The spawn gate was not adjacent to the spawn.** The previous commit's comment claimed the gate and the spawn were synchronous, which is false on the interactive path: `promptClaudeStatusline()` returns a pending promise and `p.then(doSpawn)` defers the spawn, so a signal could begin the shutdown and the user could answer the prompt before it settled. `doSpawn` now re-checks through `opts.isShuttingDown`; the call-site gate stays. **Register and unregister were unordered.** They are separate socket round trips, so unregistering first let the hub apply the register afterwards and hold a pid that had already exited — this fix's own bug through the back door. The shutdown now waits for the in-flight registration, and the crash-recovery callback both publishes its re-registration and declines to create one while shutting down. `getLock` is a getter for the same reason: unregistering from the hub that crashed releases nothing. **Each wait is bounded separately**, because `registerClient` has 3s of its own and one deadline spanning both would be spent before the unregister was ever sent — in exactly the case that needs it, where the hub applied the registration and lost only the reply. On timeout the unregister goes anyway: it releases the slot in that case, and in the other it lands where not trying would. **SIGINT is armed after all.** The previous rationale — that a no-op before the child exists would leave Ctrl-C unable to abort startup — was a false dichotomy. It routes through the same handler, which does nothing while the agent owns the terminal and unregisters when there is no agent. **One exit path.** The agent's own exit routes through `shutdown()` too, so a signal racing its unregister can no longer report 0 for a child that failed with 42. Escalation moved into the signal handler and counts actual signals, including those forwarded to a live agent — otherwise the Ctrl-C the terminal delivers to both processes read as the user insisting, and cut the unregister short. **Registration failure released the slot.** Both failure branches called `process.exit(1)` from a continuation attached before any shutdown's, so it won the race; a hub that applied the registration and lost the reply kept the slot. They go through `shutdown(1)` now. `signalExitCode` replaces the exit-code constant and is shared with `spawnAgent`'s child-exit handler, so the two sides cannot drift. They still differ when the agent traps the signal and exits 0 — passthrough is deliberate and the agent's own code is more informative — which is one of the two rejected findings. Testing: the claim that an e2e could not reach a 1ms window without a production seam was wrong. It only required the hub not to be real. `test/hub-client-signal.e2e.test.js` now stands up a fake hub socket speaking the real protocol and withholds the `register` reply, making the window as wide as it likes with no test-only branch in the server; it withholds the `unregister` reply too, so a missing spawn gate has time to actually produce an agent. Differential: removing the arming fails both cases, removing both gates fails the new one on the agent it catches. Its first draft was wrong twice and the differential caught both — asserting the marker immediately after exit races the spawned `sh` being scheduled and passes either way, and a surviving orphan inherits the client's stdio so the runner hangs instead of failing. The structural block strips whole-line comments before matching for the same class of reason: prose reading "not process.exit" satisfied a `process.exit` pattern. npm test: 2328/2328 (CCXRAY_HOME=$(mktemp -d)). test/hub-client-signal.e2e.test.js: 20/20 runs. codex gate: clean — "the remaining risk is now below the risk of continuing to change this file". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Justin Lee <justinlee@91app.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
摘要
Sidebar 從累積 8 列(3 處重複、4 處截斷)重構為固定三行版面,每行語意單一:
state_icon · agent · state_text(idle/working)$facts(灰,cost · age)或$alert(警示色,最重要的問題)Detail
Shared concern ranking (ADR 0005 shape)
PANE_CONCERN_TIERSis the single source for "what is the most important thing about this pane right now." Both the sidebar badge's row-3$alertand Mission Control's action chain read it. Previously two orderings contradicted each other —contextSignalranked context above every tool failure, while the MC action chain rankedfail >= 2above context andcache droppedabovefail == 1.The
$alertpriority order (owner-signed-off): quota-refused > stale > fail > cache-dropped.sidebarOwnedtiers (context, blocked, no-telemetry) are skipped by row 3 because rows 1 and 2 already render them.Installer migration
The old installer could only APPEND rows. A real config had 8 rows accumulated across three generations —
["$ctx","$model","$cost"], dead$tg/$ty/$tr,$summary, and four$ctx_bar_*colour rows. Running the old installer made it worse.migrateRowsArraynow replaces superseded generations and legacy default rows (gated on SECTION_MARKER to protect user-authored tables).Two bugs found only through live e2e verification
16-token pane metadata cap: herdr caps
report-metadataat 16 unique token names (set + clear combined). The plugin was sending 17. Every badge refresh failed silently. Invisible in unit tests becausereportPaneTokensis mocked. Fixed by whitelisting only the 7 tokens config rows actually render.Header parse pollution (fix(herdr): native agent launch with header identity, and the silent breaks on that path #575 regression): Node joins duplicate HTTP headers with
,. A ccxray-launched shell prepends its ownANTHROPIC_CUSTOM_HEADERS, so the server sawherdr:wY:old, X-Ccxray-Auth: ..., X-Ccxray-Agent-Id: herdr:w1D:newas one string. Badge matching viaagentIdbroke for every pane launched from a ccxray-managed shell. Fixed by taking the lastherdr:-prefixed segment.What this does NOT do
waitForTelemetrydelay is still a time guess, not a causal barrier. Filed as Visibility barrier for sidebar badge freshness (P7) #581 (P7 visibility barrier with per-pane cursor), designed by a three-model adversarial panel (Fable + Codex GPT 5.6 Sol + Sonnet).prefix+dkeybinding —lib/keybindings.jshas an in-tree decision that two keys is the ceiling.prefix+m→dis two keystrokes.Verification
herdr pane getconfirms three-row rendering withstate_labels: {}and correct token whitelistcodex gate clean
Test plan
CCXRAY_HOME=$(mktemp -d) npm test— 2302/2302install-sidebar-summaryon a config with the old 8-row layout → 7 rows, 3 visiblelaunch-claudeaction → badge auto-refreshes with three rowsherdr pane get <pane>showsstate_labels: {}for a located pane🤖 Generated with Claude Code