cmux NEXT: Session Info's Usage and the sidebar hover card show what each session spends
Summary
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, gcp lane VM x86_64 32 vCPU |
| 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.
Agent session: agent session
🤖 Generated with Claude Code


