cmux NEXT: Session Info's Usage and the sidebar hover card show what each session spends (carry #27923)
Carry of #27923
This PR carries #27923 (its owner's lane is gone). It keeps that PR's commits and authorship as they are: the branch is #27923's head cd4225c with origin/feat-cmux-next merged in (a merge, not a rebase or squash), plus two new commits.
Conflicts resolved (merge 99e7ca0):
plans/cmux-next/sidebar-owner.md: base added "Runaway session warning badge (2026-10-30)" where this PR added "Hover card names each agent chat's usage (2026-10-30)". Kept both sections, base's first. This PR's two Leo quotes (the new section and its table row) now name the host the way base's section does,[one agent host's].webviews/src/agent-session/acpmux/Localizable.xcstrings: base added the feature tip, credits and Background tasks keys at the top where this PR added itschatInspector.usage.*keys. Kept both sides' keys (2024 keys, no duplicates, valid JSON).
BLOCK fixed (9de6ae6): in Session Info's Usage, the All sessions list sorted by 24h cost showed a session's 24h token count when its cost was unknown (UsageSection.tsx sortValue). A token count sat under a cost heading. It now shows the em dash (—), as every other unknown number in Usage does. The Context sort is unchanged: it still shows context tokens.
- Test:
session-usage.test.tsx"sorted by 24h cost, a session whose cost is unknown shows an em dash, never its token count; by context it shows its context". Red without the fix (received"9M", expected"—"; 8 pass, 1 fail). Green with it: 9 pass, 0 fail.inspector-section-order.test.tsxalso passes (17 pass across both files). - Gallery: new synthetic state
usage-unknown-cost(e0319f0, committed before the fix so the same state renders both ways). It is sorted by 24h cost and includes a peer Codex session that has tokens and context but no readable cost.
Overlap: pr-overlap could not run (the shared GitHub REST quota was exhausted at the time). Base's runaway badge (#27899) touches the same sidebar-owner plan, and the conflict above is resolved.
Evidence (this carry)
Gallery agent-pane.chat-inspector, state usage-unknown-cost (synthetic data), 320 px, Chromium, captured in one fast-checks run (run 41159679892). Before is e0319f0 (the gallery state without the fix); after is 9de6ae6 (the head).
| Before | After | |
|---|---|---|
| Light | ![]() | ![]() |
| Dark | ![]() | ![]() |
Viewed after image: in both themes, All sessions is sorted by 24h cost ("24h cost" pressed, filling only its own button). dep-fleet $1,421, dep-land $1,288 and inspector-polish $61.20 come first, then unpriced-codex (lane-vm) ends in a right-aligned em dash (—). The before stills show "52M" there, its 24h token count. Nothing else in the panel changed: this chat's own Usage rows, Connectors and the sections below are identical.
The original PR's stills below (in pr-media/27923/) still hold for the rest of the surface; the carry changes no other paint.
#27923 closes once this lands.
Summary (from #27923)
Part of HQ#1920, HQ#5123 (the telemetry lane) and HQ#5226.
Leo, 2026-10-30: "that visibility into [fleet host] guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next"
And, on keeping it lean: "and acpmux ls should probably not be overly... verbose? i guess" / "to keep everything efficient... tho idk". The app follows the same rule: the sidebar row stays as it is, and Usage is folded by default to one value (this chat's 24h cost). The full numbers and the All sessions list show only when someone opens it, and the list is sorted only while it is open.
A usage audit found four long-running deputies with no autocompact cap. Each carried 290-330k of context per call and fanned out 67-90 subagents a day. Nothing in acpmux or cmux NEXT showed it. The companion acpmux change puts usageStats on every session summary, ls --json row and peer row: context per call, the autocompact cap, 24h tokens and cost, compactions and subagents. This PR shows those numbers in the app.
-
Session Info: Usage. A new row in the top block, after Context. Folded, it shows this chat's 24h cost (or its 24h tokens when no cost is known), like Context's one-line value. Open, it shows:
- this chat's numbers: cost and tokens in the last 24h, context per call against its window, the autocompact cap ("No cap" when there is none), compactions, subagents (live, last hour, last 24h) and the launcher;
- All sessions: every session acpmux lists on this Mac and its peers, sorted by 24h cost or Context with a two-option toggle (24h cost first). Each row shows its harness mark, title, host and the sorted value. Five rows show, then View all. A row opens that chat (
chat.offer.open, as a sender chip does); this chat's own row is plain text.
This is the fleet sessions view. It sits where Session Info already lives, not on a new surface, and the person can drag it like any other section.
-
Sidebar hover card. A workspace whose agent chat reports usage gets one row after the agent row: "312K context · $1,421 in 24h", with the context-window ring. A chat with no usage gets no row.
AgentChatDetail.of(summary:)readsusageStatsfrom the watch feed it already parses, so the card needs no new request. -
Design rules followed: the sort toggle fills only its own button, never the row. There is no dot and no animation. Both strings catalogs carry all 21 locales.
plans/cmux-next/sidebar-owner.mdlogs the hover-card look change (Sidebar-Look:trailer). -
Unknown is unknown. A number acpmux has not read (null, or a missing
usageStats) shows as an em dash, never 0 or $0.00. Its 24h counts stay null until acpmux's reads cover the whole day. Onlyautocompact: "none"reads as No cap;unknownreads as an em dash. -
Every number has a next step, using existing actions:
- context per call → Compact: the agent's own
/compact, through the path a context-full failure's Compact already uses, with the same reach (never a View Only chat, a read-only pane or the next chat); - subagents → Show: opens the Subagents section;
- no cap → Restart with cap: shown disabled, with a tooltip, because acpmux cannot restart a session with an autocompact cap yet. The step is named here rather than built as a new subsystem;
- a session in All sessions → clicking its row opens that chat;
- the hover card's usage row → the card already opens its workspace on click, and from there Session Info's Usage has the actions above. The hover card itself gets no buttons.
- context per call → Compact: the agent's own
Field names follow ao-vis-data's final contract: usageStats.{launcher, autocompact, autocompactCap, contextTokens, contextWindow, compactions24h, subagentsLive, subagents1h, subagents24h, tokens24h, costUsd24h, updatedAt}. Until acpmux sends the field, nothing changes: Usage and the hover-card row do not show.
Performance
- No new timer, observer or animation loop. The numbers refresh on the session list acpmux already sends (watch and
session_changed), and acpmux recomputes them on each list read and at turn end. - No per-token O(rows) work. Building the snapshot used to map
this.sessionsinto a new array on every emit. It now reuses one array until acpmux's list changes (listedEntries), so a token-rate snapshot gives the pane's session stores (hover, sub-session, Usage) the same array. Usage scans forusageStatsonly when that array changes, and sorts only while open, when the list or the sort changes.
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
snapshot sessions array allocations per streamed token (unchanged session list) | 1 | 0 | -1 | direct.ts listedEntries by construction; no bench run |
| Usage list sort, 100 sessions (p50 / p95), on a list or sort change only | n/a (no list) | 0.011 / 0.021 ms | new | bun usage-bench.ts, 200 runs, Linux x86_64 32 vCPU VM |
| Usage list sort, 500 sessions (p50 / p95) | none on base (no Usage list) | 0.032 / 0.041 ms | new | same |
| Usage list sort, 2,000 sessions (p50 / p95 / max) | none on base (no Usage list) | 0.184 / 0.298 / 0.949 ms | new | same |
| work per streamed token for Usage and the hover card row | none on base (no Usage list) | 0 (no timer; the sessions array is the same object) | none | by construction: Usage recomputes on [sessions, sort], the hover card on the watch feed's session_changed |
The sidebar row itself is unchanged: no number or badge on the row (ao-vis-warn's warning badge is that PR's). The numbers live only in the hover card, Session Info's Usage and its All sessions list. The hover card row is one O(1) usageFact per card build, on the same session_changed the card already reads; I did not bench it separately.
Testing
Review fixes: a Sol review at ef9b192 returned BLOCK because the snapshot decoded usageStats into a new object on every emit, so an open Usage section redrew its rows on each token. a72f7a8 keeps the decoded object while acpmux's numbers are unchanged (sameUsageStats) and memoizes UsageSection. Its test failed without the fix and passes with it.
Red first (e4d77b1): cd webviews && bun test test/session-usage.test.tsx failed on base with "Cannot find module '../src/agent-session/acpmux/usageStats'" (0 pass, 1 fail). Green at the head: 8 pass (unknowns render as an em dash; Compact sends; Restart with cap is disabled).
bun test test/session-usage.test.tsx test/chat-inspector.test.tsx test/subagent-limit-stepper.test.tsx test/session-hover-card.test.tsx src/agent-session/acpmux/inspector/ test/chat-inspector-*.test.ts*: all pass.- Direct-client tests run together (30 files): 233 pass, 11 fail. The same 11 fail the same way on the base
e3355039fc7in one combined run, and each file passes on its own. That is cross-file interference already on base. bun run typecheck,scripts/web-lint.shandscripts/cmux-next/prepush-gate.pypass.- Swift:
WorkspaceHoverCardUsageTests(decoding, row order and text, ring, no row without usage) is new and runs on the fleet in the cmux-next tests job; this lane VM runs no Swift.
Observed / Not run
- Observed: the webview tests, type check, web lint and the pre-push gate above.
- Not run here: the Swift suites and a DEV build (no fleet client on this host). They run in CI.
Captures
Webview (gallery agent-pane.chat-inspector, 420 px, Chromium). Before is base e3355039 variant full; after is 6bc2ccc variant usage (the base merge after it changed no UI file).
| before | after | |
|---|---|---|
| dark | ![]() | ![]() |
| light | not captured: the job died on the shared API quota (no rerun, by the quota rule); the dark before shows the same layout | ![]() |
Before: the top block is Environment, Context, Connectors. After: Usage sits after Context, folded to "$61.20 · 24h"; open, it shows this chat's numbers, Show beside Subagents, and All sessions sorted by 24h cost. The gallery passes no Compact handler, so Compact does not show here; session-usage.test.tsx covers it. More gallery states: usage-context, usage-all, usage-no-cap.
Visual review: the independent reviewer (codex gpt-6.1-sol high, fast) viewed the after images (dark and light) and gave VISUAL VERDICT: SHIP. It found no control repeated on every row, nothing bloated, cut off, colliding or wrapping. The icon and text columns line up with Context and Connectors, and the sort highlight covers only the "24h cost" button. The light still has good contrast. The changes after 6bc2ccc paint nothing different in these stills: the disabled pressed option keeps its fill, Show's highlight needs a click, and dropping cursor: default paints nothing.
No capture: the native hover-card row is one added fact row, using the card's existing row and ring (HoverCardAccessory.ring), and the sidebar row itself is unchanged. There is no hover-card still: cmux-ci on a lane VM only runs test (through the GitHub broker), not capture, so the hover-card capture has to come from a fleet host. The native gallery's "Agent running" card shows the row (WorkspaceHoverCardGallery.swift), and WorkspaceHoverCardUsageTests checks its text, order and ring.
Changelog
- Added: Session Info's Usage shows a chat's 24h cost, context per call, autocompact cap and subagents, plus every session on this Mac and its peers sorted by 24h cost or context. The sidebar's workspace hover card shows the agent chat's context and 24h cost.
Risk
risk:normal. A UI addition that reads an optional field. Without usageStats from acpmux, every surface is unchanged. Localization: 16 pane strings and 2 sidebar strings, all 21 locales.
#27923 closes once this lands.
Agent session: agent session
🤖 Generated with Claude Code






