Repository navigation
cmux NEXT: Session Info's Usage and the sidebar hover card show what each session spends (carry #27923) - #28773
Conversation
…geStats (red) Leo, 2026-10-30: "that visibility into huge-blue guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599
…sion spends Leo, 2026-10-30: "that visibility into huge-blue guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next". acpmux now reports usageStats on every session summary, ls row and peer row (context per call, autocompact cap, 24h tokens and cost, subagents). Session Info gets a Usage row after Context: folded, this chat's 24h cost; open, its numbers, then every session acpmux lists on this Mac and its peers, sorted by 24h cost or by context, five then View all, each row opening its chat. The sidebar's workspace hover card gets one row after the agent: context per call and the day's cost, with the context ring. The snapshot's sessions array is now reused until acpmux's list changes, so a snapshot per token gives the session stores nothing new; Usage sorts only on a list or sort change, and adds no timer. Green: bun test test/session-usage.test.tsx (6 pass; red before: module missing). Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599 Sidebar-Look: Leo 2026-10-30, "that visibility into huge-blue guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599
Review (Sol) found that the snapshot decoded usageStats into a new object on every emit, so an open Usage section redrew its rows on each token. direct.ts now keeps the decoded object while acpmux's numbers are the same (sameUsageStats), and UsageSection is memoized on those stable props. Red: bun test test/session-usage.test.tsx failed without the fix (sameUsageStats missing); green: 7 pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599
…xt step Follows ao-vis-data's final usageStats contract: autocompact is capped, none or unknown, and every count can be null until acpmux's reads cover the day. Null renders as an em dash, never 0 or $0.00, and only autocompact none reads No cap. Each of this chat's numbers offers its next step through existing actions: Compact beside the context (the agent's /compact, with the reach a context-full failure's Compact has) and Show beside the subagents (opens that section). No cap offers Restart with cap, disabled with a tooltip: acpmux has no restart with a cap yet, so the step is named and not invented. A list row opens its chat. Part of HQ#1920, HQ#5123 (telemetry lane), HQ#5226. Green: bun test test/session-usage.test.tsx (8 pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599
# Conflicts: # plans/cmux-next/sidebar-owner.md
inspector-section-order.test.tsx mounts the gallery's full variant, which now lists Usage in the top block: its default order, the drag across the block and a saved order missing Usage and Connectors now place Usage after Context. Green: bun test test/inspector-section-order.test.tsx (8 pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599
# Conflicts: # webviews/src/agent-session/acpmux/Localizable.xcstrings
Swift's currency format rounds half to even by default, so $1,420.50 read $1,420 in the hover card and $1,421 in Session Info's Usage (Intl). The row now rounds half away from zero, as HomeEngineStrings does; the test pins it. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599
inspector-dead-clicks and clickable-cursor were red at 5199abf on the Usage section: Show did nothing in the DOM when Subagents was already open, the pressed sort option took a click that did nothing, and both button classes set cursor: default. Show now opens Subagents, brings it into view and lights it through the same path a reveal uses (light(), shared with the reveal effect); the pressed sort option is disabled; the cursor lines are gone. Red at 5199abf, green here (bun test, each file alone): test/inspector-dead-clicks.test.tsx, test/clickable-cursor.test.ts; test/session-usage.test.tsx and test/inspector-section-order.test.tsx pass. Sidebar-Look: no visual change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-e281136dd51f7f0cd599
# Conflicts: # webviews/src/agent-session/acpmux/Localizable.xcstrings
# Conflicts: # plans/cmux-next/sidebar-owner.md
# Conflicts: # webviews/src/agent-session/acpmux/Localizable.xcstrings
Conflicts: plans/cmux-next/sidebar-owner.md (kept base's runaway badge section and this PR's hover card usage section, host name redacted as base does) and webviews/src/agent-session/acpmux/Localizable.xcstrings (kept both sides' keys). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
…ost is unknown Synthetic state usage-unknown-cost: a peer session with tokens and context but no readable 24h cost, so the cost-sorted list shows what that row reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
…ted list Sorted by 24h cost, a session whose cost acpmux could not read showed its 24h token count under the cost heading. It now shows the unknown mark (—), as every other unknown number in Usage does. The Context sort is unchanged. Test: session-usage.test.tsx, red before, green after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-63b9964940c20354b639
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a Unclear: CI failed on
Not re-run automatically: Written by red: web / Web status; a job it gathers is red on this change (its line here); do not merge until green; see lane-rules Default changesThis pull request changes 6 defaults at
Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Merge receipt: merged as Regenerate the snapshot with Shipped in:
|
direct.ts: base memoized the snapshot's session entries as sessionEntryList (sessionEntriesOf), the same change this PR made as listedEntries; base's names are kept, with the PR's summaryUsage beside them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f71664ebfb6cf5206479
A plain session list carries acpmux's last usage pass, or none at all while nothing asked since acpmux started, so Usage and the sessions by usage list stayed empty or stale. Red until the pane asks with `usage: true` (SessionPager.usage, withUsage, onShowUsage). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f71664ebfb6cf5206479
|
Found 4 test failures on Blacksmith runners: Failures
|
Base moved on 57 commits, among them the regenerated Chief memory inspector page (the fast checks' web bundle step was red at the last merge's base). No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain session list carries acpmux's last usage pass (hub/usage_view.rs), or none while nothing asked since acpmux started, and the pane never asked: Usage and the sessions by usage list stayed empty or stale. Each time Session Info shows, the pane reads _acpmux/sessions with usage: true a page at a time (SessionPager.usage) and lays the numbers over the list and the chat's own summary (withUsage), keeping a turn end's newer numbers and the same array when nothing moved. One read at a time; no timer. After it, each chat's turn end brings its own usage over session_changed, as before. The cmux host's pane policy allows usage on _acpmux/sessions (policy.json, AcpmuxPaneMethods+Params.swift, the params case): the host refuses a frame with a param it lacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-f71664ebfb6cf5206479
# Conflicts: # Packages/macOS/CmuxNext/Sources/CmuxNextAgentLanes/AgentTurnStateFeed.swift # Packages/macOS/CmuxNext/Sources/CmuxNextBridge/AgentTurnStates.swift # Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Model/SidebarWorkspaceDetail.swift # plans/cmux-next/sidebar-owner.md # webviews/src/agent-session/acpmux/App.tsx # webviews/src/agent-session/acpmux/session-info/SessionInfo.gallery.tsx # webviews/src/agent-session/acpmux/session-info/UsageSection.css # webviews/src/agent-session/acpmux/session-info/UsageSection.tsx # webviews/src/agent-session/acpmux/session-info/sectionOrder.ts # webviews/src/agent-session/acpmux/session-info/sessionInfoState.ts
…est take feat's Session Info names (#29921); format
There was a problem hiding this comment.
9 issues found across 29 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardUsageTests.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardUsageTests.swift:41">
P2: These assertions are tied to the current machine’s locale even though `usageFact` uses localized strings and locale-sensitive number/currency formatting. Running the test suite under a supported non-English locale will fail; make the formatting locale-injectable for the test or assert against explicitly localized/locale-independent expectations.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Sources/CmuxNextBridge/AgentTurnStates.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextBridge/AgentTurnStates.swift:104">
P2: The `tokens` parser truncates fractional values, so malformed or non-integral `contextTokens`/`contextWindow` values are displayed as smaller valid counts instead of an em dash. Reject non-integral numbers with an exact `Int` conversion.</violation>
<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextBridge/AgentTurnStates.swift:418">
P2: `applyUsage` can overwrite newer `session_changed` usage with an older in-flight read, leaving the hover card stale until another refresh. Preserve `usageStats.updatedAt` and apply a read only when it is at least as new as the stored snapshot.</violation>
</file>
<file name="plans/cmux-next/sidebar-owner.md">
<violation number="1" location="plans/cmux-next/sidebar-owner.md:284">
P3: The owner record overstates the hover-card contract: cost-only or context-only `usageStats` still produce a usage row without a ring. Document the ring as conditional on both context and context-window values being known.</violation>
</file>
<file name="webviews/src/agent-session/acpmux/usageStats.ts">
<violation number="1" location="webviews/src/agent-session/acpmux/usageStats.ts:70">
P2: `sameUsageStats` treats the read timestamp as a usage change. Since `SessionPager.sameBut` explicitly ignores `updatedAt`, unchanged usage rereads still create a new summary object and redraw the Usage section; exclude `updatedAt` from this equality check.</violation>
</file>
<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HoverCardUsageReadTests.swift">
<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HoverCardUsageReadTests.swift:42">
P2: This test only inspects the static request payload; it never exercises `AgentTurnStateFeed.connect`, so a regression that sends additional usage reads after connection would still pass. Start the feed against a recording stand-in (or inject a connection) and assert the actual outbound requests.</violation>
<violation number="2" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HoverCardUsageReadTests.swift:81">
P2: This pagination test bypasses the feed’s pagination path, so `handle` could stop sending pages or send the wrong cursor while the test still passes. Feed the response through a recording connection and assert the emitted follow-up request.</violation>
</file>
<file name="webviews/src/agent-session/acpmux/sessionPages.ts">
<violation number="1" location="webviews/src/agent-session/acpmux/sessionPages.ts:178">
P2: `usage()` drops `nextCursor` whenever a page has no valid session rows, so an empty intermediate page prevents usage reads for all following sessions. Preserve the cursor on empty pages, as `wholePageOf()` does.</violation>
<violation number="2" location="webviews/src/agent-session/acpmux/sessionPages.ts:182">
P2: Usage is keyed only by bare `sessionId`, so a local chat and a peer chat with the same ID overwrite each other and both rows can display the wrong usage. Key the read and lookup by the qualified session identity, including the peer.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| let kinds = card.facts.map(\.kind) | ||
| let usage = try #require(card.facts.first { $0.kind == "usage" }) | ||
| #expect(kinds.firstIndex(of: "usage") == kinds.firstIndex(of: "agent").map { $0 + 1 }) | ||
| #expect(usage.text == "312K context · $1,421 in 24h") |
There was a problem hiding this comment.
P2: These assertions are tied to the current machine’s locale even though usageFact uses localized strings and locale-sensitive number/currency formatting. Running the test suite under a supported non-English locale will fail; make the formatting locale-injectable for the test or assert against explicitly localized/locale-independent expectations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/WorkspaceHoverCardUsageTests.swift, line 41:
<comment>These assertions are tied to the current machine’s locale even though `usageFact` uses localized strings and locale-sensitive number/currency formatting. Running the test suite under a supported non-English locale will fail; make the formatting locale-injectable for the test or assert against explicitly localized/locale-independent expectations.</comment>
<file context>
@@ -0,0 +1,56 @@
+ let kinds = card.facts.map(\.kind)
+ let usage = try #require(card.facts.first { $0.kind == "usage" })
+ #expect(kinds.firstIndex(of: "usage") == kinds.firstIndex(of: "agent").map { $0 + 1 })
+ #expect(usage.text == "312K context · $1,421 in 24h")
+ #expect(usage.icon == .contextUsage)
+ #expect(usage.accessory == .ring(0.312, tone: .secondary))
</file context>
| let amount = number.doubleValue | ||
| return amount.isFinite && amount >= 0 ? amount : nil | ||
| } | ||
| let tokens = { (value: Any?) -> Int? in amount(value).flatMap { $0 < Double(Int.max) ? Int($0) : nil } } |
There was a problem hiding this comment.
P2: The tokens parser truncates fractional values, so malformed or non-integral contextTokens/contextWindow values are displayed as smaller valid counts instead of an em dash. Reject non-integral numbers with an exact Int conversion.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextBridge/AgentTurnStates.swift, line 104:
<comment>The `tokens` parser truncates fractional values, so malformed or non-integral `contextTokens`/`contextWindow` values are displayed as smaller valid counts instead of an em dash. Reject non-integral numbers with an exact `Int` conversion.</comment>
<file context>
@@ -84,17 +94,40 @@ public nonisolated struct AgentChatDetail: Hashable, Sendable {
+ let amount = number.doubleValue
+ return amount.isFinite && amount >= 0 ? amount : nil
+ }
+ let tokens = { (value: Any?) -> Int? in amount(value).flatMap { $0 < Double(Int.max) ? Int($0) : nil } }
let count = { (value: Any?) -> Int in max(0, (value as? NSNumber)?.intValue ?? 0) }
let detail = AgentChatDetail(model: text(summary["model"]), turnStartedAtMs: (turn?["startedAt"] as? NSNumber)?.uint64Value,
</file context>
| let tokens = { (value: Any?) -> Int? in amount(value).flatMap { $0 < Double(Int.max) ? Int($0) : nil } } | |
| let tokens = { (value: Any?) -> Int? in amount(value).flatMap { Int(exactly: $0) } } |
| const next = usageStatsOf(value); | ||
| if (!next || !previous) return next; | ||
| const keys = new Set([...Object.keys(next), ...Object.keys(previous)]) as Set<keyof SessionUsageStats>; | ||
| for (const key of keys) if (next[key] !== previous[key]) return next; |
There was a problem hiding this comment.
P2: sameUsageStats treats the read timestamp as a usage change. Since SessionPager.sameBut explicitly ignores updatedAt, unchanged usage rereads still create a new summary object and redraw the Usage section; exclude updatedAt from this equality check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/usageStats.ts, line 70:
<comment>`sameUsageStats` treats the read timestamp as a usage change. Since `SessionPager.sameBut` explicitly ignores `updatedAt`, unchanged usage rereads still create a new summary object and redraw the Usage section; exclude `updatedAt` from this equality check.</comment>
<file context>
@@ -0,0 +1,112 @@
+ const next = usageStatsOf(value);
+ if (!next || !previous) return next;
+ const keys = new Set([...Object.keys(next), ...Object.keys(previous)]) as Set<keyof SessionUsageStats>;
+ for (const key of keys) if (next[key] !== previous[key]) return next;
+ return previous;
+}
</file context>
| #expect(turns.detail(for: Self.chat("s1"))?.model == "m") | ||
| } | ||
|
|
||
| @Test func aUsagePageNamesItsNextCursor() throws { |
There was a problem hiding this comment.
P2: This pagination test bypasses the feed’s pagination path, so handle could stop sending pages or send the wrong cursor while the test still passes. Feed the response through a recording connection and assert the emitted follow-up request.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HoverCardUsageReadTests.swift, line 81:
<comment>This pagination test bypasses the feed’s pagination path, so `handle` could stop sending pages or send the wrong cursor while the test still passes. Feed the response through a recording connection and assert the emitted follow-up request.</comment>
<file context>
@@ -0,0 +1,95 @@
+ #expect(turns.detail(for: Self.chat("s1"))?.model == "m")
+ }
+
+ @Test func aUsagePageNamesItsNextCursor() throws {
+ guard case .usage(let numbers, let next) = try #require(Self.usage([["sessionId": "s1", "usageStats": Self.stats],
+ ["sessionId": "s2", "status": "idle"]],
</file context>
| } | ||
|
|
||
| @Test func theWatchAsksForEveryChatsUsageOnceWhenItConnects() throws { | ||
| let reads = Self.requests().filter { $0["method"] as? String == "_acpmux/sessions" } |
There was a problem hiding this comment.
P2: This test only inspects the static request payload; it never exercises AgentTurnStateFeed.connect, so a regression that sends additional usage reads after connection would still pass. Start the feed against a recording stand-in (or inject a connection) and assert the actual outbound requests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/HoverCardUsageReadTests.swift, line 42:
<comment>This test only inspects the static request payload; it never exercises `AgentTurnStateFeed.connect`, so a regression that sends additional usage reads after connection would still pass. Start the feed against a recording stand-in (or inject a connection) and assert the actual outbound requests.</comment>
<file context>
@@ -0,0 +1,95 @@
+ }
+
+ @Test func theWatchAsksForEveryChatsUsageOnceWhenItConnects() throws {
+ let reads = Self.requests().filter { $0["method"] as? String == "_acpmux/sessions" }
+ #expect(reads.count == 1, "one usage read per connection, never one per change")
+ let params = try #require(reads.first?["params"] as? [String: Any])
</file context>
| const page = pageOf<S>( | ||
| await this.request("_acpmux/sessions", { usage: true, limit: SESSION_PAGE, ...(cursor ? { cursor } : {}) }), | ||
| ); |
There was a problem hiding this comment.
P2: usage() drops nextCursor whenever a page has no valid session rows, so an empty intermediate page prevents usage reads for all following sessions. Preserve the cursor on empty pages, as wholePageOf() does.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/sessionPages.ts, line 178:
<comment>`usage()` drops `nextCursor` whenever a page has no valid session rows, so an empty intermediate page prevents usage reads for all following sessions. Preserve the cursor on empty pages, as `wholePageOf()` does.</comment>
<file context>
@@ -163,8 +163,62 @@ export class SessionPager<S extends Summary> {
+ const seen = new Set<string>();
+ let cursor: string | undefined;
+ do {
+ const page = pageOf<S>(
+ await this.request("_acpmux/sessions", { usage: true, limit: SESSION_PAGE, ...(cursor ? { cursor } : {}) }),
+ );
</file context>
| const page = pageOf<S>( | |
| await this.request("_acpmux/sessions", { usage: true, limit: SESSION_PAGE, ...(cursor ? { cursor } : {}) }), | |
| ); | |
| const page = wholePageOf<S>( | |
| await this.request("_acpmux/sessions", { usage: true, limit: SESSION_PAGE, ...(cursor ? { cursor } : {}) }), | |
| cursor, | |
| ); |
| public func applyUsage(_ usage: [String: AgentChatDetail], source: String = AgentTurnStateStore.localSource) { | ||
| for (id, numbers) in usage { | ||
| guard let record = sessions[id], record.source == source else { continue } | ||
| let detail = (record.detail ?? AgentChatDetail()).withUsage(of: numbers) |
There was a problem hiding this comment.
P2: applyUsage can overwrite newer session_changed usage with an older in-flight read, leaving the hover card stale until another refresh. Preserve usageStats.updatedAt and apply a read only when it is at least as new as the stored snapshot.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextBridge/AgentTurnStates.swift, line 418:
<comment>`applyUsage` can overwrite newer `session_changed` usage with an older in-flight read, leaving the hover card stale until another refresh. Preserve `usageStats.updatedAt` and apply a read only when it is at least as new as the stored snapshot.</comment>
<file context>
@@ -376,6 +409,17 @@ public final class AgentTurnStateStore {
+ public func applyUsage(_ usage: [String: AgentChatDetail], source: String = AgentTurnStateStore.localSource) {
+ for (id, numbers) in usage {
+ guard let record = sessions[id], record.source == source else { continue }
+ let detail = (record.detail ?? AgentChatDetail()).withUsage(of: numbers)
+ if record.detail != detail { record.detail = detail }
+ }
</file context>
| await this.request("_acpmux/sessions", { usage: true, limit: SESSION_PAGE, ...(cursor ? { cursor } : {}) }), | ||
| ); | ||
| for (const session of page.sessions) | ||
| if (session.usageStats !== undefined) found.set(session.sessionId, session.usageStats); |
There was a problem hiding this comment.
P2: Usage is keyed only by bare sessionId, so a local chat and a peer chat with the same ID overwrite each other and both rows can display the wrong usage. Key the read and lookup by the qualified session identity, including the peer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/acpmux/sessionPages.ts, line 182:
<comment>Usage is keyed only by bare `sessionId`, so a local chat and a peer chat with the same ID overwrite each other and both rows can display the wrong usage. Key the read and lookup by the qualified session identity, including the peer.</comment>
<file context>
@@ -163,8 +163,62 @@ export class SessionPager<S extends Summary> {
+ await this.request("_acpmux/sessions", { usage: true, limit: SESSION_PAGE, ...(cursor ? { cursor } : {}) }),
+ );
+ for (const session of page.sessions)
+ if (session.usageStats !== undefined) found.set(session.sessionId, session.usageStats);
+ cursor = page.next;
+ if (cursor !== undefined && (seen.has(cursor) || seen.size >= MAX_PAGES)) break;
</file context>
| | Group rail shape | Chrome's line on that edge: 2 pt, flat ends, the header bar's own color, a 6 pt cut below the header, down to the last member's bottom. Debug `sidebar.groupLineLayout`: `edge` (default, the row above), `joined` (no cut, the bar's bottom-left corner square: one L, band header only), `inside` (the header reaches the corner inset further left, 4 pt compact and 5 pt comfortable, capped at the list's edge: the line under its corner). Members never move. | Leo 2026-10-23 ("WTF HAPPENED TO FOLLOWING CHROME"; asks ask-5b16be6ee9, ask-40133bf821, ask-7e10ebd553), #24513 | | ||
| | Row text | As far left as it originally was. A row with no icon starts its title at the row inset; no empty icon slot pushes it right. A top-level group's rows keep the ungrouped rows' left edge. Only nested groups step in. | Lawrence 11405 ("text needs to be as left as possible (how it originally was)"); #23975 | | ||
| | Home unread | A blue dot, the same dot as other unread rows. No count badge. | Lawrence 11384 | | ||
| | Hover card usage | A workspace whose agent chat reports acpmux `usageStats` gets one row after the agent row: the context each call carries and the last 24 hours' cost ("312K context · $1,421 in 24h"), with the context-window ring. No usage, no row. | Leo 2026-10-30 ("that visibility into [one agent host's] guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next") | |
There was a problem hiding this comment.
P3: The owner record overstates the hover-card contract: cost-only or context-only usageStats still produce a usage row without a ring. Document the ring as conditional on both context and context-window values being known.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At plans/cmux-next/sidebar-owner.md, line 284:
<comment>The owner record overstates the hover-card contract: cost-only or context-only `usageStats` still produce a usage row without a ring. Document the ring as conditional on both context and context-window values being known.</comment>
<file context>
@@ -281,6 +281,7 @@ A workspace close removes only the workspace. The daemon close path and remote-c
| Group rail shape | Chrome's line on that edge: 2 pt, flat ends, the header bar's own color, a 6 pt cut below the header, down to the last member's bottom. Debug `sidebar.groupLineLayout`: `edge` (default, the row above), `joined` (no cut, the bar's bottom-left corner square: one L, band header only), `inside` (the header reaches the corner inset further left, 4 pt compact and 5 pt comfortable, capped at the list's edge: the line under its corner). Members never move. | Leo 2026-10-23 ("WTF HAPPENED TO FOLLOWING CHROME"; asks ask-5b16be6ee9, ask-40133bf821, ask-7e10ebd553), #24513 |
| Row text | As far left as it originally was. A row with no icon starts its title at the row inset; no empty icon slot pushes it right. A top-level group's rows keep the ungrouped rows' left edge. Only nested groups step in. | Lawrence 11405 ("text needs to be as left as possible (how it originally was)"); #23975 |
| Home unread | A blue dot, the same dot as other unread rows. No count badge. | Lawrence 11384 |
+| Hover card usage | A workspace whose agent chat reports acpmux `usageStats` gets one row after the agent row: the context each call carries and the last 24 hours' cost ("312K context · $1,421 in 24h"), with the context-window ring. No usage, no row. | Leo 2026-10-30 ("that visibility into [one agent host's] guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next") |
| Trailing indicators | One center x in every row kind and density (section, status, band and chat history headers' chevrons too, a band header in every look, cards included, through `SidebarRegionLayout.itemTrailingTrim`): Home and band items, selected and unselected workspace rows, pinned, nested and top-level group headers (chevron and resting spinner), members, at compact, comfortable and expanded, Minimal Mode on and off. The dot or spinner owns the trailing column; a disclosure, a count, the keycap and the hover close and more sit left of it. On hover the mark gives way: a hovered workspace row draws no dot or spinner while its close, more or pin show (at the default end spot the end-most control takes the column), a hovered loading group header shows its chevron and no spinner, and leaving clears the controls at once as the mark returns. | Lawrence 11914 ("the home blue dot isn't lined up with the other trailing dots"), 11959 ("trailing dots and spinners must line up horizontally across sections"); Leo 2026-10-28 ("If you're hovering over a workspace or whatever, just don't show the spinner. Also ... why the fuck is it not aligned? Oh, because it's the inner tab thing") |
| Status colors | One color per meaning in every row kind (Home, band items, workspace rows, group headers, nested): blue `systemBlue` unread or new, orange `systemOrange` needs attention (a permission prompt, a question, waiting on you), red `systemRed` an error. Tokens `Palette.statusUnread/statusAttention/statusError`; sidebar indicators opt in with `StatusIndicatorView.usesStatusColors`. A row shows one: red, else orange, else blue, else its spinner. A group header, open or collapsed, rolls up its subtree the same way; a collapsed header with none of those keeps its loading spinner. | Lawrence 11983 ("make sure sidebar has blue, orange/yellow for needs attention, and red for errors"); takes back 10-18's "an error is Ready (blue)" and 10-20's "All we care about is the loading" for headers |
</file context>
| | Hover card usage | A workspace whose agent chat reports acpmux `usageStats` gets one row after the agent row: the context each call carries and the last 24 hours' cost ("312K context · $1,421 in 24h"), with the context-window ring. No usage, no row. | Leo 2026-10-30 ("that visibility into [one agent host's] guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next") | | |
| | Hover card usage | A workspace whose agent chat reports acpmux `usageStats` gets one row after the agent row: the context each call carries and the last 24 hours' cost ("312K context · $1,421 in 24h"), with a context-window ring when both values are known. No usage, no row. | Leo 2026-10-30 ("that visibility into [one agent host's] guys was (and is?) pretty bad i feel like we can make more improvements to acpmux/cmux-next") | |
…on for AgentLaneStore and AgentSessionRef (they left CmuxNextAgentLanes' exports on feat)
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Conflict wave: I merged feat-cmux-next (5a27309) into this carry.
|
|
Review (independent Codex gpt-6.1-sol, high, via the review router; full diff from merge base 5a27309 to this head): SHIP at f38ca14. [P2] [follow-up] |
…27923-u # Conflicts: # webviews/src/agent-session/acpmux/Localizable.xcstrings
…p block (#31160) * base red: Session Info's six-dot geometry test counts Usage in the top block #28773 added Usage after Context in the top block (sectionOrder.ts GROUP_SECTIONS.about). The browser geometry test still measured Context straight to Connectors, so it read Usage's 28px row as a gap. It now measures Environment, Context, Usage, Connectors (0px each) and checks Usage and Connectors carry no hairline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-2f629b93d5a20c58e6f8 * test: keep the six-dot geometry test's section list within the format width Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-2f629b93d5a20c58e6f8 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ault (#32850) Leo's order for the summary panel: "1. Machines showing up so I know if it's cloud or not 2. context 3. Subagents/background tasks running 4. Sources". DEFAULT_ORDER is now environment, context, usage, subagents, tasks, sources, changes, outputs, connectors. Usage keeps its place after Context (#28773); the sections he did not name follow Sources; none is dropped. A saved order (any drag) still wins, and the stored format is unchanged, so no migration. Block membership (the hairlines, the #27291 per-block format) is unchanged. The gallery's full chat asserts the order; a new unit test pins the width stages (gutter at 1800px, push at 900px, push at the 200px floor waiting for the toggle at 370px, never over the chat in a tall pane; a short pane's popover is the one overlay). Agent-Session: https://cmux.com/deeplink/agent?id=aid-7a7568197607c3cdfb81 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
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
cd4225cwithorigin/feat-cmux-nextmerged 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.tsxsortValue). 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.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).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-overlapcould 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, stateusage-unknown-cost(synthetic data), 320 px, Chromium, captured in one fast-checks run (run 41159679892). Before ise0319f0(the gallery state without the fix); after is9de6ae6(the head).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
usageStatson every session summary,ls --jsonrow 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:
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:
/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);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
session_changed), and acpmux recomputes them on each list read and at turn end.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.sessionsarray allocations per streamed token (unchanged session list)listedEntriesby construction; no bench runbun usage-bench.ts, 200 runs, Linux x86_64 32 vCPU VM[sessions, sort], the hover card on the watch feed'ssession_changedThe 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)
usageFactper card build, on the samesession_changedthe card already reads; I did not bench it separately.Testing
Review fixes: a Sol review at
ef9b192returned BLOCK because the snapshot decodedusageStatsinto a new object on every emit, so an open Usage section redrew its rows on each token.a72f7a8keeps the decoded object while acpmux's numbers are unchanged (sameUsageStats) and memoizesUsageSection. Its test failed without the fix and passes with it.Red first (
e4d77b1):cd webviews && bun test test/session-usage.test.tsxfailed 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.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.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
Captures
Webview (gallery
agent-pane.chat-inspector, 420 px, Chromium). Before is basee3355039variantfull; after is6bc2cccvariantusage(the base merge after it changed no UI file).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.tsxcovers 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
6bc2cccpaint nothing different in these stills: the disabled pressed option keeps its fill, Show's highlight needs a click, and droppingcursor: defaultpaints 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-cion a lane VM only runstest(through the GitHub broker), notcapture, so the hover-card capture has to come from a fleet host. The native gallery's "Agent running" card shows the row (WorkspaceHoverCardGallery.swift), andWorkspaceHoverCardUsageTestschecks its text, order and ring.Changelog
Risk
risk:normal. A UI addition that reads an optional field. Without
usageStatsfrom 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