feat: usage analytics — optional host.usage.summary RPC + usage surfaces - #1102
Conversation
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>
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Summary by CodeRabbit
WalkthroughThe change adds a versioned ChangesUsage analytics platform
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
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
📒 Files selected for processing (78)
clients/gui-app/src/components/chat/__tests__/chat-usage-dialog.test.tsxclients/gui-app/src/components/chat/chat-usage-dialog.tsxclients/gui-app/src/components/epic-canvas/__tests__/epic-shell-usage-entry-point.test.tsxclients/gui-app/src/components/epic-canvas/canvas/tab-strip-context-menu.tsxclients/gui-app/src/components/epic-canvas/canvas/tab-strip.tsxclients/gui-app/src/components/epic-canvas/epic-shell.tsxclients/gui-app/src/components/epic-canvas/panels/__tests__/epic-usage-dialog.test.tsxclients/gui-app/src/components/epic-canvas/panels/epic-sweep-action.tsxclients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsxclients/gui-app/src/components/epic-canvas/panels/epic-usage-entry-point.tsxclients/gui-app/src/components/epic-canvas/panels/status-row-chrome-boundary.tsxclients/gui-app/src/components/layout/dialogs/desktop/report-issue-dialog.tsxclients/gui-app/src/components/settings/SETTINGS.mdclients/gui-app/src/components/settings/panels/__tests__/usage-settings-panel.test.tsxclients/gui-app/src/components/settings/panels/usage-settings-panel.tsxclients/gui-app/src/components/settings/settings-modal-content.tsxclients/gui-app/src/components/usage-analytics/__tests__/epic-usage-window-picker.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-breakdown-table.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-breakdown-toggle.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-chat-breakdown.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-cost-figure.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-daily-chart.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-day-breakdown-table.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-error-card.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-metric-toggle.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-turn-drilldown.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-window-picker.test.tsxclients/gui-app/src/components/usage-analytics/epic-usage-window-picker.tsxclients/gui-app/src/components/usage-analytics/usage-breakdown-table.tsxclients/gui-app/src/components/usage-analytics/usage-breakdown-toggle.tsxclients/gui-app/src/components/usage-analytics/usage-chat-breakdown.tsxclients/gui-app/src/components/usage-analytics/usage-cost-figure.tsxclients/gui-app/src/components/usage-analytics/usage-cost-quality-panel.tsxclients/gui-app/src/components/usage-analytics/usage-daily-chart.tsxclients/gui-app/src/components/usage-analytics/usage-day-breakdown-table.tsxclients/gui-app/src/components/usage-analytics/usage-error-card.tsxclients/gui-app/src/components/usage-analytics/usage-harness-split.tsxclients/gui-app/src/components/usage-analytics/usage-metric-toggle.tsxclients/gui-app/src/components/usage-analytics/usage-stat-tiles.tsxclients/gui-app/src/components/usage-analytics/usage-summary-panel.tsxclients/gui-app/src/components/usage-analytics/usage-turn-drilldown.tsxclients/gui-app/src/components/usage-analytics/usage-window-picker.tsxclients/gui-app/src/hooks/usage-analytics/use-usage-summary-query.tsclients/gui-app/src/hooks/usage-analytics/use-usage-summary-support.tsclients/gui-app/src/index.cssclients/gui-app/src/lib/analytics.tsclients/gui-app/src/lib/host-rpc-policy/host-method-policy-table.tsclients/gui-app/src/lib/query-keys/host-query-keys.tsclients/gui-app/src/lib/query-keys/index.tsclients/gui-app/src/lib/settings-sections.tsclients/gui-app/src/lib/usage-analytics/__tests__/cost-format.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/day-window.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/format-metric-value.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/usage-breakdown.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/usage-chart-data.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/usage-harness-split.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/usage-series-scale.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/usage-stat-tiles.test.tsclients/gui-app/src/lib/usage-analytics/cost-format.tsclients/gui-app/src/lib/usage-analytics/day-window.tsclients/gui-app/src/lib/usage-analytics/format-metric-value.tsclients/gui-app/src/lib/usage-analytics/usage-breakdown.tsclients/gui-app/src/lib/usage-analytics/usage-chart-data.tsclients/gui-app/src/lib/usage-analytics/usage-harness-split.tsclients/gui-app/src/lib/usage-analytics/usage-series-scale.tsclients/gui-app/src/lib/usage-analytics/usage-stat-tiles.tsclients/gui-app/src/lib/usage-analytics/viewer-timezone.tsclients/gui-app/src/routeTree.gen.tsclients/gui-app/src/routes/settings.usage.tsxclients/gui-app/src/stores/chats/chat-usage-dialog-store.tsclients/gui-app/src/stores/tabs/kinds/settings.tsxclients/gui-app/src/styles/usage-analytics-chart.cssclients/gui-app/src/traycer-app.tsxprotocol/src/host/index.tsprotocol/src/host/registry.tsprotocol/src/host/usage-analytics/contracts.tsprotocol/src/host/usage-analytics/index.tsprotocol/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>
There was a problem hiding this comment.
💡 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".
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>
There was a problem hiding this comment.
💡 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".
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>
…ke-retry-deadline
…er into usage-gui-buildout
🤖 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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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>
There was a problem hiding this comment.
💡 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".
…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>
There was a problem hiding this comment.
💡 Codex Review
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".
…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>
…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>
There was a problem hiding this comment.
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
📒 Files selected for processing (31)
clients/gui-app/src/components/chat/__tests__/chat-usage-dialog.test.tsxclients/gui-app/src/components/chat/chat-usage-dialog.tsxclients/gui-app/src/components/epic-canvas/__tests__/epic-shell-usage-entry-point.test.tsxclients/gui-app/src/components/epic-canvas/panels/__tests__/epic-usage-dialog.test.tsxclients/gui-app/src/components/epic-canvas/panels/epic-usage-dialog.tsxclients/gui-app/src/components/settings/SETTINGS.mdclients/gui-app/src/components/settings/panels/__tests__/usage-settings-panel-account-scope.test.tsxclients/gui-app/src/components/settings/panels/__tests__/usage-settings-panel.test.tsxclients/gui-app/src/components/settings/panels/usage-settings-panel.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-cost-figure.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-day-breakdown-table.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-error-card.test.tsxclients/gui-app/src/components/usage-analytics/__tests__/usage-summary-panel-host-scope.test.tsxclients/gui-app/src/components/usage-analytics/epic-usage-window-picker.tsxclients/gui-app/src/components/usage-analytics/usage-breakdown-table.tsxclients/gui-app/src/components/usage-analytics/usage-cost-figure.tsxclients/gui-app/src/components/usage-analytics/usage-error-card.tsxclients/gui-app/src/components/usage-analytics/usage-host-filter.tsxclients/gui-app/src/components/usage-analytics/usage-host-split.tsxclients/gui-app/src/components/usage-analytics/usage-summary-panel.tsxclients/gui-app/src/hooks/usage-analytics/use-usage-summary-query.tsclients/gui-app/src/lib/__tests__/analytics.test.tsclients/gui-app/src/lib/analytics.tsclients/gui-app/src/lib/settings-sections.tsclients/gui-app/src/lib/usage-analytics/__tests__/cost-format.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/format-metric-value.test.tsclients/gui-app/src/lib/usage-analytics/__tests__/usage-host-split.test.tsclients/gui-app/src/lib/usage-analytics/cost-format.tsclients/gui-app/src/lib/usage-analytics/usage-host-split.tsclients/gui-app/src/styles/usage-analytics-chart.cssprotocol/src/host/usage-analytics/schemas.ts
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>
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>
…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>
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)
host.usage.summary— request{timezone, windowDays, epicId?, chatId?}, response{servedBy, summary, coverage}. Registered withdegrade: {kind: "unsupported"}, so older hosts simply lack it and clients feature-detect.chatIdfilter, 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
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 formatgreen 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