Skip to content

feat: usage analytics — optional host.usage.summary RPC + usage surfaces - #1102

Merged
tanveergill merged 34 commits into
mainfrom
usage-gui-buildout
Aug 11, 2026
Merged

tanveergill merged 34 commits into
mainfrom
usage-gui-buildout

Conversation

@tanveergill

Copy link
Copy Markdown
Contributor

Client-side half of the usage-analytics capability: a new optional protocol method and the GUI surfaces that consume it. The host/server implementation lives in the internal repo and pins this branch.

Protocol (additive, unreleased-window)

  • New optional method host.usage.summary — request {timezone, windowDays, epicId?, chatId?}, response {servedBy, summary, coverage}. Registered with degrade: {kind: "unsupported"}, so older hosts simply lack it and clients feature-detect.
  • Scoped-view additions on the same unreleased method: chatId filter, per-chat grouping, an "entire epic" window (valid only with a scope filter), and chat-scoped per-turn rows (capped, with a truncation flag).

No change to the released protocol floor; no version bridges required while the method is unreleased.

GUI

  • Usage dashboard (Settings → Host → Usage): window picker, cost/token toggle, per-harness split, stacked daily chart with legend filters, token stat tiles, Model/Day breakdown.
  • Scoped views: a numberless entry point in the epic chrome opening a scoped panel (breakdown by chat/agent, "entire epic" window), and a per-chat usage dialog with a per-turn drill-down.
  • No ambient cost display anywhere — cost is always on demand.

Honesty rules (deliberate, please preserve in review)

Dollar figures are "if billed at full API rate", never money spent — subscription plans bill separately. Totals never silently omit turns that could not be priced, and agents that report no cache/reasoning detail are shown as absent rather than zero.

Verification

bun run compile, bun run lint, bun run format green across all five OSS projects; gui-app suite green apart from a pre-existing unrelated reading-position flake (ablation-verified on a clean base).

🤖 Generated with Claude Code

tanveergill and others added 6 commits August 9, 2026 20:26
New optional v1.0 RPC contract for the usage-analytics summary
capability (ticket 6 of the usage-analytics epic): typed request
{timezone, windowDays, epicId?} / response {servedBy, summary,
coverage}, registered with degrade: {kind: "unsupported"} so older
clients simply don't call it. Not part of the frozen v1.0.0 released
floor.

Schemas are an independent zod mirror of the shared
@traycerai/common usage-summary types (protocol can't import
@traycerai/common), matching the existing pattern for other
usage-analytics wire types.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
Add GUI usage surfaces backed by the optional `host.usage.summary` RPC:

- Settings -> Host group -> "Usage" panel: window picker (7/30/90 days,
  viewer's IANA timezone), cost/token metric toggle, stacked daily chart
  (harness identity, capped at 8 direct series + "Other"), and a
  harness/model breakdown table.
- Epic-scoped cost badge in the epic canvas status row, using the same
  RPC with an `epicId` filter and a fixed 30-day window.
- Capability-gated: `host.usage.summary` is negotiated per the protocol's
  optional-RPC handshake, and an unsupported host renders a
  HostScopeGate-shaped capability notice instead of the panel body -
  never a bare empty state or a crash.

Honesty elements (requirements, not polish):
- Every dollar figure carries the "if billed at full API rate" qualifier.
- Totals with unpriced turns render as "$X priced subtotal + N unpriced
  turn(s)", never a bare number.
- `servedBy: "local"` responses surface a this-machine-only scope note.
- The cloud-unavailable RPC failure renders as a retryable error card
  (manual Retry, since this transport path never carries
  `fatalDetails.retryable` for `UsageSummaryCloudUnavailableError`) -
  never a silent fallback to local data.

Charts are hand-rolled SVG/CSS per the dataviz skill (gui-app has no
chart dependency): fixed-order 8-hue categorical palette validated
against both light and dark chart surfaces via the skill's own
validator, 2px stacked-segment gaps, 4px rounded outer segments,
hover+focus tooltips, and a breakdown table as the contrast/CVD relief
channel. Fluid sizing throughout (viewport-capped chart height, no
fixed layout px).

Deviation from the original ticket shape: lands as a Settings section
rather than a new top-level system tab. Mounting a genuine tab would
touch ~25 files in the tab-kind discriminated union - a blast radius
that couldn't be safely verified without live-app iteration, which is
out of scope for this pass. Coordinator-approved; a top-level tab is
left as an explicit non-goal for now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
…llation

Ticket-7 fixup-01: the epic cost badge never rendered on a live host despite
a confirmed matching, priced usage fact - its siblings in the same status
row rendered fine, and the Settings -> Usage panel showed the same data
through the same RPC on the same host, so this was not a mount-point,
capability-negotiation, or host-identity bug (all independently verified via
a new integration test that mounts the real `<EpicShell>` composition
through a real `HostClient` + `MockHostMessenger`, not a mocked substitute).

Root cause: `EpicCostBadge` gated its query by passing `client` as `null`
until `host.usage.summary` was confirmed supported, instead of using
`useHostQuery`'s own `enabled` option like every other conditional host
query in this codebase. Nulling the client changes `useHostQuery`'s cache
key (built from the bound client's resolved host id), so the query's first
REAL fetch starts as a brand-new query at whatever moment `supported` flips
true - squarely the startup window in which a host-bind/auth-context
transition (`HostClient.setRequestContext`/`bind`) can cancel an in-flight
request. That cancellation surfaces as a coordinator control-flow error,
which `withHostQueryErrorBoundary` turns into a SILENT, REVERTING
`CancelledError` - the query lands on `data: undefined, error: null` and
stays there: `host.usage.summary` polled `null`, and neither
window-focus nor reconnect refetch is enabled app-wide, so nothing else ever
retries it. The badge's own render-nothing rule for "no data yet" then
matches this permanently-stuck state indistinguishably from a genuinely
unsupported host or a genuinely empty window - so it just sat there,
silently, forever.

Fix:
- `useUsageSummaryForClient` now takes explicit `enabled`/`poll` params
  instead of folding `enabled` into the `client` argument; both call sites
  (Settings' `UsageSummaryPanel`, `EpicCostBadge`) pass the real client
  unconditionally, so the query key never churns across the capability gate
  flipping.
- `host.usage.summary`'s poll policy moves from `null` to a 15-minute fixed
  interval (matching `host.getRateLimitUsage`'s cadence) - the Settings panel
  opts out (`poll: false`, it already controls its own refetch triggers); the
  ambient, unsupervised epic cost badge opts in (`poll: true`), so a
  silently-reverted fetch self-heals within a bounded time instead of never
  recovering.
- `EpicCostBadge` now wraps its body in `StatusRowChromeBoundary` (extracted
  from `EpicSweepAction`, its sibling in the same status row, into a shared
  file), matching the same "host hooks throw when the runtime is incomplete"
  defense every other host-backed status-row affordance already carries.
- Removed `useUsageSummary`/`hostQueryKeys.usageSummary`, dead code left over
  from the ticket-7 pivot to Settings-section placement (never had a caller).

Test: `epic-shell-cost-badge.test.tsx` mounts the real `<EpicShell>` /
`<EpicSessionProvider>` composition through a real `HostClient` +
`MockHostMessenger`. Its fourth case reproduces the root cause mechanically
- the mock handler throws `HostRequestControlFlowError` on the first call,
which the same production error-boundary path turns into the same silent
revert - and asserts the badge is still absent immediately after (correct:
nothing to show yet, no error surfaced) but appears once the poll interval
fires a fresh, successful fetch. Verified this test fails (times out with
the badge permanently absent) with `poll: false` at the badge's call site,
confirming it exercises the actual fix rather than passing by construction.

Deviation not yet resolved: the investigation (see the fixup ticket) also
surfaced that `recordNegotiatedHostMethods` is only ever called from the
local WS transport (`WsRpcClient`) - `RemoteSession` never records a
negotiated manifest for a remote host, so `useHostSupportsMethod` fails
closed forever for every optional-method gate in the app on a remote host,
not just this one. Flagging this to the coordinator as a likely pre-existing,
cross-cutting gap; out of scope to fix under this ticket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
Additive-only extensions to the still-unreleased `host.usage.summary`
method's schemas (protocol v1.0, unreleased - see `gh release list`
verification in the ticket):

- `chatId` filter and `window: "epic"` request fields, both `.optional()`
  so a pre-ticket-10 client (ticket 7's already-landed GUI) validates
  unchanged.
- Response: per-chat `chatBuckets` (sorted by chatId), per-provenance
  `provenanceSplit` (cost/fact-count/token-count for unpriced/modelPriced/
  providerReported) on totals, `knownCacheSavingsUsd`/`knownReasoningTokens`
  sums on totals and each bucket, chat-scoped `turnRows` capped at
  `USAGE_TURN_ROWS_MAX` (500) with a `turnRowsTruncated` flag.

Updates ticket 7's gui-app test fixtures for the newly-required response
fields (all zero/null-filled, behavior-preserving).

Ticket: usage-analytics-plan/tickets/10-pre-landing-data-fields (epic
7e401ffd-fd95-4d2f-b5b0-3c416d8724c8)

Signed-off-by: Tanveer Gill <simar@traycer.ai>
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
…e shape)

Evolves the Settings -> Host -> Usage panel to the t3code dashboard shape
(reference screenshot from the user, 2026-08-10). Same placement, no new
navigation:

- Per-harness cost split under the headline: one row per harness with a
  share bar, its % of the window's cost, and its token total
  (`buildUsageHarnessSplitRows`). Row colors key off the daily chart's own
  series scale (`colorVar`/`labelFor`) so a harness reads the same hue in
  both views - "color follows the entity" per the dataviz skill.
- Daily chart upgrade: the existing stacked-by-harness chart's legend chips
  now double as a series FILTER - clicking one zeroes that series' segments
  via `applyUsageSeriesVisibility` without touching `scale.order` or any
  chip's color, so a hidden series can always come back in the same slot.
  A date-range label ("Aug 1 - Aug 10, 2026") sits beside the window picker.
- Five-tile stat row: processed tokens (+ per-active-day), cached input
  (+ % of observed input), uncached input (+ cache writes), output
  (+ "includes N reasoning" when reasoning data exists), cache savings
  (+ multiple-of-raw-cost when computable). `usage-stat-tiles.ts` owns every
  "absent, not zero" computation once - a tile's secondary line only renders
  when the underlying figure is a real, positive number.
- Cost-quality panel: % of the window's cost by provenance rung
  (provider-reported / modeled rate / unpriced, reusing the sweep dialog's
  tier-pill green/amber/muted ramp for the same "how much do I trust this"
  shape) plus the known cache-savings figure.
- Breakdown Model/Day toggle: the existing harness/model table gets a
  sibling `UsageDayBreakdownTable` (`buildUsageDayBreakdownRows` folds the
  same buckets by day instead), switched via a new `UsageBreakdownToggle`.
- A window-wide honesty note ("Excludes N turns with no usage reported")
  renders under the stat tiles whenever `usageCompletenessBreakdown.absent >
  0` - those turns still contribute silent zeros to every token sum, which
  would otherwise misread as "no caching happened" rather than "nothing was
  reported" (no wire field for per-field cache absence exists, so this is
  the honest signal available without extending the still-unreleased
  protocol).

All honesty rules stay enforced by construction through the existing
`UsageCostFigure`/`describeCostCoverage`/`servedByScopeNote` trio - none of
the new components render a dollar figure of their own.

`totalTokensForBucket` is refactored to delegate to a new `sumTokenTotals`
helper that takes just the `tokens` shape (not a whole `UsageBucket`) - a
no-behavior-change refactor, but needed by ticket 12's chat/turn rows, which
carry the same `tokens` shape without the rest of `UsageBucket`'s fields.

Fixture tests for `usage-settings-panel.test.tsx` extended to assert the new
dashboard elements render end-to-end.

Ticket: usage-analytics-plan/tickets/11-dashboard-buildout-gui (epic
7e401ffd-fd95-4d2f-b5b0-3c416d8724c8)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
Removes the ambient epic-canvas cost badge and replaces it with an
on-demand epic usage panel, plus a chat-level cost line reachable from
the tab strip's context menu, with per-turn drill-down.

Ambient badge removal (user ruling: no dollar figure belongs ambient
anywhere):
- Delete `epic-canvas/panels/epic-cost-badge.tsx` and its test. This
  also deletes the whole failure-mode class fixup-01 patched (the
  startup-window client-identity race) rather than working around it -
  there is no more ambient query to race.
- `epic-shell.tsx`: `EpicCostBadge` -> `EpicUsageEntryPoint` in the
  status row.

Numberless entry point + on-demand epic panel:
- `epic-usage-entry-point.tsx` (new): capability-gated icon button
  (`useUsageSummarySupported`, app-wide host scope via
  `useReactiveActiveHostId`/`useHostClient` - this sits in the status
  row, not a tab) wrapped in `StatusRowChromeBoundary`. Renders `null`
  when unsupported. No query until clicked.
- `epic-usage-dialog.tsx` (new): opens on click. Window picker (7/30/90
  days + "entire epic"), headline cost figure, daily trend chart (cost
  metric, skipped when factCount is 0), by-chat/agent breakdown
  (`UsageChatBreakdown` over `chatBuckets` - an A2A child chat is just
  another chatBucket row, so this doubles as by-agent), loading/error
  states (retryable error card, never a silent fallback), and a
  "View full usage" footer that hands off to the Settings usage
  dashboard (`openSettings({section: "usage", resetToGeneral: false})`).
  The "entire epic" window reads the host's own resolved bounds
  (`summary.window.endAtExclusive`/`windowDays`) for the trend chart's
  x-axis days rather than the client's `nowMs`, per
  `resolveUsageSummaryEpicWindowBounds` (packages/common, internal
  repo, read-only reference - not modified).
- `epic-usage-window-picker.tsx` (new): `EpicUsageWindow = 7 | 30 | 90
  | "epic"` Tabs control.
- `usage-chat-breakdown.tsx` (new): rows sorted by cost descending,
  joins each chat's title via `useEpicTreeNode` (falls back to the raw
  chatId for a swept chat no longer in the open tree), explicit empty
  state.

Chat cost line (chat overflow, never the header) + per-turn drill-down:
- No dedicated "chat overflow/details" surface existed in this
  codebase (chat-tile.tsx has no overflow menu; the sidebar row menu is
  a command menu, not a details surface; the tab-strip hover tooltip
  can't host a click-to-expand drilldown). Landed the "Usage" entry in
  `TabStripContextMenu` (tab-strip.tsx's own right-click menu, the
  closest existing analogue to "chat overflow") instead - a judgment
  call, not a spec-given location; flagging for review.
- `tab-strip-context-menu.tsx`: new `onOpenUsage: (() => void) | null`
  prop, rendered as a separator + "Usage" item (LineChart icon) after
  "Reveal in Sidebar" when non-null.
- `tab-strip.tsx`: per-tab `onOpenUsage` computed in `TabItem`, gated
  on `useUsageSummarySupported(hostId)` for chat-type tabs; opens the
  chat via a small Zustand store rather than local component state,
  since the dialog needs to be reachable app-wide, not tab-scoped.
- `chat-usage-dialog-store.ts` (new): `{ target: {hostId, chatId,
  chatTitle} | null, open, close }`.
- `chat-usage-dialog.tsx` (new), mounted once in `traycer-app.tsx`
  inside `TraycerAppRuntimeSurface` (needs `HostRuntimeProvider`
  context, which `ReportIssueDialogHost`/`Toaster` sit outside of):
  headline cost figure for the chat's own epic-window request
  (`window: "epic"`, `chatId` set), collapsible per-turn drill-down
  (`usage-turn-drilldown.tsx`, new) honoring `turnRowsTruncated` -
  em-dash for an unpriced turn's cost, never $0.00.
- `use-usage-summary-query.ts`: `buildUsageSummaryRequest` now accepts
  optional `chatId`/`window: "epic"` to build these scoped requests.

Shared extraction:
- `sumTokenTotals` (added to `usage-chart-data.ts` in ticket 11) reused
  directly for `UsageChatBucket`/`UsageTurnRow` totals, since those
  types don't carry the extra fields `totalTokensForBucket`'s
  `UsageBucket` parameter requires.

Docs: SETTINGS.md - added the ticket 12 bullet (ambient badge removal
+ pointer to the new panel's own doc comments).

Deviation: the "Usage" tab-strip context-menu placement (see above) -
no existing "chat overflow/details" surface matched the ticket's
wording; this was the closest analogue.

Verification:
- Scoped test files for every new/changed component pass (chat-usage-
  dialog, epic-usage-dialog, epic-usage-window-picker, usage-chat-
  breakdown, usage-turn-drilldown, epic-shell-usage-entry-point x3
  including a real-mount proof of zero ambient fetch, tab-strip
  regression suite - 38 tests, no change in outcome).
- `bun run compile` clean after the type fixes noted in this session
  (local `UsageSummaryQueryResult` alias instead of `ReturnType<typeof
  ...>`; `DEFAULT_WINDOW_DAYS` kept separate from the widened
  `DEFAULT_WINDOW` literal).

Needs live verification (dev-desktop, not run here): entry-point
discoverability + click-to-open at real widths/both themes; epic panel
layout incl. by-chat/agent breakdown with a many-chat epic; "entire
epic" window's trend chart against a real multi-day fact span; tab-
strip context-menu item visibility/placement; chat usage dialog +
drill-down against a chat with >turnRows page size (truncation banner)
and against a genuinely unpriced turn.

Ticket: usage-analytics-plan/tickets/12-scoped-views-gui (epic 7e401ffd-fd95-4d2f-b5b0-3c416d8724c8)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f5c56c07-7f68-4bc3-a8e6-cd805d0589ce

📥 Commits

Reviewing files that changed from the base of the PR and between c67c654 and 3374cb7.

📒 Files selected for processing (1)
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-summary-panel-host-scope.test.tsx

Summary by CodeRabbit

  • New Features

    • Added a Usage dashboard in Settings with cost and token metrics, date filters, host filtering, charts, breakdowns, and retryable error states.
    • Added usage views for individual chats and epics, including per-turn details, trends, model or day grouping, and links to Settings.
    • Added Usage options to chat-tab context menus and epic status controls.
    • Added clear messaging when usage data is unavailable or unsupported.
  • Documentation

    • Updated Settings documentation with usage capabilities, pricing details, data limitations, and dashboard guidance.

Walkthrough

The change adds a versioned host.usage.summary RPC, shared usage analytics utilities and components, a Usage settings dashboard, epic and chat usage dialogs, capability-gated entry points, routing, styling, and comprehensive tests.

Changes

Usage analytics platform

Layer / File(s) Summary
Usage-summary protocol contract
protocol/src/host/usage-analytics/*, protocol/src/host/registry.ts, protocol/src/host/index.ts
Defines validated usage-summary request and response schemas and registers the optional host.usage.summary v1.0 RPC.
Analytics data and query foundation
clients/gui-app/src/hooks/usage-analytics/*, clients/gui-app/src/lib/usage-analytics/*, clients/gui-app/src/components/usage-analytics/*
Adds query construction, timezone handling, cost formatting, aggregation, chart data, controls, tables, error states, and usage visualizations.
Usage dashboard rendering
clients/gui-app/src/components/settings/*, clients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsx, clients/gui-app/src/routes/settings.usage.tsx, clients/gui-app/src/routeTree.gen.ts
Adds the account-scoped Usage settings route and epic usage dialog with configurable windows, charts, breakdowns, statistics, loading states, unavailable states, and retryable errors.
Epic and chat entry points
clients/gui-app/src/components/epic-canvas/*, clients/gui-app/src/components/chat/*, clients/gui-app/src/stores/chats/chat-usage-dialog-store.ts, clients/gui-app/src/traycer-app.tsx
Adds capability-gated epic and chat usage actions, a global chat usage dialog, and shared status-row error handling.
Behavior validation
clients/gui-app/src/**/__tests__/*, clients/gui-app/src/lib/usage-analytics/__tests__/*
Adds unit, component, and integration coverage for analytics calculations, rendering, capability negotiation, on-demand requests, error handling, and dialog state.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: protocol-compat-override

Poem

A rabbit checks the charts at night,
With tokens stacked in colors bright.
Epic clicks and chat costs show,
Unsupported hosts softly say no.
Queries wait until you choose,
Then hop through trails of usage news.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the optional host.usage.summary RPC and related usage analytics surfaces added by the changeset.
Description check ✅ Passed The description directly explains the new protocol method, GUI usage surfaces, scope behavior, pricing rules, and verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch usage-gui-buildout

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb4225a0bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/lib/usage-analytics/day-window.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 27

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@clients/gui-app/src/components/chat/chat-usage-dialog.tsx`:
- Around line 160-161: Update the usage drilldown gating in the component
containing hasUsage and turnRows to base hasUsage on turnRows.length rather than
summary.totals.factCount. Keep the existing summary.turnRows fallback to an
empty array, and ensure the Collapsible, label, and body are only shown when
rendered per-turn rows exist.

In
`@clients/gui-app/src/components/epic-canvas/__tests__/epic-shell-usage-entry-point.test.tsx`:
- Around line 275-284: Update renderShell so EpicSessionProvider receives
tabId={TAB_ID}, matching the tab ID passed to EpicShell; keep the existing
EPIC_ID values for epicId unchanged.

In
`@clients/gui-app/src/components/epic-canvas/panels/__tests__/epic-usage-dialog.test.tsx`:
- Around line 193-205: Update the test around the window switch and inspect the
second host.usage.summary invocation after clicking epic-usage-window-epic.
Assert that its request payload contains window: "epic", while preserving the
existing wait for the request to complete.

In `@clients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsx`:
- Line 66: Replace the mount-time nowMs anchor with
summary.window.endAtExclusive when constructing chart days and any related
fixed-window date calculations. Apply this consistently across the fixed-window
and epic-window branches, including the affected memoized logic, while
preserving the existing bucket and range behavior.

In
`@clients/gui-app/src/components/epic-canvas/panels/status-row-chrome-boundary.tsx`:
- Around line 26-38: Update StatusRowChromeBoundary to accept a readiness or
session reset key from the EpicShell/EpicSessionGate flow, and clear failed in
response to that key changing. Ensure the status-row child can render again
after host or session readiness changes, while preserving the existing error
capture and null fallback behavior.

In
`@clients/gui-app/src/components/usage-analytics/__tests__/epic-usage-window-picker.test.tsx`:
- Around line 13-22: Replace implementation-specific test ID queries with
accessible tab role queries: in
clients/gui-app/src/components/usage-analytics/__tests__/epic-usage-window-picker.test.tsx
lines 13-22, query the “7 days” and “Entire epic” tabs by role and visible name;
in
clients/gui-app/src/components/usage-analytics/__tests__/usage-window-picker.test.tsx
lines 16-27, query the “30 days,” “90 days,” and “7 days” tabs similarly; and in
clients/gui-app/src/components/usage-analytics/__tests__/usage-breakdown-toggle.test.tsx
line 15, query the “Day” tab by role and name.

In
`@clients/gui-app/src/components/usage-analytics/__tests__/usage-chat-breakdown.test.tsx`:
- Around line 15-18: Remove the useEpicTreeNode mock in
usage-chat-breakdown.test.tsx and use the real epic store instead. Populate the
store with the required epic tree data during each test setup, and reset or
clear that store after each test so cases remain isolated.
- Around line 49-55: Update the usage chat breakdown tests around the rendered
rows to query list items by their accessible role, then assert the expected
accessible names and ordering. Replace the existing getByText assertions in both
referenced test cases with role-based queries, without using data-testid for
these user-visible checks.

In
`@clients/gui-app/src/components/usage-analytics/__tests__/usage-daily-chart.test.tsx`:
- Around line 46-72: Update the tests using the legend-chip queries in
clients/gui-app/src/components/usage-analytics/__tests__/usage-daily-chart.test.tsx:46-72
to select the Claude and Codex controls with getByRole("button", { name:
/claude|codex/i }) instead of test IDs. Also update the Retry control query in
clients/gui-app/src/components/usage-analytics/__tests__/usage-error-card.test.tsx:20-29
to use getByRole("button", { name: "Retry" }); preserve the existing assertions
and interactions.

In
`@clients/gui-app/src/components/usage-analytics/__tests__/usage-metric-toggle.test.tsx`:
- Line 16: Replace test-id queries in usage-metric-toggle.test.tsx: use
getByRole("tab", { name: "Tokens" }) for the Radix tab trigger. In
usage-breakdown-table.test.tsx, replace the four native table-cell queries with
getByRole("cell", { name: ... }) while preserving their expected names and
assertions.

In `@clients/gui-app/src/components/usage-analytics/usage-breakdown-table.tsx`:
- Around line 60-63: Update the key expression in the rows map within the usage
breakdown table to encode harnessId and model as an unambiguous structural pair,
such as a serialized tuple, instead of joining them with a space. Preserve the
existing row rendering and ensure distinct value pairs always produce distinct
React keys.

In `@clients/gui-app/src/components/usage-analytics/usage-chat-breakdown.tsx`:
- Around line 56-58: Remove the w-16 width constraint from the cost value span
rendering formatUsd(row.knownCostUsd), while preserving its shrink, alignment,
numeric, font, and foreground styling so the formatted cost can size naturally.

In `@clients/gui-app/src/components/usage-analytics/usage-daily-chart.tsx`:
- Line 101: Define a shared CSS custom property for the chart gutter in
usage-analytics-chart.css, expressing the combined y-axis width, row gap, and
plot padding. Replace the hardcoded padding values on the y-axis-related
elements at the x-axis and legend locations, while preserving the existing
layout and aligning all three areas through that single variable.
- Around line 151-156: Update the column button in the usage daily chart to
include an accessible name via aria-label, using the imported formatDayLabel
helper with column.day; keep the existing tooltip description and decorative bar
content unchanged.

In `@clients/gui-app/src/components/usage-analytics/usage-harness-split.tsx`:
- Around line 52-75: Update the harness row layout around the label, share bar,
percentage, cost, and token spans to use fluid sizing instead of fixed
non-shrinking widths. Allow the share bar to wrap onto a full-width row at
narrow widths while keeping the cost and token values visible, and preserve the
existing labels, formatting, and bar sizing behavior.

In `@clients/gui-app/src/components/usage-analytics/usage-summary-panel.tsx`:
- Around line 136-145: Update UsageErrorCard to accept an isPending prop, render
inline AgentSpinningDots while pending, and disable Retry during refetch without
changing its label. Pass each query’s isFetching value from the usage error
handlers in
clients/gui-app/src/components/usage-analytics/usage-summary-panel.tsx (lines
136-145), clients/gui-app/src/components/chat/chat-usage-dialog.tsx (lines
141-150), and the corresponding error handler in epic-usage-dialog.tsx.

In `@clients/gui-app/src/components/usage-analytics/usage-turn-drilldown.tsx`:
- Around line 60-76: Update the row layout containing formatTurnTimestamp,
row.model, tokens, costUsd, and OutcomeBadge to use a responsive wrapping or
breakpoint-based grid instead of fixed w-32, w-16, and w-14 columns. Keep text
columns fluid with minmax(0, 1fr), allowing narrow dialog widths to wrap without
hiding values, and preserve the existing content and badge behavior.

In `@clients/gui-app/src/lib/settings-sections.ts`:
- Around line 28-29: Add "usage" to the SETTINGS_PATHS allowlists used by tab
validation and desktop-tab restoration, updating the corresponding allowlist
definitions in the tabs store and desktop-tabs persistence flow. Preserve all
existing settings paths.

In `@clients/gui-app/src/lib/usage-analytics/__tests__/day-window.test.ts`:
- Around line 33-42: Update the “never returns more entries” test around
lastNCalendarDays to use a fall-back DST instant and assert exactly 10 results,
with unique ordered calendar dates covering every expected New York local date
in the requested window. Replace the upper-bound-only assertion while retaining
the uniqueness check.

In `@clients/gui-app/src/lib/usage-analytics/day-window.ts`:
- Around line 26-34: Update the date-generation logic around formatter and the
days loop to derive the starting local YYYY-MM-DD date, then decrement its
calendar date fields one day at a time rather than subtracting dayMs elapsed
milliseconds. Preserve the existing window ordering, deduplication via seen, and
days output consumed by the chart axis; remove the 24-hour millisecond
calculation from this operation.

In `@clients/gui-app/src/lib/usage-analytics/format-metric-value.ts`:
- Around line 55-62: Update formatDateRangeLabel to extract the year from both
first and last endpoints; when the years differ, include each year in the
rendered range so cross-New-Year ranges identify both dates correctly, while
preserving the existing same-year formatting.

In `@clients/gui-app/src/lib/usage-analytics/usage-series-scale.ts`:
- Around line 53-65: Update the comparator in harnessIdsByFirstAppearance to
sort by day first and use harnessId as a deterministic tie-breaker when days are
equal, preserving earliest-day ordering while making same-day series order
independent of input order.

In `@protocol/src/host/usage-analytics/schemas.ts`:
- Around line 116-117: Update the schema definition for turnRows to enforce a
maximum array length of USAGE_TURN_ROWS_MAX, reusing the exported constant
rather than duplicating its numeric value. Apply the same constraint to the
additional turnRows schema occurrence noted in the review, while preserving the
existing row validation.
- Around line 40-41: Update the day field in usageSummaryBucketSchema to
validate a strict ISO calendar-day value in YYYY-MM-DD format, rejecting
shortened or arbitrary strings while preserving valid dates used by grouping and
charting.
- Around line 54-59: Update usageSummaryWindowBoundsSchema to add an
object-level validation requiring endAtExclusive to be greater than or equal to
startAtInclusive, while preserving the existing individual field validations and
reporting the constraint through the schema validation mechanism.
- Around line 158-159: Update hostUsageSummaryRequestSchemaV10 to use
z.strictObject() instead of chaining .strict() on z.object(), preserving the
existing schema fields and validation behavior.
- Line 160: Define a shared timezone schema that validates IANA timezone
identifiers, then replace the current z.string() timezone definitions in both
request and response schemas with it. Ensure invalid values such as "Not/AZone"
are rejected before lastNCalendarDays passes them to Intl.DateTimeFormat.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e3fc11ae-f169-41a6-b115-b5e02aa33f98

📥 Commits

Reviewing files that changed from the base of the PR and between 6367d3a and eb4225a.

📒 Files selected for processing (78)
  • clients/gui-app/src/components/chat/__tests__/chat-usage-dialog.test.tsx
  • clients/gui-app/src/components/chat/chat-usage-dialog.tsx
  • clients/gui-app/src/components/epic-canvas/__tests__/epic-shell-usage-entry-point.test.tsx
  • clients/gui-app/src/components/epic-canvas/canvas/tab-strip-context-menu.tsx
  • clients/gui-app/src/components/epic-canvas/canvas/tab-strip.tsx
  • clients/gui-app/src/components/epic-canvas/epic-shell.tsx
  • clients/gui-app/src/components/epic-canvas/panels/__tests__/epic-usage-dialog.test.tsx
  • clients/gui-app/src/components/epic-canvas/panels/epic-sweep-action.tsx
  • clients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsx
  • clients/gui-app/src/components/epic-canvas/panels/epic-usage-entry-point.tsx
  • clients/gui-app/src/components/epic-canvas/panels/status-row-chrome-boundary.tsx
  • clients/gui-app/src/components/layout/dialogs/desktop/report-issue-dialog.tsx
  • clients/gui-app/src/components/settings/SETTINGS.md
  • clients/gui-app/src/components/settings/panels/__tests__/usage-settings-panel.test.tsx
  • clients/gui-app/src/components/settings/panels/usage-settings-panel.tsx
  • clients/gui-app/src/components/settings/settings-modal-content.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/epic-usage-window-picker.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-breakdown-table.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-breakdown-toggle.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-chat-breakdown.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-cost-figure.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-daily-chart.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-day-breakdown-table.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-error-card.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-metric-toggle.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-turn-drilldown.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-window-picker.test.tsx
  • clients/gui-app/src/components/usage-analytics/epic-usage-window-picker.tsx
  • clients/gui-app/src/components/usage-analytics/usage-breakdown-table.tsx
  • clients/gui-app/src/components/usage-analytics/usage-breakdown-toggle.tsx
  • clients/gui-app/src/components/usage-analytics/usage-chat-breakdown.tsx
  • clients/gui-app/src/components/usage-analytics/usage-cost-figure.tsx
  • clients/gui-app/src/components/usage-analytics/usage-cost-quality-panel.tsx
  • clients/gui-app/src/components/usage-analytics/usage-daily-chart.tsx
  • clients/gui-app/src/components/usage-analytics/usage-day-breakdown-table.tsx
  • clients/gui-app/src/components/usage-analytics/usage-error-card.tsx
  • clients/gui-app/src/components/usage-analytics/usage-harness-split.tsx
  • clients/gui-app/src/components/usage-analytics/usage-metric-toggle.tsx
  • clients/gui-app/src/components/usage-analytics/usage-stat-tiles.tsx
  • clients/gui-app/src/components/usage-analytics/usage-summary-panel.tsx
  • clients/gui-app/src/components/usage-analytics/usage-turn-drilldown.tsx
  • clients/gui-app/src/components/usage-analytics/usage-window-picker.tsx
  • clients/gui-app/src/hooks/usage-analytics/use-usage-summary-query.ts
  • clients/gui-app/src/hooks/usage-analytics/use-usage-summary-support.ts
  • clients/gui-app/src/index.css
  • clients/gui-app/src/lib/analytics.ts
  • clients/gui-app/src/lib/host-rpc-policy/host-method-policy-table.ts
  • clients/gui-app/src/lib/query-keys/host-query-keys.ts
  • clients/gui-app/src/lib/query-keys/index.ts
  • clients/gui-app/src/lib/settings-sections.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/cost-format.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/day-window.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/format-metric-value.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/usage-breakdown.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/usage-chart-data.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/usage-harness-split.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/usage-series-scale.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/usage-stat-tiles.test.ts
  • clients/gui-app/src/lib/usage-analytics/cost-format.ts
  • clients/gui-app/src/lib/usage-analytics/day-window.ts
  • clients/gui-app/src/lib/usage-analytics/format-metric-value.ts
  • clients/gui-app/src/lib/usage-analytics/usage-breakdown.ts
  • clients/gui-app/src/lib/usage-analytics/usage-chart-data.ts
  • clients/gui-app/src/lib/usage-analytics/usage-harness-split.ts
  • clients/gui-app/src/lib/usage-analytics/usage-series-scale.ts
  • clients/gui-app/src/lib/usage-analytics/usage-stat-tiles.ts
  • clients/gui-app/src/lib/usage-analytics/viewer-timezone.ts
  • clients/gui-app/src/routeTree.gen.ts
  • clients/gui-app/src/routes/settings.usage.tsx
  • clients/gui-app/src/stores/chats/chat-usage-dialog-store.ts
  • clients/gui-app/src/stores/tabs/kinds/settings.tsx
  • clients/gui-app/src/styles/usage-analytics-chart.css
  • clients/gui-app/src/traycer-app.tsx
  • protocol/src/host/index.ts
  • protocol/src/host/registry.ts
  • protocol/src/host/usage-analytics/contracts.ts
  • protocol/src/host/usage-analytics/index.ts
  • protocol/src/host/usage-analytics/schemas.ts

Comment thread clients/gui-app/src/components/chat/chat-usage-dialog.tsx Outdated
Comment thread clients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsx Outdated
Comment thread protocol/src/host/usage-analytics/schemas.ts Outdated
Comment thread protocol/src/host/usage-analytics/schemas.ts
Comment thread protocol/src/host/usage-analytics/schemas.ts
Comment thread protocol/src/host/usage-analytics/schemas.ts
Comment thread protocol/src/host/usage-analytics/schemas.ts
The ticket 11/12 worktree had no pre-commit hooks installed, so these files
were committed unformatted and would fail CI's format check. No behavior
change - line wrapping only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 771f42646a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsx Outdated
Comment thread clients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsx Outdated
Prettier's canonical output for this file; the prior formatting commit
captured a non-converged intermediate (nx cache masked the second pass),
which failed CI's format check.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: debcee808e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/lib/usage-analytics/cost-format.ts Outdated
Comment thread clients/gui-app/src/components/usage-analytics/usage-chat-breakdown.tsx Outdated
Comment thread clients/gui-app/src/components/usage-analytics/usage-stat-tiles.tsx
tanveergill and others added 6 commits August 10, 2026 14:22
User ruling 2026-08-10 (three rounds, final): the provenance vocabulary
and standing disclaimers were too verbose - match t3code's density.
Implements the pricing-provenance artifact's amended "Product framing"
section exactly, across the dashboard AND both ticket-12 scoped
dialogs (epic + chat), which all route through the same
`UsageCostFigure`.

Headline + footnote (`cost-format.ts`):
- `describeCostCoverage`/`FULL_RATE_QUALIFIER`/`CostCoverageText`
  replaced by `describeCostHeadline`, which returns the dollar amount
  with its own trailing asterisk (`$X.XX*`) plus a footnote that is
  ALWAYS exactly "* if billed at full API rate" - the "priced
  subtotal" / "+ N unpriced turns" standing phrasing is gone.
- The ONE exception, "· N turns not counted", is appended to the
  footnote only while `coverage.unpricedFactCount > 0` - the single
  place coverage gaps still claim standing pixels.

Tooltip (`usageCostTooltip`, new):
- Everything that used to be standing text now lives in a tooltip on
  the figure/asterisk: the estimate-at-list-prices framing, that a
  subscription bills separately, the exact-vs-estimate split with
  amounts (from `totals.provenanceSplit`), and the not-counted detail.
  Plain English throughout - the words "provenance", "modeled", and
  "unpriced" never appear (enforced by a dedicated test).

`UsageCostFigure` (`usage-cost-figure.tsx`) stays the single owner:
rewired to `describeCostHeadline`/`usageCostTooltip`, wraps the figure
in `TooltipWrapper`. The trigger is a real `<button type="button">`
(no `onClick` - focus alone is its job) rather than `tabIndex` on a
`<span>`, matching this codebase's established pattern
(`pr-source-notice.tsx`) for `jsx-a11y/no-noninteractive-tabindex` -
a keyboard/AT user can reach the explanation without a mouse. Falls
back to a plain `<span>` when there's no usage (nothing to explain,
no tooltip, no unnecessary focus stop).

Deletions:
- `usage-cost-quality-panel.tsx` deleted outright (dashboard's %-by-
  provenance-rung panel + cache-savings sub-line - the cache-savings
  figure is already one of ticket 11's stat tiles, so nothing is lost).
- Both breakdown tables (`usage-breakdown-table.tsx`,
  `usage-day-breakdown-table.tsx`) lose their Provenance column and
  `ProvenanceBadge` - cost and tokens stay directly reachable without
  hovering, per that table's own existing "dataviz relief channel"
  role, just without the badge.
- The scoped dialogs (`epic-usage-dialog.tsx`, `chat-usage-dialog.tsx`)
  needed no direct changes - both already route every dollar figure
  through `UsageCostFigure` and never rendered a provenance chip of
  their own (the chat drill-down's per-turn rows only ever carried an
  outcome badge, not a provenance one - already compliant).

Out of scope (per the fixup ticket): wire/summary changes - the
`provenanceSplit` field stays on `UsageSummaryTotals` for the tooltip;
the per-row `provenance` field on `UsageBreakdownRow`/
`UsageDayBreakdownRow`/`UsageHarnessSplitRow` and the
`weakestProvenance`/`PROVENANCE_RANK` helpers that compute it are left
in place (unused by rendering now, but out of scope - a data-layer
change beyond the ticket's listed removals); `usageCompletenessAbsentNote`
("Excludes N turns with no usage reported") is untouched - a usage-
completeness signal (turns with NO data at all), not a cost-provenance
one, and not named anywhere in the ticket or the amended artifact
section.

Docs: SETTINGS.md - reworded the stale "priced subtotal + N unpriced
turns" description, removed the cost-quality-panel mention from the
ticket 11 bullet, and added a dedicated fixup-01 bullet pointing at
`usage-cost-figure.tsx`'s own doc comment as the one place the current
rule is described.

Ticket: usage-analytics-plan/tickets/11-dashboard-buildout-gui/fixup-01-cost-presentation (epic 7e401ffd-fd95-4d2f-b5b0-3c416d8724c8)

Verification:
- `bunx vitest run` scoped to every touched file (10 files, 48 tests):
  pass, twice.
- `bun run compile` (root of `traycer/`, all 5 workspaces): exit 0.
- `bun run lint` (same scope, `--max-warnings 0`): exit 0. Caught and
  fixed two real issues on the first pass: `no-noninteractive-tabindex`
  on the original span+tabIndex trigger (see the button rationale
  above), and an unnecessary optional chain in the new tooltip test.
- Full `bun run test` (gui-app, unscoped): attempted three times in the
  background; each run was killed before completion by what appears to
  be a background-shell-survival issue in this session (no output, no
  surviving process, no completion record - not a test failure). Not
  re-attempted a fourth time. Mitigating evidence: every touched file's
  scoped suite is green, full lint and full compile are clean, and the
  full unscoped suite passed cleanly on this exact worktree immediately
  before this commit (tickets 11+12, same session) with only 6 known
  pre-existing failures in `reading-position/service.test.ts` - a file
  untouched by any commit in this branch, reproducing identically in
  isolation. Flagging for the coordinator to re-run if a definitive
  full-suite pass is wanted before landing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
…ness, a11y, wire constraints

Triage of the 33 open CodeRabbit/Codex threads on #1102.

Correctness:
- lastNCalendarDays walked a fixed 24h stride, so a local DST day (23h or
  25h) either skipped a calendar date or mapped two samples onto one. A real
  10-day America/New_York window ending 2026-11-05 returned 9 columns,
  silently dropping its oldest day while that day's usage still counted in
  the totals. Resolve the anchor to a local date once, then decrement
  calendar fields.
- lastNCalendarDays now degrades to UTC instead of throwing when the zone is
  one the runtime does not know; the response timezone is only length-checked
  on the wire, and a RangeError mid-render took the whole dialog down.
- EpicUsageDialog anchored its chart on a Date.now() sampled at mount, and
  the dialog stays mounted with its EpicShell while closed - opening it after
  a local midnight built columns ending on the previous day and dropped the
  newest buckets. Anchor both window kinds on the response's own
  window.endAtExclusive - 1 (the last instant the window includes).
- formatDateRangeLabel read the year off the last day only, stamping the
  wrong year on the first endpoint of any window straddling New Year.
- formatUsd rounded a real sub-half-cent amount to "$0.00", showing billable
  usage as free; it now reads "<$0.01", matching the chat cost row's existing
  "<$0.0001" treatment. A true zero still formats as "$0.00".
- /settings/usage was missing from both SETTINGS_PATHS allowlists, so tab
  validation and desktop-tab restoration rejected the route the epic dialog's
  "View full usage" hands off to.
- The chat drilldown gated on totals.factCount while rendering turnRows,
  offering "Show 0 turns" onto an empty body when the wire's nullable
  turnRows is absent.
- harnessIdsByFirstAppearance had no same-day tie-breaker, so color slots
  depended on host response ordering.
- The breakdown table's space-joined row key collided across different
  harness/model pairs.

Accessibility:
- The daily chart's column trigger contained only decorative bars, leaving
  the button with no accessible name; its tooltip is wired as a description,
  which is announced inconsistently without one.

Layout:
- The turn drilldown's fixed column widths overran the narrow
  w-[min(92vw,32rem)] dialog; columns now keep their aligned widths while
  there is room and wrap instead of hiding values.

Wire (host.usage.summary is optional and unreleased):
- bucket.day is now shaped YYYY-MM-DD rather than any 1-10 char string.
- turnRows is bound to the exported USAGE_TURN_ROWS_MAX, which the file
  documented as the cap but no schema applied.

Tests: DST coverage for both transitions plus the unknown-zone fallback;
epic-window switch now asserts the request payload rather than only that a
request happened; sub-cent formatting; test-id queries converted to Testing
Library role queries per clients/gui-app/AGENTS.md; the usage entry-point
test's session provider now shares the shell's tabId.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee409d3ec9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/components/usage-analytics/usage-summary-panel.tsx Outdated
…w too

Follow-up to ee409d3, which fixed the mount-time chart anchor in
EpicUsageDialog but left the same defect in UsageSummaryPanel - the Settings
usage dashboard.

`nowMs` was captured once in a lazy `useState` initializer and fed both the
chart's x-axis and the date-range label. The panel outlives a local midnight
easily, and the query has several refetch triggers that survive one: window
switch, Retry, host reconnect, window refocus. Any of those could return
buckets for the new day against an axis still ending on the old one, so the
newest bucket vanished from the chart while remaining in the totals, and the
range label sat a day behind the data beside it.

`daysForResponse` now derives both from `summary.window` -
`endAtExclusive - 1` (the last instant the window includes) and the
response's own `timezone`, matching the rule EpicUsageDialog already uses.
With no response there is no range to describe, so the axis is empty and the
label renders nothing; the body is showing its loading or error state in that
same pass.

Deriving the zone from the response also drops the last `getViewerTimeZone()`
read on the render path here - the axis and the data it plots now agree on
one zone by construction rather than by the request and response happening to
carry the same one.

The panel test's window fixture was `startAtInclusive: 0, endAtExclusive: 1`,
which no resolver emits; it is now a real 30-day window ending 2026-08-09,
and the test asserts the label reads "Jul 11 - Aug 9, 2026" rather than
merely that the element exists. Ablation-checked: restoring the mount-time
anchor turns that assertion red ("Jul 12 - Aug 10, 2026").

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 141dae09b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/components/usage-analytics/usage-harness-split.tsx Outdated
Comment thread clients/gui-app/src/components/settings/panels/usage-settings-panel.tsx Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99992c0562

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/components/usage-analytics/usage-daily-chart.tsx Outdated
…ader

Two follow-up findings on PR #1102.

usage-harness-split.tsx - the row's fixed columns (w-24, w-14, two w-16, the
min-w-8 bar and five gap-3 gaps) came to ~380px of non-shrinking content, and
its container SettingsModalContent is overflow-x-hidden. Below that width the
cost and token values were clipped outright rather than left scrollable-to.

The desktop shell's 960px minimum window does leave slack at 100% text size
(~520px of content pane against ~380px of row), so this is not reachable by
resizing the app alone - but rem-based columns outgrow a px-sized pane under
browser text-only zoom or an OS font-scaling setting, and silently clipping
the cost figure under accessibility scaling is precisely what the
fluid-sizing rule in clients/gui-app/AGENTS.md exists to prevent. The row now
wraps, the label shrinks and truncates, and the value columns hold their
aligned widths as minimums.

An earlier round declined the same claim because the reviewer had named
EpicUsageDialog, where this component is not rendered. That refutation was
correct about the surface and wrong to stop there.

usage-settings-panel.tsx - the header asserted usage "on this host"
unconditionally, but scope is a property of the response: servedBy "cloud"
spans every device on the account. That made the header a standing false
claim for every cloud-served read, and nothing corrected it, since
servedByScopeNote is deliberately null for cloud (an account-wide total is
what the reader expects there) and only speaks up for "local". The header is
now scope-neutral, leaving that helper as the single place scope is asserted.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
…t, not the Host-group pick

Ticket 13 moved this section from the HOST group to ACCOUNT, but it kept
resolving both its transport and its capability gate through `useHostScope`
- the Settings sidebar's Host-group picker, whose pick is remembered across
sections.

So picking an unreachable or since-removed host under the Host group and then
opening Usage under Account left `HostScopeGate` refusing to render the
dashboard, behind a notice about a host that section never needed. The active
host could serve the account-wide request perfectly well, and the page's own
default is "All hosts" - nothing about it varies by the picked host.
`group: "account"` in `settings-sections.ts` is precisely the statement that
this section is not host-scoped; the gate exists for the ones that are. The
panel is not inside a tab, so the tab-host rule does not apply either - this
is the app-wide surface case, and `useReactiveActiveHostId()`/`useHostClient()`
is the pattern for it.

`useHostScope` stays, but only as the NAME DIRECTORY. `scope.hosts` is the
merged directory-plus-registry host model, which is what turns the host ids in
the summary into names for the in-page filter and the by-host breakdown. No
client, no status, no gating is taken from it any more.

The capability check moves to `useHostMethodSupport`, which distinguishes "the
host answered and does not have the method" from "no handshake has completed
yet". The boolean wrapper collapses both into false, which under the active
host would have flashed "Usage isn't available on <host> yet" during a cold
start - a claim about the host that is merely unverified.

Ablation-checked: restoring either half of the old wiring (the scope's client
or the scope's host id for the capability check) turns all three new tests
red.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2d27a77d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/lib/analytics.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b971cc30f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ices

`ANALYTICS_SETTINGS_SECTIONS` is the runtime allowlist `sanitizeAnalyticsProperties`
validates `section` against, and it had drifted from the
`AnalyticsSettingsSection` union it is supposed to mirror: 10 entries against
the union's 12. Both `usage` (added by this PR) and `devices` were missing, so
`Analytics.track` returned `false` and dropped every navigation event for those
two sections - no type error, no runtime error, no telemetry.

The set is now built from an object literal constrained by
`satisfies Record<AnalyticsSettingsSection, true>`, so a section added to the
union without being listed here is a COMPILE error rather than a silent
telemetry hole. That is the point of the change: a bare `Set<string>` beside a
union is a seam that can only fail quietly, and fixing the two missing strings
without closing the seam would have left the next one to be found the same way.

`devices` is not this PR's regression, but it is the same one-word defect in
the list being corrected, demonstrably dropping events today.

Both gates verified by ablation: removing `usage` from the record fails
`bun run compile`, and reverting to the old bare list turns the new test red
naming exactly `['devices', 'usage']`.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c8f8f1d3a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/components/usage-analytics/usage-summary-panel.tsx Outdated
…host is active

Follow-up to e2d27a7, which is where this regressed. Moving the panel off
`HostScopeGate` also removed that gate's "No hosts yet" branch, and the
capability-pending state took its place: with `activeHostId === null` there is
no host to hand shake with, so `useHostMethodSupport` stays `null` forever and
the panel showed "Loading usage…" indefinitely.

That state is reachable, not theoretical - the section is
`requiresLocalHost: false`, so it stays navigable on a fresh install and after
every host is removed.

The no-active-host case is now checked BEFORE the pending branch and renders
an actionable notice. It is gated on the host lists having settled
(`scope.isLoading`), so the ordinary hydration window where the active host is
briefly null still shows the spinner rather than flashing "No host connected"
at someone who has one.

`UsageUnsupportedNotice` becomes a `title`/`detail`/`testId` `UsageNotice`, so
both terminal states share one shape instead of growing a second near-copy.

Ablation-checked: disabling the new branch turns the added test red.
Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review


P2 Badge Escape the key separator instead of embedding a NUL byte

This template literal contains a literal NUL byte in the TypeScript source. Git consequently classifies the entire module as binary (git diff --numstat reports - -), hiding future textual diffs and preventing normal line-based merge handling for an otherwise ordinary source file. Use an escaped separator such as \0/\u0000, or a structured key, so the runtime key remains distinct without making the source binary.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread clients/gui-app/src/components/usage-analytics/epic-usage-window-picker.tsx Outdated
…wing to one

The shared aggregator applies the `hostId` filter to the FACTS before it
groups them, so a filtered response's `hostBuckets` names only the host that
was asked for. The picker rebuilt its options from the current response, the
directory and the selection - so choosing a host collapsed the list to that
host plus whatever the directory could name.

For hosts the directory does not carry - removed or currently undialable
machines the account still has usage for - that meant no way to move from one
straight to another: back to All hosts, wait for a round trip, then pick.

The panel now remembers the host ids from the last UNFILTERED response and
unions them into the options. Only an unfiltered response is evidence about
hosts other than the selected one, so only that one updates the set; it is a
wholesale replace rather than an accumulator, so a host that stops appearing
drops out on the next All-hosts read instead of lingering forever.

Ablation-checked: feeding the options the current response alone turns the new
test red on the host that is absent from both the filtered response and the
directory.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 11, 2026
…ping

`TabsList` is `inline-flex w-fit` with a fixed `h-8` and `whitespace-nowrap`
triggers, and this picker carries the widest option set any of them has -
7 / 30 / 90 days plus "Entire epic". Its dialog is
`w-[min(92vw,32rem)] overflow-hidden`, so under increased text scaling the row
outgrows the box and clips the trailing option out of reach: a lost option,
not merely a cramped row.

Wrapping needs the height to follow, since the primitive pins `h-8` through a
`group-data-horizontal/tabs` variant that would otherwise cut the second row
off - hence the matching `group-data-horizontal/tabs:h-auto` rather than a
bare `h-auto`.

Third instance of the same fluid-sizing rule in this PR, after the turn
drilldown and the harness split, and the same trigger as the harness split:
the desktop shell's 960px minimum leaves room at 100% text size, but rem-sized
content inside a px-capped surface does not.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
…line) into usage-gui-buildout

Internal development@96f5d974c requires the heldChain/retryDeadlineStartedAt
protocol fields from this line; the usage-analytics train requires this
branch's host.usage.summary work. This merge lets the internal train pin one
commit containing both.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@clients/gui-app/src/components/usage-analytics/__tests__/usage-summary-panel-host-scope.test.tsx`:
- Around line 270-279: Extend the test around the host selection flow to inspect
the final captured host.usage.summary request and assert that its request
parameters include hostId set to "host-a", while preserving the existing picker
text assertion. Use the test’s existing request-capture mechanism rather than
adding a separate mock or transport.

In `@clients/gui-app/src/components/usage-analytics/usage-host-split.tsx`:
- Around line 54-82: Replace the fixed width and minimum-width classes in the
usage split row with fluid flex sizing and responsive width caps: update the
host label span, cost bar, percentage, cost, and token columns around the
visible row markup. Preserve truncation, alignment, and bar behavior while
ensuring the layout can shrink cleanly in narrow Settings surfaces without fixed
px/rem layout dimensions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cda5ff28-b405-4756-800e-44f0247d324a

📥 Commits

Reviewing files that changed from the base of the PR and between 7028000 and c67c654.

📒 Files selected for processing (31)
  • clients/gui-app/src/components/chat/__tests__/chat-usage-dialog.test.tsx
  • clients/gui-app/src/components/chat/chat-usage-dialog.tsx
  • clients/gui-app/src/components/epic-canvas/__tests__/epic-shell-usage-entry-point.test.tsx
  • clients/gui-app/src/components/epic-canvas/panels/__tests__/epic-usage-dialog.test.tsx
  • clients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsx
  • clients/gui-app/src/components/settings/SETTINGS.md
  • clients/gui-app/src/components/settings/panels/__tests__/usage-settings-panel-account-scope.test.tsx
  • clients/gui-app/src/components/settings/panels/__tests__/usage-settings-panel.test.tsx
  • clients/gui-app/src/components/settings/panels/usage-settings-panel.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-cost-figure.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-day-breakdown-table.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-error-card.test.tsx
  • clients/gui-app/src/components/usage-analytics/__tests__/usage-summary-panel-host-scope.test.tsx
  • clients/gui-app/src/components/usage-analytics/epic-usage-window-picker.tsx
  • clients/gui-app/src/components/usage-analytics/usage-breakdown-table.tsx
  • clients/gui-app/src/components/usage-analytics/usage-cost-figure.tsx
  • clients/gui-app/src/components/usage-analytics/usage-error-card.tsx
  • clients/gui-app/src/components/usage-analytics/usage-host-filter.tsx
  • clients/gui-app/src/components/usage-analytics/usage-host-split.tsx
  • clients/gui-app/src/components/usage-analytics/usage-summary-panel.tsx
  • clients/gui-app/src/hooks/usage-analytics/use-usage-summary-query.ts
  • clients/gui-app/src/lib/__tests__/analytics.test.ts
  • clients/gui-app/src/lib/analytics.ts
  • clients/gui-app/src/lib/settings-sections.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/cost-format.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/format-metric-value.test.ts
  • clients/gui-app/src/lib/usage-analytics/__tests__/usage-host-split.test.ts
  • clients/gui-app/src/lib/usage-analytics/cost-format.ts
  • clients/gui-app/src/lib/usage-analytics/usage-host-split.ts
  • clients/gui-app/src/styles/usage-analytics-chart.css
  • protocol/src/host/usage-analytics/schemas.ts

Comment thread clients/gui-app/src/components/usage-analytics/usage-host-split.tsx
The picker updating locally is not proof - the follow-up request must
carry hostId. Post-freeze review thread on #1102.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
@tanveergill
tanveergill merged commit e810d6c into main Aug 11, 2026
20 checks passed
@tanveergill
tanveergill deleted the usage-gui-buildout branch August 11, 2026 06:52
hdkshingala added a commit that referenced this pull request Aug 11, 2026
Development's own new pin. Six OSS commits; two files conflicted and both were the same shape - one import added on each side at the same line, both kept:

  - protocol/src/host/registry.ts: this branch's `hostRebindLocalStoreV10` beside main's `hostUsageSummaryV10` (#1102 usage analytics).
  - clients/gui-app/.../epic-shell.tsx: `EpicDurabilityBadge` beside `EpicUsageEntryPoint`; both were already rendered in the auto-merged JSX, so dropping either would have left a used-but-unimported symbol.

Verified: bun run compile green across the OSS graph (5 projects).
Signed-off-by: Hardik Shingala <hardik@traycer.ai>
tanveergill added a commit that referenced this pull request Aug 13, 2026
…is, by-chat table (#1115)

Dashboard feedback round on the usage surfaces (follow-up to #1102):

- **ECharts stacked area chart** replaces the hand-rolled stacked bars
on the usage daily chart (Settings dashboard + epic Usage dialog).
Tree-shaken `echarts` (core + LineChart + Grid/Tooltip components +
**SVG renderer**). The SVG renderer is load-bearing: the harness palette
reaches the chart as `var(--usage-series-N)` strings that Chromium
resolves against the `.usage-chart-root` scoped palette (and its `.dark`
override), so live theme switching needs no resolve-at-mount plumbing —
verified against zrender 6.1's SVG path, which writes fill/stroke
strings into DOM attributes verbatim.
- **Readable x-axis**: the old axis gave each day label exactly one
column-width (`flex-1 truncate`), clipping every label past ~10 days to
"Jul …". ECharts thins labels (`hideOverlap`) instead of truncating.
- **Legend semantics preserved**: the chip filter still zeroes hidden
series in place (slots/colors never shift), tested end-to-end against
the option the chart instance actually receives.
- **By-chat/agent breakdown is now a real table** (Chat / agent, Tokens,
Cost) matching the other breakdown tables' styling.
- Tests: pure option-builder unit suite; jsdom suites run against a
recording `echarts/core` mock registered in the shared setup file
(zrender needs a real canvas for text measurement).

No wire changes; no protocol changes. `echarts@6.1.0` added via the root
catalog.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant