Feat: Scope abctl's sessions list, spend band and drawer to the chosen agent - #1198
Conversation
SessionSummary.Agent names the coding agent a session belongs to: the affinity owner that claimed it (rossoctl#1194), else the first event from a known agent (pipeline.EventClient.AffinityName), first-wins like Title. The default and pending buckets name none. /v1/usage takes an optional agent=, the label group=agent reports. A ledger window filters its rows to that agent before folding, so every grouping stays exact. A ring window reads the agent axis uncapped and narrows through usage.ScopeToAgent, the same narrowing abctl applies client-side; it keeps group=currency where the agent billed in one unit and serves any other grouping as none, saying so in group. Snapshot.Agent echoes the filter only when it was applied, so a client can tell a narrowed answer from a server that ignored the parameter. Without agent= the response is unchanged. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
The AGENTS pane's scope now reaches every view that shows money or sessions, not just the usage pane: - Sessions lists only the chosen agent's sessions (SessionSummary.Agent); the default and pending buckets, which belong to no one agent, are hidden, and a scope no session belongs to says so. Unscoped, an AGENT column appears when the listed sessions name two agents. - The spend band and its drawer ask /v1/usage?agent= and, against a server that does not echo the filter, narrow the group=agent series with usage.NarrowToAgent — the server's own narrowing. A scope change restarts their polls, so no unscoped figure stays under a scoped title. The drawer's axis cycle drops the agent axis while scoped. - Every pane under the band names the scope in its title. - The AGENTS pane gains a SESSIONS count once sessions name their agent. - A pending bucket the server no longer lists is not drawn as a cached session: it was adopted. Unscoped, the band and drawer request exactly what they did, and a server that names no session agents leaves the sessions and agents tables unchanged. The help note and README now say what the scope reaches; TestPaneKeys_TheAgentsNoteSaysWhatTheScopeDoesNotReach pinned the old limitation and is replaced by one pinning the surfaces it reaches. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
… axis AgentSnapshot finds an agent the response cap would fold away; a scope hides cached-only rows as well as other agents' sessions; and a scope set while the drawer sat on the agent axis neither asks for it nor lists it in the hint's cycle. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (47)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review at
|
Fixes review: sessions were scoped by agent name while the band and drawer used the versioned label (agent_scope.go:34) Fixes review: Enter did nothing in the AGENTS picker after its columns changed (agents_pane.go:263) Fixes review: a server that names no session's agent emptied the scoped sessions list (sessions_pane.go:390) Fixes review: the ring's one-unit currency guard under agent= had no test that could fail (scope.go:193) Fixes review: the ring filed a one-unit agent's plain and unpriced requests under that unit for group=currency (scope.go:196) Fixes review: a scoped drawer headed an empty BY NONE column and `a` changed nothing visible (scope.go:192) Fixes review: SessionSummary.Agent was set for a claimed default bucket (store.go:875) Fixes review: cached pending rows were hidden with no agent picked (sessions_pane.go:311) Fixes review: the agent= suffix was appended after the events and detail titles were fitted (app.go:2287) Fixes review: the AGENT cell rendered a server string unsanitised (sessions_pane.go:277) Fixes review: agent= was missing from the /v1/usage docs and godoc, and agent from the /v1/sessions field list (docs/pricing.md:184) Fixes review: FilterAgent's unknown-bucket match and the agent= length refusal had no tests Fixes review: `a` from the agent index under a scope refetched the axis already shown (spend_drawer.go:390) Fixes review: the SESSIONS placeholder was an en dash, not emptyCell (agents_pane.go:404) Fixes review: a stale "NO SESSIONS COLUMN" comment (agents_pane.go:203) Fixes review: sessionListed and sessionsListTwoAgents sat between cachedOnlySessionIDs' godoc and its function Fixes review: maxAgentLabelLen was 512 against the 96-byte label cap (usage.go:260) Files: - CLAUDE.md - cmd/abctl/README.md - cmd/abctl/tui/agent_scope.go - cmd/abctl/tui/agent_scope_test.go - cmd/abctl/tui/agents_pane.go - cmd/abctl/tui/app.go - cmd/abctl/tui/footer.go - cmd/abctl/tui/sessions_pane.go - cmd/abctl/tui/spend.go - cmd/abctl/tui/spend_drawer.go - core/cost/usage/agent_snapshot_test.go - core/cost/usage/scope.go - core/cost/usage/usage.go - core/session/agent_test.go - core/session/store.go - core/sessionapi/usage.go - core/sessionapi/usage_agent_test.go - docs/pricing.md Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
mrsabath
left a comment
There was a problem hiding this comment.
Reviewed the three commits you flagged (ec7dd022, afec2cc3, fe1ca095) plus the fix commit b05f157; #1195's money-unit work excluded as asked.
No must-fix issues. Two non-blocking comments inline — one asks for a sentence of rationale, one for a clause in a doc comment.
What I checked and found clean
- Label agreement across paths, the dual-mode risk here.
SessionSummary.Agent, the ring'sbyAgentkey andagent=matching all route throughusage.AgentLabel.store.gostoringAgentLabel(event.Client)while keying the dedup byAffinityName()is the right split: the owner is matched by name, the wire label is whatagent=compares.TestListSessions_AgentIsTheGroupAgentLabelpins it, including a label pastMaxLabelLenwhere truncation could have desynced the two. - Ledger/ring parity —
ledger.FilterAgentreuseslabelFor(r, GroupAgent), so the""→unknownjoin is the same string from either producer; covered for both windows. - Echo semantics — the unconditional
snap.Agent = agenton the ledger path is safe viaomitempty, and the absent-key case is asserted rather than assumed. - Series cap —
AgentSnapshotreads the agent axis uncapped, so an agent pastMaxSeriesInResponseis findable. - Input bounding —
agent=capped atusage.MaxLabelLen, matching the storage cap rather than the 512 an earlier round had; both sides of the boundary tested. - Escape sanitisation — the new AGENT cell and every title suffix go through
sanitizeLabel, with a test scanning for0x1b. - Older-server degradation — sessions list stays whole with a
list not scopedfooter, band narrows client-side, drawer names the missing breakdown. Three distinct behaviours, three tests. - Column/row arity — columns and rows change together in both rebuild paths, cursor visibility survives the column change, and all three arrival orders plus resize are covered.
- Commits and docs — all signed off, subjects within length;
CLAUDE.md,docs/pricing.md, README and the help overlay all updated. The test pinning the old limitation was replaced rather than deleted, which is the right call.
Notes
- I could not run the tests. No clone of this repo was available to me, so I reviewed the diff and fetched individual files at
b05f157but executed nothing — yourgo test/golangci-lintclaims are unverified by me. I did confirm every helper the new code references (setCursorVisible,sameColumns,sanitizeLabel,formatCount,clipRow) is defined at the head. Adoptedis a new/v1/sessionsfield doing real work (hiding adoptedpending:rows) but isn't in the PR body's Server section — worth a line there, since it's a wire-contract change.CLAUDE.mdwas updated for it.- Your own "Not yet done" list is candid, and I'd single out the same item: the first-wins ordering assumption with
session.client_affinityoff is the one claim no unit test can settle. Worth exercising by hand against a live proxy before merge.
| idle.Buckets = narrowBucketsToAgent(snap.Buckets, agent) | ||
| scoped = &idle | ||
| } | ||
| scoped.UngroupedCostMicros, scoped.UngroupedAvoidedMicros = nil, nil |
There was a problem hiding this comment.
suggestion — dropping the window residuals here reverses a documented invariant in this same file, silently.
Thirty lines up, ScopeToAgent carries a long comment block explaining that UngroupedCostMicros and UngroupedAvoidedMicros are deliberately kept: "a residual is a fact about the WINDOW, true whichever agent is scoped to" — the same argument that keeps DaysOutsideRetention — with the disclosure duty handed to the caller.
Clearing them unconditionally here is defensible on the merits: /v1/usage?agent= serves group=none, so there's no series left for a residual to be the complement of, and a client summing per-agent figures against a scoped total finds no shortfall needing explanation. But a future reader comparing the two functions has nothing telling them which behaviour is intentional and which is an oversight.
A sentence in NarrowToAgent's doc — that the residual is meaningless once the grouping is downgraded to none — would settle it.
Worth noting the asymmetry isn't total, and the parts that survive are the right ones: Degraded, DaysOutsideRetention and SeriesOvershootMicros all come through both paths, which is consistent with what their docs claim.
| if err != nil || snap.Agent == agent { | ||
| return snap, err | ||
| } | ||
| all, err := client.GetUsageWindow(ctx, window, resolution, "", usage.GroupAgent) |
There was a problem hiding this comment.
nit — the doc comment doesn't mention that the caller's group is necessarily lost on this path.
Asking for GroupAgent is correct and unavoidable: it's the only axis carrying the per-agent series there is to narrow from. The consequence is that a drawer asking for group=model under a scope, against a server predating agent=, gets group=none back, and drawerScopeNote then renders "no model breakdown for one agent in …". That's the right user-visible outcome and TestSpendDrawer_SaysWhenOneAgentHasNoBreakdown pins it.
So purely a readability point: the function's doc says it "narrows the group=agent series itself", which reads as though the requested grouping were preserved. One clause saying the grouping degrades to none would save the next reader the trace through NarrowToAgent.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
esnible
left a comment
There was a problem hiding this comment.
Summary
The scope plumbing is careful work — the shared fetchUsageScoped fallback covers band, drawer and usage pane (the gap the description flagged as unverified is in fact closed), the Snapshot.Agent echo contract holds on all three producer branches, ledger.FilterAgent keys on the same label the ring folds by, and the cols[:1:1] / r[:1:1] insertions correctly avoid aliasing. Three things block merge: the session→agent label can disagree with the group=agent key for Claude Code's WebFetch client, the scoped drawer renders BY NONE over its own header on every ring-path window, and the new AGENT column reintroduces the TITLE width non-monotonicity growSessionsTitle exists to prevent.
Author: huang195 (MEMBER — maintainer)
Areas reviewed: Go (server: session store, sessionapi, cost/usage, cost/ledger; client: abctl TUI, apiclient), Docs
Agent/IDE config (.claude/.vscode): none
Commits: 20 commits; all signed off except the merge commit 538d2c7 (DCO check passes). Reviewed the four commits after the #1195 merge base, per the description.
CI status: DCO pass, CodeRabbit pending
One correction for the description (not a code issue): it says group=currency for an agent billing in one unit is answerable on the ring. It is not — ringSnapshot drops group entirely when agent != "", and NarrowToAgent sets Group = GroupNone unconditionally. The code, TestHandleUsage_AgentAndGroupCurrency, and docs/pricing.md ("falls back to none for every grouping") all agree with each other; only the PR text is stale. Nothing to fix unless the single-unit case was actually intended, in which case it is missing.
The agent=(other) and agent=unknown spoofability is consistent with the existing overflowLabel godoc, so I am not asking for a change there.
| } | ||
| } | ||
| if agentName != "" && sessionID != DefaultSessionID && !strings.HasPrefix(sessionID, PendingPrefix) && sess.agentLabel(agentName) == "" { | ||
| sess.agents = append(sess.agents, sessionAgent{name: agentName, label: usage.AgentLabel(event.Client)}) |
There was a problem hiding this comment.
must-fix — sess.agents is keyed by event.Client.AffinityName() but stores usage.AgentLabel(event.Client) (= Label()). Those two disagree for exactly the client AffinityName was added for, as its own godoc says ("WIDER THAN Name, AND ONLY HERE").
For Claude Code's WebFetch UA Claude-User (claude-code/2.1.284; +https://support.anthropic.com/), ParseUserAgent leaves Name == "" (the first token claude-user is not a knownClients key, and trailingKnownClient cuts "(claude-code/2.1.284;" to "(claude-code", which does not match). So:
AffinityName()trims"();,"→"claude-code"Label()/usage.AgentLabel()→ the raw UA string, its owngroup=agentseries key
Because first-wins keys on name, not label:
- Claude Code hydrates and claims session
abc(owners["abc"] = "claude-code"), entry holding no events yet — which Feat: Keep each coding agent's header-less requests in its own session #1194's doc calls the common case, not a race. - A header-less WebFetch call is routed into
abcbySessionForClient("claude-code")and appended first →agents = [{name: "claude-code", label: "Claude-User (claude-code/2.1.284; …)"}]. - Every later
claude-cli/2.1.284event hitssess.agentLabel("claude-code") != ""and is skipped. agentOfLockedtakes the owner branch → the raw UA.
The session's Agent is then a label holding only its WebFetch calls, while all its LLM spend is filed under claude-code/2.1.284. sessionListed compares s.Agent == scope, so the session is hidden from the claude-code/2.1.284 scope — the row an operator actually picks, since it is the one with the money. Symmetrically, /v1/usage?agent=Claude-User (claude-code/2.1.284; …) (60 bytes, under the 96-byte bound, so accepted) returns the WebFetch sliver.
This is what the last commit ("fix: Match a session's agent by the label agent= uses") claims to close. Neither new test catches it: TestListSessions_NamesTheSessionsAgent puts the WebFetch client in its own fetch-only session (where the raw label is genuinely right, and the assertion blesses it), and TestListSessions_AgentIsTheGroupAgentLabel gives each of its three clients a separate session — so no test ever puts two labels sharing an AffinityName in one session.
Keying first-wins on the label, or preferring the label whose AffinityName matches the owner and that carries a version, would make the SessionSummary.Agent doc ("as usage.AgentLabel names it, which is the label agent= matches") true on the owner path.
Same root cause, milder symptom: first-wins-on-name also pins claude-code/2.1.284 across a self-update to 2.1.285, so post-upgrade spend lands in a different series and is invisible under the session's scope. That one at least follows the documented rule.
|
|
||
| func (m *model) drawerLabels() (usage.Group, string) { | ||
| axis, window := m.spend.axis(), spanLabelFor(m.spend.window()) | ||
| axis, window := m.spendAxis(), spanLabelFor(m.spend.window()) |
There was a problem hiding this comment.
must-fix — this line is changed to m.spendAxis(), but the snap.Group override fourteen lines down (line 518) is left untouched, and that override now always fires under a scope.
usage.GroupNone is the literal "none" and Snapshot.Group is tagged json:"group" — not omitempty. A scoped fetch over a duration window takes the ring path (ringSnapshot → AgentSnapshot → NarrowToAgent), and NarrowToAgent sets scoped.Group = GroupNone unconditionally. So the reply always carries "group":"none", sanitizeLabel("none") is non-empty, axis becomes "none", and drawerHeaders renders "BY " + ToUpper("none").
Reproduce: scope to any agent (↵ in the AGENTS pane), then $ to expand the drawer. The default window is spanHour (window: "1h" — a duration, so the ring answers), and the header reads BY NONE:
WHERE IT WENT BY NONE
Note the asymmetry: on the ledger path (today/7d/month), ledgerSnapshotFor filters rows and keeps snap.Group = applied, so a real per-model series comes back and the header is right. Pressing w from the hour to TODAY therefore silently flips the header between BY NONE and BY MODEL for the same scope — the label tracking which storage backend answered is precisely the mislabel this function's own comment says it exists to prevent.
TestSpendDrawer_SaysWhenOneAgentHasNoBreakdown uses a fixture returning "group":"none" — the exact case — but asserts only strings.Contains(view, "no model breakdown for one agent"), never the header, so BY NONE passes.
Suggest skipping the override when the parsed group is GroupNone (or when snap.Agent != ""), falling back to m.spendAxis() — the axis actually requested, which the new hintAxis/asked plumbing already uses correctly for the hint line but not for the header.
|
|
||
| // sessionsColumnsWithAgent is sessionsColumnsFor plus an AGENT column after TITLE when agent is set | ||
| // and the column fits without narrowing any other; see rebuildSessionsTable for when it is asked. | ||
| func sessionsColumnsWithAgent(termWidth int, agent bool) []table.Column { |
There was a problem hiding this comment.
must-fix — the AGENT column reintroduces the TITLE width non-monotonicity growSessionsTitle was written to prevent.
Declared base widths sum to 88 + 8×2 padding = 104 rendered. Adding AGENT at sessionsAgentWidth = 19 (+2) makes 125, and the new fits loop requires that no column shrink, so AGENT appears only at termWidth >= 125. growSessionsTitle runs last and hands all slack to TITLE; it reserves room for COST/SAVED (its comment explains why) but knows nothing about AGENT:
| termWidth | AGENT | TITLE |
|---|---|---|
| 123 | no | 30 |
| 124 | no | 31 |
| 125 | yes | 11 |
| 130 | yes | 16 |
| 145 | yes | 31 |
So with two agents listed at 124 columns showing 31 characters of each path, dragging the terminal one column wider truncates every TITLE to 11 characters — a left-truncated leaf fragment — until 145. That is verbatim the harm documented a few lines down:
"27 to 11 as the terminal got WIDER, a legible path becoming a leaf fragment, which is the harm this column's floor exists to prevent. Column PRESENCE was already monotonic; this is what makes width so."
Fix: reserve AGENT's 21 columns in growSessionsTitle below 125 the same way COST/SAVED are reserved below sessionsShowMoney's threshold.
Related, lower severity: because the threshold is 125, at 120 columns — a common width, and the one two of the new tests use — AGENT never appears no matter how many agents are listed, so two agents' sessions interleave with nothing distinguishing them. The README's "an AGENT column appears only when the listed sessions name two agents" is really "…and the terminal is at least 125 columns", which nothing states.
| // The band above every pane is scoped too, so every pane says so; usage and agents word it | ||
| // themselves above, and sessionHeader carries it for the events and detail panes. | ||
| if m.agentScope != "" && m.pane != paneUsage && m.pane != paneAgents && m.pane != paneEvents && m.pane != paneDetail { | ||
| title += " · agent=" + sanitizeLabel(m.agentScope) |
There was a problem hiding this comment.
suggestion — this gates on m.agentScope != "" rather than m.sessionsScope() != "".
Against a server that never sets SessionSummary.Agent, the sessions list is deliberately not narrowed (sessionsScope() correctly returns "") but the title still reads abctl · <endpoint> · agent=bob-shell/2.0.5 over the full unfiltered list. The only thing correcting it is the new footer marker, which goes through fitStatusLine and truncates from the right, after [paused] / [filter: …] / [sort: …]: at 80 columns with a restored filter active the 44-column marker is dropped entirely, leaving a title asserting a scope the list does not have and nothing on screen to contradict it.
Suggest gating the paneSessions suffix on m.sessionsScope() (or wording it as scoping the band rather than the list).
Second, smaller point: the negative pane list means the suffix is also appended to paneNamespaces, panePods, panePipeline, panePluginDetail and paneCatalog. The plugin catalog and detail panes have nothing to do with an agent scope, and the namespace/pod pickers render before a client exists, so there is no spend band above them for the "the band is scoped too" justification to apply. An allowlist of the panes that actually sit under the band would opt future panes out by default instead of in.
| // rather than an error. The narrowed buckets carry no series, so any grouping is served as none, | ||
| // and Group says so. | ||
| func NarrowToAgent(snap Snapshot, agent string) Snapshot { | ||
| scoped, err := ScopeToAgent(&snap, agent, NarrowBuckets) |
There was a problem hiding this comment.
suggestion — NarrowToAgent cannot distinguish "this agent had no traffic" from "this agent's label was not in the folded series", and collapses both into the zeroed idle snapshot with Agent echoed — i.e. asserting "this is that agent's spend".
On the server path this is safe today: AgentSnapshot reads with maxSeries = math.MaxInt, which TestAgentSnapshot_FindsAnAgentPastTheSeriesCap pins. But NarrowToAgent is exported and the client fallback calls it on a capped response — cmd/abctl/tui/agent_scope.go does GetUsageWindow(…, GroupAgent) (server-capped at MaxSeriesInResponse = 16) and then usage.NarrowToAgent(*all, agent). Against an older server that ignores agent=, a deployment with 17+ agents where the selected one is outside the top 16 by cost gets a zeroed snapshot with Agent set — indistinguishable from idle. It under-reports to zero rather than showing another agent's figures, but it is still a wrong number wearing a correct label.
Same shape at record time: ring[slot].byAgent is bounded by maxLabelsPerBucket = 64, so past 63 distinct agents in one minute the label is replaced by (other) and the uncapped read cannot recover it — again a zeroed echo rather than a disclosure. AgentSnapshot's godoc currently claims unqualified that "an agent past MaxSeriesInResponse is still found", which is worth qualifying.
Consider having NarrowToAgent distinguish the two cases (leave Agent unset, or set a capped-series marker, when (other) is present and the agent is not) so a client can fall back instead of rendering a false zero.
| if id == DefaultSessionID || strings.HasPrefix(id, PendingPrefix) { | ||
| return "" | ||
| } | ||
| if owner := s.owners[id]; owner != "" { |
There was a problem hiding this comment.
suggestion — the owner branch is taken whenever owners[id] != "", and returns "" when that owner has sent nothing in. Two ways that surfaces as a session hidden from every scope:
Stale owner entry. pruneOwnersLocked sweeps only once len(s.owners) reaches 2*maxSessions, so an entry can outlive its session's eviction (the targeted deletes on the eviction/rekey paths cover the common cases, but not all of them). If the id is later reused: Bob claims abc → abc is evicted → Claude Code creates abc and sends into it → agentOfLocked takes the owner branch, agentLabel("bob-shell") is "", and the session reports no agent even though sess.agents[0] names it correctly. It is then hidden under every scope and gets no AGENT cell.
Just-adopted session. adoptLocked renames the pending:<client> entry into the target id, but every event that accumulated under the pending id was skipped by the prefix guard, so the renamed entry's agents is empty while adoptedByLocked still lists the bucket. ListSessions can return {id: "task-1", adopted: ["pending:bob-shell"], agent: ""} — hidden from the bob-shell/2.0.5 scope while the spend it just absorbed is attributed to that agent in /v1/usage. Transient (the next headered append fixes it), but it is exactly the window in which the TUI refetches after an adoption.
Falling through to agents[0] when agentLabel(owner) == "" would cover both, and would also make a claimed-but-silent session visible under the agent whose traffic is actually in it.
(The Rekey(DefaultSessionID, …) path on A2A traffic has the same "the exclusion guard outlives the exclusion" shape, but coding-agent traffic does not normally take it — not worth blocking on.)
| func (m *model) cycleSpendAxis() tea.Cmd { | ||
| m.spend.groupIdx = (m.spend.groupIdx + 1) % len(spendDrawerAxes) | ||
| axes := m.spendAxes() | ||
| next := axes[(slices.Index(axes, m.spendAxis())+1)%len(axes)] |
There was a problem hiding this comment.
nit — axes[(slices.Index(axes, m.spendAxis())+1)%len(axes)] divides by len(axes), and spendAxes() filters GroupAgent out, so if spendDrawerAxes were ever reduced to just {GroupAgent} this panics on % 0 (and spendAxis()'s spendDrawerAxes[0] would be wrong too). Not reachable today — three axes with GroupAgent last, and I confirmed slices.Index cannot return −1 because spendAxis() maps the agent axis to GroupModel, which spendAxes() always contains. A len(axes) == 0 guard or an invariant comment would match how this file already treats its other unreachable-but-guarded cases (e.g. the i < len(tiers) guard).
Summary
Makes abctl's agent scope reach every pane. Today, picking an agent in the AGENTS pane (
A, or thestartup picker) narrows only the usage pane: the sessions list and the spend band keep showing
every agent, so IBM Bob and Claude Code cannot be told apart in the two views people use most.
Depends on #1195 (money in its own unit), which is merged into this branch. Until #1195 lands
this diff includes it — review the last four commits:
ec7dd022,afec2cc3,fe1ca095,b05f157b.Server
SessionSummary.Agent(omitempty): the coding agent a session belongs to. Never setfor
defaultorpending:buckets./v1/usage?agent=<label>(optional): narrows a window to one agent. Ledger windows filter rowson the agent key; ring windows read the per-agent tally. On the ring only
group=noneis answerable — any other grouping isdowngraded and reported in
group, the existing convention.Snapshot.Agentechoes the filter only when it was applied, which is how a client tells anolder server that ignored the parameter.
abctl
default,pending:and cached-only rows; the title reads
· agent=<label>. An AGENT column appears only when thelisted sessions name two agents.
agent=while scoped; the drawer drops its agent axis.usage.ScopeToAgent,as the usage pane already does — pinned by
TestFetchUsageScoped_NarrowsWhateverTheServerDid.pending:<agent>row Feat: Keep each coding agent's header-less requests in its own session #1194 left behind after an adoption.What does not change
agent=. The new columns need a server that setsSessionSummary.Agent.↵toggles the scope,Esckeeps it.spend band") becomes
TestPaneKeys_TheAgentsNoteSaysWhatTheScopeReaches.Not yet done
Claude Code running concurrently.
session.client_affinityoff,SessionSummary.Agentrelies on each session's firstknown-agent event being its own, which holds in the ordering observed on a laptop.
Context
Last of three PRs finishing Bob/Claude Code separation in abctl (#1194 merged, #1195 open).
Related: #943, #1116.
Deferred
claude-code/2.1.284), so itstops covering an agent once that agent auto-updates. Folding versions is a product decision
across the AGENTS pane, the usage pane's scope and
abctl cost --agent, which must keep givingthe pane's figures.
agent=, a ring window servesgroup=currencyasnone, so a two-unit agent's LAST 1H reads(mixed)where ledger-backed spans split it. It wouldextend the ring's
labelUnitscross-tabulation, which Feat: Show every abctl money figure in its own billing unit #1195 adds.Test plan
go test ./...per module:core,cmd/abctl,cmd/authbridge-proxygolangci-lint run --new-from-rev=upstream/main: no new issuesAssisted-By: Claude Code
Summary by CodeRabbit