Skip to content

feat(dashboard): add local usage statistics dashboard with session detail#948

Open
myk1yt wants to merge 32 commits into
Zoo-Code-Org:mainfrom
myk1yt:feature/local-usage-stats
Open

feat(dashboard): add local usage statistics dashboard with session detail#948
myk1yt wants to merge 32 commits into
Zoo-Code-Org:mainfrom
myk1yt:feature/local-usage-stats

Conversation

@myk1yt

@myk1yt myk1yt commented Jul 19, 2026

Copy link
Copy Markdown

Closes #947

Summary

Adds a local usage statistics dashboard accessible from the sidebar. Tracks LLM API call token usage and costs per session, with daily heatmap, session list/detail views, model/mode filtering, and export.

Features

Dashboard (Sidebar)

  • Summary cards: Total cost, tokens (input/output), API calls, active sessions
  • Daily Activity heatmap: 30/90-day CSS grid heatmap with day-by-day granularity
  • Time range filter: Today / 7 days / 30 days / All time / Custom date range
  • Model / Provider / Mode filters: Filter sessions by any dimension
  • Session list: Grouped by root task ID (subtasks appear under parent)
  • Session detail: Expandable API call list with per-call token breakdown
  • Mode column: Each API call shows which mode (code, debug, ask, etc.) was used
  • Multi-model/mode display: Session summary shows ALL models and modes used (not just first)
  • Export: JSON / CSV format
  • Clear: Nonce-protected confirmation dialog

Usage Recording

  • Single instrumentation point: Task.ts terminal finalize — no streaming duplicates
  • Append-only NDJSON: Each API call = one immutable event (5MiB rotation, 100MiB cap)
  • Local-only storage: globalStorage/usage-stats/ — no cloud sync, no telemetry
  • No sensitive data: API keys, prompts, responses never stored

Cost Calculation

  • 11 provider totalCost: Added totalCost computation to OpenAI, Anthropic Vertex, DeepSeek, xAI, Mistral, Kenari, Poe, Qwen-Code, Bedrock, Moonshot, and OpenAI-compatible providers
  • On-the-fly recalculation: Old events without costUsd are recalculated at query time using costRecalculation.ts — no NDJSON mutation
  • Provider model registries: Static pricing tables for cost lookup

Internationalization

  • 18 locale files: en, ca, de, es, fr, hi, id, it, ja, ko, nl, pl, pt-BR, ru, tr, vi, zh-CN, zh-TW
  • All strings translated: No English fallbacks — each locale uses its native language
  • Diacritics verified: Turkish, French, Spanish, Portuguese-BR, Polish accent marks validated

Architecture

Webview (React)                     Extension (Node.js)
┌─────────────────┐                ┌──────────────────────┐
│ DashboardView    │──IPC──────────▶│ usageStatsMessageHandler │
│  SessionList     │◀──────────────│  UsageAggregator        │
│  SessionDetail   │               │  UsageRecorder          │
│  UsageHeatmap    │               │  UsageEventStore        │
│  DashboardSummary│               │  costRecalculation      │
└─────────────────┘                └──────────────────────┘

New Files

File Purpose
packages/types/src/usage-stats.ts UsageEventV1, StatsQuery, StatsSnapshot, SessionSummary, SessionDetail, APICallRecord types
src/services/stats/UsageEventStore.ts NDJSON append-only storage with rotation
src/services/stats/UsageAggregator.ts Timezone-aware aggregation
src/services/stats/UsageRecorder.ts Task lifecycle integration
src/services/stats/UsageStatsService.ts Facade service
src/services/stats/costRecalculation.ts On-the-fly cost computation
src/core/webview/usageStatsMessageHandler.ts Extension ↔ Webview message handler
webview-ui/src/components/dashboard/DashboardView.tsx Main dashboard component
webview-ui/src/components/dashboard/DashboardSummary.tsx Summary cards
webview-ui/src/components/dashboard/SessionList.tsx Session list with filters
webview-ui/src/components/dashboard/SessionDetail.tsx Expandable session detail
webview-ui/src/components/stats/UsageHeatmap.tsx CSS grid daily heatmap
webview-ui/src/utils/formatNumber.ts Shared format utilities
webview-ui/src/i18n/locales/*/dashboard.json 18 locale files

Modified Files

File Change
src/api/providers/openai.ts totalCost calculation
src/api/providers/openai-compatible.ts totalCost calculation
src/api/providers/moonshot.ts totalCost calculation
src/api/providers/deepseek.ts totalCost calculation
src/api/providers/xai.ts totalCost calculation
src/api/providers/mistral.ts totalCost calculation
src/api/providers/kenari.ts totalCost calculation
src/api/providers/poe.ts totalCost calculation
src/api/providers/qwen-code.ts totalCost calculation
src/api/providers/bedrock.ts totalCost calculation
src/api/providers/anthropic-vertex.ts totalCost calculation
packages/types/src/vscode-extension-host.ts Dashboard IPC types
packages/types/src/vscode.ts commandIds changes
src/package.json dashboardButtonClicked command
src/package.nls.json + 16 locale variants Command title, BOM fix
src/activate/registerCommands.ts Dashboard command registration
src/core/webview/webviewMessageHandler.ts Dashboard message routing
webview-ui/src/App.tsx Dashboard tab routing

Key Design Decisions

  1. Dashboard replaces /stats: Sidebar entry point instead of slash command — more discoverable
  2. Session grouping by root task ID: Subtasks appear under parent session via parentTaskId chain
  3. Multi-model/mode aggregation: Sessions show all unique models and modes used, not just the first
  4. Cost recalculation at query time: Old events without cost data are computed on-the-fly using provider registries — no NDJSON mutation
  5. Single instrumentation point: Task.ts terminal finalize prevents streaming duplicates
  6. Local-only storage: No cloud sync, no telemetry, no sensitive data

Testing

  • pnpm check-types — 11/11 packages pass
  • pnpm lint — 0 warnings
  • pnpm --dir src test — 6946 tests pass
  • pnpm --dir webview-ui test — 1479 tests pass
  • check-translations — 17 non-English locales verified, 0 missing translations
  • pnpm build — clean build
  • pnpm vsix — VSIX package generated

CI Compatibility Fixes

  • Removed UTF-8 BOM from all package.nls.*.json files (17 files) for Linux JSON parser
  • Updated provider test expectations to include totalCost field (3 spec files)
  • Restored missing diacritics in 5 locale dashboard time strings (tr, fr, es, pt-BR, pl)

Summary by CodeRabbit

  • New Features

    • Added an in-app Usage Statistics Dashboard accessible from the sidebar with token/cost totals, configurable breakdowns, and a daily activity heatmap.
    • Added session list with model/provider/mode filtering and session detail with expandable API call list including Mode column.
    • Added multi-model and multi-mode display per session — shows all unique models and modes used, not just the first.
    • Added usage export (JSON/CSV) and safe clearing with confirmation and nonce-based validation.
    • Added on-the-fly cost recalculation for 11 LLM providers using static pricing registries.
    • Added full internationalization for 18 locales with verified native-language translations.
  • Improvements

    • Stats now surface completion/failure/cancellation and data coverage details.
    • CSV exports are hardened for spreadsheet safety.
    • Sessions grouped by root task ID with subtask aggregation.
  • Tests

    • Added automated coverage for recording, aggregation, export/clear, cost calculation, and the dashboard UI.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds local usage-statistics schemas, append-only event recording, aggregation, export and clearing services, extension message handling, the /stats command, and a webview statistics panel with tests and localization.

Changes

Usage contracts and entry points

Layer / File(s) Summary
Usage schemas and message contracts
packages/types/src/usage-stats.ts, packages/types/src/vscode-extension-host.ts, packages/types/src/index.ts
Defines versioned usage events, queries, buckets, snapshots, and typed webview request/response payloads.
Commands and localization
packages/types/src/vscode.ts, src/activate/registerCommands.ts, src/package.json, src/package.nls*.json, src/services/command/*
Registers openUsageStats, adds the stats built-in command, and supplies command title strings.

Statistics engine

Layer / File(s) Summary
Storage and recording
src/services/stats/UsageEventStore.ts, src/services/stats/UsageRecorder.ts
Persists terminal usage events as NDJSON with locking, deduplication, rotation, quarantine handling, clearing, and cap tracking.
Aggregation and service facade
src/services/stats/UsageAggregator.ts, src/services/stats/UsageStatsService.ts, src/services/stats/index.ts
Aggregates timezone-aware buckets, supports filtering and inclusion semantics, and provides JSON/CSV export, nonce-protected clearing, and history backfill.

Extension integration

Layer / File(s) Summary
Task instrumentation and provider wiring
src/core/task/Task.ts, src/core/webview/ClineProvider.ts
Initializes usage services and records completed, failed, or cancelled API attempts at terminal finalize.
Webview message handling
src/core/webview/usageStatsMessageHandler.ts, src/core/webview/webviewMessageHandler.ts
Validates queries, handles get/clear/export/nonce requests, writes exports, and emits correlated responses and change notifications.

Statistics webview

Layer / File(s) Summary
Navigation and presentation
webview-ui/src/App.tsx, webview-ui/src/components/chat/ChatView.tsx, webview-ui/src/components/stats/*, webview-ui/src/i18n/locales/*/stats.json
Adds the stats tab, exact /stats interception, summary cards, heatmap, breakdown table, loading/error/empty states, export controls, clear confirmation flow, and localized strings.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant StatsView
  participant WebviewMessageHandler
  participant UsageStatsService
  participant UsageEventStore
  User->>StatsView: Open /stats
  StatsView->>WebviewMessageHandler: getUsageStats
  WebviewMessageHandler->>UsageStatsService: queryStats
  UsageStatsService->>UsageEventStore: readAll
  UsageStatsService-->>WebviewMessageHandler: StatsSnapshot
  WebviewMessageHandler-->>StatsView: getUsageStatsResponse
  StatsView-->>User: Render summary and buckets
Loading

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#32: Both changes act on computed token/cost totals in the LLM task completion path and touch shared webview-related contracts.

Suggested labels: awaiting-author

Suggested reviewers: taltas, edelauna, hannesrudolph

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.86% 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
Linked Issues check ✅ Passed The PR implements the requested /stats command, local NDJSON storage, recording, aggregation, export, reset nonce flow, heatmap, and stats panel.
Out of Scope Changes check ✅ Passed No obvious out-of-scope changes stand out; the added tests, locales, and support wiring all serve the /stats feature.
Title check ✅ Passed The title clearly reflects the main change: adding a local usage statistics dashboard with session details.
Description check ✅ Passed The description is detailed and includes the linked issue, summary, feature list, architecture, and testing, though it doesn't follow the template headings exactly.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (5)
packages/types/src/usage-stats.ts (1)

20-24: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider stricter validation for date strings and numeric values.

occurredAt/generatedAt/from/to are plain z.string() rather than z.string().datetime(), and SourcedNumber.value/StatsBucket numeric fields allow negative or non-finite values. Since this schema is the contract for corruption-quarantining of append-only NDJSON data, tightening these constraints would let the schema itself reject malformed events earlier instead of relying entirely on downstream logic.

♻️ Example tightening
 export const SourcedNumber = z.object({
-	value: z.number(),
+	value: z.number().finite().nonnegative(),
 	source: UsageValueSource,
 })
-	occurredAt: z.string(), // ISO 8601 UTC
+	occurredAt: z.string().datetime(), // ISO 8601 UTC

Since zod's .datetime() strictness (e.g. offset support) varies by version, please confirm the behavior for zod 3.25.76 before applying broadly.

Also applies to: 69-76, 102-114

🤖 Prompt for 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.

In `@packages/types/src/usage-stats.ts` around lines 20 - 24, Tighten the
usage-stats schema by validating occurredAt, generatedAt, from, and to with the
Zod 3.25.76-compatible datetime constraint, confirming required offset behavior
before applying it. Update SourcedNumber.value and the numeric fields in
StatsBucket to reject negative and non-finite values while preserving valid
event data and the existing schema contract.
src/services/stats/UsageAggregator.ts (1)

456-498: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Collapse the identical inclusion-semantics branches or implement the intended behavior.

All three if/else if/else blocks for cacheReadInInput, cacheWriteInInput, and reasoningInOutput perform the exact same bucket.X += tokens, yet the surrounding comments (466-469) state that for "included" the value is already in inputTokens/outputTokens and should not be added separately "to avoid double counting". The code never implements that distinction. Since totalTokens is read directly from the event this isn't a live double-count bug, but the dead branches plus contradictory comments are a trap for future edits. Either flatten to a single unconditional add or implement the documented semantics explicitly.

🤖 Prompt for 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.

In `@src/services/stats/UsageAggregator.ts` around lines 456 - 498, Resolve the
contradiction in the token accumulation logic by either collapsing the identical
branches in the cacheReadInInput, cacheWriteInInput, and reasoningInOutput
handling into unconditional bucket.X additions, or implementing the documented
included/excluded semantics explicitly. Keep the chosen behavior consistent with
the surrounding comments and avoid retaining dead conditional branches.
src/services/stats/UsageStatsService.ts (1)

259-376: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract the shared preset/time-range/timezone resolution into one helper. UsageStatsService.filterEventsByQuery/resolvePresetRange/toTimezoneStartOfDay/getTimezoneOffsetMinutes reimplement the same day/preset/offset math as UsageAggregator.resolveTimeRange/toTimezoneDate/startOfDay/getTimezoneOffsetMinutes. Two copies of subtle DST/offset arithmetic can silently diverge, causing exportStats to include a different event set than queryStats for the same query.

  • src/services/stats/UsageStatsService.ts#L259-L376: replace the local preset/timezone helpers with the shared implementation used by the aggregator (or delegate range resolution to it).
  • src/services/stats/UsageAggregator.ts#L143-L271: move resolveTimeRange/toTimezoneDate/startOfDay/getTimezoneOffsetMinutes into a shared module both files import.
🤖 Prompt for 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.

In `@src/services/stats/UsageStatsService.ts` around lines 259 - 376, Extract the
shared preset, time-range, and timezone calculations from UsageStatsService
methods filterEventsByQuery, resolvePresetRange, toTimezoneStartOfDay, and
getTimezoneOffsetMinutes into a shared module, then update UsageStatsService.ts
lines 259-376 to use it. Move UsageAggregator’s resolveTimeRange,
toTimezoneDate, startOfDay, and getTimezoneOffsetMinutes from UsageAggregator.ts
lines 143-271 into the same module and update the aggregator imports/callers,
preserving identical range and DST behavior for both queryStats and exportStats.
webview-ui/src/components/stats/StatsSummary.tsx (1)

46-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded English strings bypass i18n in stats UI. These stats components use raw English words in user-facing output instead of t(...) keys, unlike the rest of the panel. Route them through the translation layer (add keys to stats.json).

  • webview-ui/src/components/stats/StatsSummary.tsx#L46-L50: replace the literal unknown in ({unknownCount} unknown) with a translated string.
  • webview-ui/src/components/stats/UsageHeatmap.tsx#L173-L177: replace the literal tokens/events in the tooltip content with translated strings.
🤖 Prompt for 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.

In `@webview-ui/src/components/stats/StatsSummary.tsx` around lines 46 - 50, The
stats UI contains hardcoded English strings that must use the translation layer.
In webview-ui/src/components/stats/StatsSummary.tsx lines 46-50, update
StatsSummary to translate “unknown”; in
webview-ui/src/components/stats/UsageHeatmap.tsx lines 173-177, translate
“tokens” and “events” in the tooltip. Add the corresponding keys and
translations to stats.json while preserving the existing count and tooltip
formatting.
src/services/stats/UsageEventStore.ts (1)

588-621: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

rebuildIdempotencySet reads every segment fully on each initialize().

Combined with a new UsageEventStore being constructed per Task (see src/core/task/Task.ts L533), the first append of every task re-reads and re-parses the entire current generation (up to 100 MiB) into memory to rebuild the in-memory dedupe set. This is fire-and-forget so it won't block the task, but it is repeated I/O + parse + string allocation that grows with stored history. Consider sharing a single store instance (e.g. the provider's UsageStatsService store) across tasks so the scan happens once per process.

🤖 Prompt for 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.

In `@src/services/stats/UsageEventStore.ts` around lines 588 - 621, The
UsageEventStore is recreated per task, causing rebuildIdempotencySet to rescan
and parse the full segment history on every initialize(). Reuse a single
UsageEventStore instance, preferably the provider-owned store exposed through
UsageStatsService, across tasks so initialization and idempotency rebuilding
occur once per process; preserve existing append and deduplication behavior.
🤖 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 `@packages/types/src/__tests__/usage-stats.spec.ts`:
- Around line 133-138: Rename the test case around UsageEventV1.parse to
describe that a zero attempt value is accepted, matching its current input and
assertion. Keep the existing test body unchanged and make the name reflect the
absence of a minimum constraint.

In `@src/package.nls.ca.json`:
- Line 1: Remove the leading UTF-8 BOM from src/package.nls.ca.json so the file
begins directly with the JSON object and remains compatible with strict
JSON.parse consumers.

In `@src/package.nls.de.json`:
- Line 1: Remove the leading BOM character from src/package.nls.de.json,
src/package.nls.es.json, src/package.nls.fr.json, src/package.nls.zh-CN.json,
and src/package.nls.zh-TW.json at line 1, leaving each file as valid JSON
beginning with its opening brace.

In `@src/package.nls.hi.json`:
- Line 1: Remove the leading UTF-8 BOM before the opening JSON object in
src/package.nls.hi.json (line 1), src/package.nls.id.json (line 1), and
src/package.nls.it.json (line 1); re-save all three localization files as UTF-8
without BOM while preserving their JSON content.

In `@src/package.nls.ja.json`:
- Line 1: Remove the leading BOM character from src/package.nls.ja.json#L1-L1,
src/package.nls.ko.json#L1-L1, src/package.nls.nl.json#L1-L1,
src/package.nls.pl.json#L1-L1, and src/package.nls.pt-BR.json#L1-L1, leaving
each file as valid JSON beginning directly with its opening brace.

In `@src/services/stats/__tests__/UsageAggregator.spec.ts`:
- Around line 436-451: Update the events in the “should sort category buckets by
totalTokens descending then name ascending” test to populate usage.totalTokens
with values matching the intended order: anthropic 3000, google 2000, and openai
1000. Keep the existing assertions and inputTokens fixtures unchanged.

In `@webview-ui/src/components/stats/StatsView.tsx`:
- Around line 176-180: Update the usageStatsChanged handling in the
message-listener effect to store the debounce timer in a ref instead of
returning cleanup from handleMessage. Clear any existing timer before scheduling
a new fetchStats call, and clear the ref-held timer in the effect cleanup so
pending refetches cannot run after unmount.

---

Nitpick comments:
In `@packages/types/src/usage-stats.ts`:
- Around line 20-24: Tighten the usage-stats schema by validating occurredAt,
generatedAt, from, and to with the Zod 3.25.76-compatible datetime constraint,
confirming required offset behavior before applying it. Update
SourcedNumber.value and the numeric fields in StatsBucket to reject negative and
non-finite values while preserving valid event data and the existing schema
contract.

In `@src/services/stats/UsageAggregator.ts`:
- Around line 456-498: Resolve the contradiction in the token accumulation logic
by either collapsing the identical branches in the cacheReadInInput,
cacheWriteInInput, and reasoningInOutput handling into unconditional bucket.X
additions, or implementing the documented included/excluded semantics
explicitly. Keep the chosen behavior consistent with the surrounding comments
and avoid retaining dead conditional branches.

In `@src/services/stats/UsageEventStore.ts`:
- Around line 588-621: The UsageEventStore is recreated per task, causing
rebuildIdempotencySet to rescan and parse the full segment history on every
initialize(). Reuse a single UsageEventStore instance, preferably the
provider-owned store exposed through UsageStatsService, across tasks so
initialization and idempotency rebuilding occur once per process; preserve
existing append and deduplication behavior.

In `@src/services/stats/UsageStatsService.ts`:
- Around line 259-376: Extract the shared preset, time-range, and timezone
calculations from UsageStatsService methods filterEventsByQuery,
resolvePresetRange, toTimezoneStartOfDay, and getTimezoneOffsetMinutes into a
shared module, then update UsageStatsService.ts lines 259-376 to use it. Move
UsageAggregator’s resolveTimeRange, toTimezoneDate, startOfDay, and
getTimezoneOffsetMinutes from UsageAggregator.ts lines 143-271 into the same
module and update the aggregator imports/callers, preserving identical range and
DST behavior for both queryStats and exportStats.

In `@webview-ui/src/components/stats/StatsSummary.tsx`:
- Around line 46-50: The stats UI contains hardcoded English strings that must
use the translation layer. In webview-ui/src/components/stats/StatsSummary.tsx
lines 46-50, update StatsSummary to translate “unknown”; in
webview-ui/src/components/stats/UsageHeatmap.tsx lines 173-177, translate
“tokens” and “events” in the tooltip. Add the corresponding keys and
translations to stats.json while preserving the existing count and tooltip
formatting.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f0ac10b1-4bba-4501-ba94-ef6ebdff01df

📥 Commits

Reviewing files that changed from the base of the PR and between c322f3c and 8b47ba1.

📒 Files selected for processing (48)
  • packages/types/src/__tests__/usage-stats.spec.ts
  • packages/types/src/index.ts
  • packages/types/src/usage-stats.ts
  • packages/types/src/vscode-extension-host.ts
  • packages/types/src/vscode.ts
  • src/activate/registerCommands.ts
  • src/core/task/Task.ts
  • src/core/task/__tests__/Task.usage-stats.spec.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/usageStatsMessageHandler.spec.ts
  • src/core/webview/usageStatsMessageHandler.ts
  • src/core/webview/webviewMessageHandler.ts
  • src/package.json
  • src/package.nls.ca.json
  • src/package.nls.de.json
  • src/package.nls.es.json
  • src/package.nls.fr.json
  • src/package.nls.hi.json
  • src/package.nls.id.json
  • src/package.nls.it.json
  • src/package.nls.ja.json
  • src/package.nls.json
  • src/package.nls.ko.json
  • src/package.nls.nl.json
  • src/package.nls.pl.json
  • src/package.nls.pt-BR.json
  • src/package.nls.ru.json
  • src/package.nls.tr.json
  • src/package.nls.vi.json
  • src/package.nls.zh-CN.json
  • src/package.nls.zh-TW.json
  • src/services/command/__tests__/built-in-commands.spec.ts
  • src/services/command/built-in-commands.ts
  • src/services/stats/UsageAggregator.ts
  • src/services/stats/UsageEventStore.ts
  • src/services/stats/UsageRecorder.ts
  • src/services/stats/UsageStatsService.ts
  • src/services/stats/__tests__/UsageAggregator.spec.ts
  • src/services/stats/__tests__/UsageEventStore.spec.ts
  • src/services/stats/index.ts
  • webview-ui/src/App.tsx
  • webview-ui/src/components/chat/ChatView.tsx
  • webview-ui/src/components/chat/__tests__/ChatView.stats-command.spec.tsx
  • webview-ui/src/components/stats/StatsSummary.tsx
  • webview-ui/src/components/stats/StatsView.tsx
  • webview-ui/src/components/stats/UsageHeatmap.tsx
  • webview-ui/src/components/stats/__tests__/StatsView.spec.tsx
  • webview-ui/src/i18n/locales/en/stats.json

Comment on lines +133 to +138
it("should reject negative attempt", () => {
// z.number() accepts negatives, but attempt should be >= 0 logically
// This test confirms the schema accepts any number (no min constraint in V1)
const result = UsageEventV1.parse({ ...validEvent, attempt: 0 })
expect(result.attempt).toBe(0)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test name contradicts its body.

The test is titled "should reject negative attempt" but it parses attempt: 0 and asserts it's accepted, not rejected. No negative value is ever passed. The name should reflect what's actually verified (that any number, including 0, is accepted — no min constraint).

💚 Proposed fix
-		it("should reject negative attempt", () => {
-			// z.number() accepts negatives, but attempt should be >= 0 logically
-			// This test confirms the schema accepts any number (no min constraint in V1)
-			const result = UsageEventV1.parse({ ...validEvent, attempt: 0 })
-			expect(result.attempt).toBe(0)
-		})
+		it("should accept attempt of 0 (no min constraint in V1)", () => {
+			// z.number() has no lower bound in V1; this documents current behavior.
+			const result = UsageEventV1.parse({ ...validEvent, attempt: 0 })
+			expect(result.attempt).toBe(0)
+		})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should reject negative attempt", () => {
// z.number() accepts negatives, but attempt should be >= 0 logically
// This test confirms the schema accepts any number (no min constraint in V1)
const result = UsageEventV1.parse({ ...validEvent, attempt: 0 })
expect(result.attempt).toBe(0)
})
it("should accept attempt of 0 (no min constraint in V1)", () => {
// z.number() has no lower bound in V1; this documents current behavior.
const result = UsageEventV1.parse({ ...validEvent, attempt: 0 })
expect(result.attempt).toBe(0)
})
🤖 Prompt for 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.

In `@packages/types/src/__tests__/usage-stats.spec.ts` around lines 133 - 138,
Rename the test case around UsageEventV1.parse to describe that a zero attempt
value is accepted, matching its current input and assertion. Keep the existing
test body unchanged and make the name reflect the absence of a minimum
constraint.

Comment thread src/package.nls.ca.json Outdated
Comment thread src/package.nls.de.json Outdated
@@ -1,4 +1,4 @@
{
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
for f in src/package.nls.de.json src/package.nls.es.json src/package.nls.fr.json src/package.nls.zh-CN.json src/package.nls.zh-TW.json src/package.nls.json; do
  printf '%s: ' "$f"
  head -c3 "$f" | xxd -p
done

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 538


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

files = [
    "src/package.nls.de.json",
    "src/package.nls.es.json",
    "src/package.nls.fr.json",
    "src/package.nls.zh-CN.json",
    "src/package.nls.zh-TW.json",
    "src/package.nls.json",
]

for f in files:
    data = Path(f).read_bytes()
    print(f"{f}: {data[:3].hex()}  BOM={data.startswith(b'\\xef\\xbb\\xbf')}")
PY

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 408


🏁 Script executed:

rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' 'package\.nls|JSON\.parse|BOM|utf-8' .

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 50377


🏁 Script executed:

rg -n --hidden --glob '!**/node_modules/**' --glob '!**/.git/**' 'package\.nls|JSON\.parse|readFile.*json|utf-8-bom|strip.*bom|BOM' .

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 50377


🌐 Web query:

VS Code package.nls JSON BOM strip localization bundle loader JSON.parse

💡 Result:

In Visual Studio Code development, issues involving JSON.parse errors with package.nls.json or localization bundle files are frequently caused by the presence of a UTF-8 Byte Order Mark (BOM) [1]. Standard Node.js JSON.parse does not automatically strip a leading BOM, which results in a syntax error when the file content is read into a string [2][3]. To resolve this, developers must ensure that files are read and processed without the BOM, or explicitly strip it before parsing [1]. The following technical details apply to handling these localization files: 1. BOM Stripping: If you are programmatically reading these files in a VS Code extension or build tool, use the following approach to ensure compatibility: if (content.charCodeAt(0) === 0xFEFF) { content = content.slice(1); } const json = JSON.parse(content); 2. Localization Infrastructure: VS Code currently uses the @vscode/l10n and @vscode/l10n-dev libraries for handling localization [4][5][6]. Older extensions may still use the deprecated vscode-nls and vscode-nls-dev packages [7][8]. When using these older tools, ensure that your build pipeline (e.g., Gulp tasks) handles file encoding consistently [7]. 3. Common Pitfalls: - Encoding Mismatches: Always ensure files are saved as UTF-8 without BOM if possible, as tools like standard JSON.parse are sensitive to the leading bytes [2][3][1]. - Invalid JSON Structure: Beyond BOM issues, ensure that your package.nls.json follows the expected schema. If you include "comment" fields for localization purposes, ensure the environment (VS Code version) supports the schema extension, otherwise, it may throw type errors or fail to load [9]. - Hidden Characters: Tools sometimes inject zero-width spaces (\u200b) or other non-printable characters for specific localization scenarios (e.g., URL formatting); ensure your JSON parser or validation logic accounts for these if you are manipulating the JSON object manually [10]. If you are encountering a JSON.parse error during extension loading, check the specific file content for the UTF-8 BOM byte sequence (0xEF, 0xBB, 0xBF) at the start of the file [3][1]. Stripping this prefix is the standard solution for making JSON files BOM-tolerant in the VS Code ecosystem [1].

Citations:


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
for f in [
    "src/package.nls.de.json",
    "src/package.nls.es.json",
    "src/package.nls.fr.json",
    "src/package.nls.zh-CN.json",
    "src/package.nls.zh-TW.json",
    "src/package.nls.json",
]:
    data = Path(f).read_bytes()
    print(f"{f}: {data[:3].hex()} BOM={data.startswith(b'\xef\xbb\xbf')}")
PY

printf '\n--- esbuild ---\n'
sed -n '90,120p' apps/vscode-nightly/esbuild.mjs

printf '\n--- i18n setup ---\n'
sed -n '1,120p' src/i18n/setup.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 3807


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

files = [
    "src/package.nls.de.json",
    "src/package.nls.es.json",
    "src/package.nls.fr.json",
    "src/package.nls.zh-CN.json",
    "src/package.nls.zh-TW.json",
    "src/package.nls.json",
]

for f in files:
    data = Path(f).read_bytes()
    bom = data.startswith(b"\xef\xbb\xbf")
    print(f"{f}: {data[:3].hex()} BOM={bom}")
PY

rg -n "strip-bom|package\.nls" apps/vscode-nightly src scripts .github

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 3409


🌐 Web query:

VS Code package.nls.*.json loader BOM strip source code localization JSON.parse

💡 Result:

In the context of VS Code localization and file loading, the Byte Order Mark (BOM) — specifically the UTF-8 BOM (U+FEFF) — is frequently encountered in package.nls.json files and other localization resources. Because standard JSON.parse() in Node.js and modern JavaScript engines can fail or behave unexpectedly if a leading BOM is present, VS Code infrastructure often includes explicit logic to strip it before parsing [1][2][3][4]. General approaches observed in VS Code source code for handling the BOM include: 1. Buffer Inspection and Conversion: Often, files are read into a Buffer. Before converting the buffer to a UTF-8 string for parsing, or immediately after string conversion, code checks for the BOM. For example, some loaders convert the Buffer to a string using 'utf8' and then perform a check: if (contents.charCodeAt(0) === 0xFEFF) { contents = contents.slice(1); } [1][2] 2. Automated Tools and Build Pipelines: The VS Code build pipeline (e.g., vscode-nls-dev or the core build tools) ensures that when localization files are generated or processed, they are handled in a way that is compatible with downstream consumers [5][6]. When files are read, the logic typically identifies package.nls.json files and uses JSON.parse(buffer.toString('utf8')), sometimes relying on the fact that the platform-specific filesystem or previous build steps have sanitized the content [5][3]. 3. BOM-Tolerant Parsing: In cases where internal models (like TextModel) represent file content, the constructor often contains a defensive check to strip the BOM [1][2]. This allows callers to process the file content without needing to special-case the first character of the first line [1]. In summary, while JSON.parse is the standard tool for reading package.nls.json files [5][3][6], VS Code's technical ecosystem maintains robust defensive programming practices to detect and remove the UTF-8 BOM, ensuring it does not interfere with string matching, parsing, or identity construction [1][2][7][4].

Citations:


🌐 Web query:

site:github.com/microsoft/vscode build/lib/i18n.ts BOM JSON.parse package.nls

💡 Result:

In the context of the VS Code build system, particularly within build/lib/i18n.ts and related localization scripts, the handling of UTF-8 Byte Order Marks (BOM) is a deliberate technical choice to ensure compatibility and correct file encoding across different platforms [1][2]. When parsing localization files such as package.nls.json, the build scripts often read the file contents as UTF-8 strings [1][3][4]. Because JSON.parse can sometimes fail or behave unexpectedly if a file contains an invisible leading UTF-8 BOM (U+FEFF), developers and build utilities in the VS Code codebase implement specific patterns to handle this: 1. Encoding and BOM Handling: Build scripts that generate localization output (such as.isl files) explicitly include a UTF-8 BOM [1][2]. This is done by creating a Buffer with the BOM sequence [0xEF, 0xBB, 0xBF] and concatenating it with the file content [1][2]. 2. Parsing Logic: When reading configuration files like package.nls.json, the build system typically reads the file content as 'utf8' [1][3][4]. Recent updates and patterns in the codebase demonstrate that if code encounters a leading BOM during processing (for example, in text models), it may explicitly strip the character to prevent it from interfering with subsequent logic or JSON parsing [2]. 3. Reliability: This ensures that files are correctly recognized as UTF-8 by various tools (including Windows-specific utilities) while avoiding common runtime issues where the BOM character might cause a JSON.parse syntax error [2]. In summary, the interaction between these components is designed to strictly manage file encoding by explicitly writing a BOM where required for external compatibility and stripping it or ensuring standard UTF-8 parsing when reading files into the build pipeline to maintain JSON structural integrity [1][2].

Citations:


🏁 Script executed:

sed -n '1,120p' .github/workflows/code-qa.yml

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 5375


🏁 Script executed:

sed -n '1,140p' scripts/find-missing-translations.js
printf '\n---\n'
sed -n '270,360p' scripts/find-missing-translations.js

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 7104


Strip the BOM from these locale JSON files. src/package.nls.de.json, src/package.nls.es.json, src/package.nls.fr.json, src/package.nls.zh-CN.json, and src/package.nls.zh-TW.json all start with \uFEFF; scripts/find-missing-translations.js reads them with plain JSON.parse, so the package-nls check will fail on these files.

📍 Affects 5 files
  • src/package.nls.de.json#L1-L1 (this comment)
  • src/package.nls.es.json#L1-L1
  • src/package.nls.fr.json#L1-L1
  • src/package.nls.zh-CN.json#L1-L1
  • src/package.nls.zh-TW.json#L1-L1
🤖 Prompt for 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.

In `@src/package.nls.de.json` at line 1, Remove the leading BOM character from
src/package.nls.de.json, src/package.nls.es.json, src/package.nls.fr.json,
src/package.nls.zh-CN.json, and src/package.nls.zh-TW.json at line 1, leaving
each file as valid JSON beginning with its opening brace.

Comment thread src/package.nls.hi.json Outdated
@@ -1,4 +1,4 @@
{
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the accidental UTF-8 BOM from the localization JSON files. These three package.nls.*.json files gained a leading \uFEFF byte-order mark before the opening {. It adds diff noise and can break strict JSON.parse-based tooling; re-save each file as UTF-8 without BOM.

  • src/package.nls.hi.json#L1-L1: strip the leading BOM before {.
  • src/package.nls.id.json#L1-L1: strip the leading BOM before {.
  • src/package.nls.it.json#L1-L1: strip the leading BOM before {.
📍 Affects 3 files
  • src/package.nls.hi.json#L1-L1 (this comment)
  • src/package.nls.id.json#L1-L1
  • src/package.nls.it.json#L1-L1
🤖 Prompt for 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.

In `@src/package.nls.hi.json` at line 1, Remove the leading UTF-8 BOM before the
opening JSON object in src/package.nls.hi.json (line 1), src/package.nls.id.json
(line 1), and src/package.nls.it.json (line 1); re-save all three localization
files as UTF-8 without BOM while preserving their JSON content.

Comment thread src/package.nls.ja.json Outdated
@@ -1,4 +1,4 @@
{
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

files = [
    "src/package.nls.ja.json",
    "src/package.nls.ko.json",
    "src/package.nls.nl.json",
    "src/package.nls.pl.json",
    "src/package.nls.pt-BR.json",
]
for f in files:
    data = Path(f).read_bytes()
    print(f, data[:3], "BOM" if data.startswith(b'\xef\xbb\xbf') else "no-bom")
PY

printf '\n--- NLS-related files ---\n'
git ls-files 'src/*' | rg 'package\.nls|nls|i18n|localization|package\.json' || true

printf '\n--- Search for BOM handling / JSON parsing ---\n'
rg -n "BOM|utf-8-sig|strip.*bom|read.*json|JSON\.parse|nls" src . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' | sed -n '1,200p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 28894


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path

files = [
    "src/package.nls.ja.json",
    "src/package.nls.ko.json",
    "src/package.nls.nl.json",
    "src/package.nls.pl.json",
    "src/package.nls.pt-BR.json",
]
for f in files:
    data = Path(f).read_bytes()
    print(f"{f}: startswith_bom={data.startswith(b'\\xef\\xbb\\xbf')} first_bytes={data[:10]!r}")
PY

printf '\n--- matching files ---\n'
git ls-files 'src/*' | rg 'package\.nls|nls|i18n|localization|package\.json' || true

printf '\n--- BOM / JSON parsing references ---\n'
rg -n "BOM|utf-8-sig|strip.*bom|read.*json|JSON\.parse|nls" src . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**' | sed -n '1,220p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 30974


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
for f in [
    "src/package.nls.ja.json",
    "src/package.nls.ko.json",
    "src/package.nls.nl.json",
    "src/package.nls.pl.json",
    "src/package.nls.pt-BR.json",
]:
    b = Path(f).read_bytes()
    print(f, "BOM" if b.startswith(b"\xef\xbb\xbf") else "no-BOM", b[:6])

print("\n--- likely NLS loader files ---")
for p in Path("src").rglob("*"):
    if p.is_file() and any(k in p.name.lower() for k in ["nls", "localization", "package"]):
        if p.suffix in [".ts", ".js", ".mjs", ".cjs", ".json", ".jsonc"]:
            print(p)
PY

printf '\n--- search for JSON loading in src ---\n'
rg -n "readFileSync\\(|JSON\\.parse\\(|stripBOM|vscode-nls|nls" src -g '!**/node_modules/**' | sed -n '1,220p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 25110


🏁 Script executed:

ast-grep outline src/shared/package.ts --view expanded
printf '\n--- src/i18n/setup.ts ---\n'
sed -n '1,140p' src/i18n/setup.ts
printf '\n--- src/package.json scripts/keywords around localization ---\n'
sed -n '1,220p' src/package.json | sed -n '1,220p'
printf '\n--- search package.nls references ---\n'
rg -n "package\.nls|nls\.json|vscode-nls|localization" src scripts packages webview-ui apps -g '!**/node_modules/**' | sed -n '1,240p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 10065


🏁 Script executed:

sed -n '260,380p' scripts/find-missing-translations.js
printf '\n---\n'
sed -n '90,135p' apps/vscode-nightly/esbuild.mjs
printf '\n--- JSON.parse with BOM probe ---\n'
node - <<'JS'
const samples = ['\ufeff{}', '\ufeff{\n  "a": 1\n}', '{}'];
for (const s of samples) {
  try {
    const v = JSON.parse(s);
    console.log('ok', JSON.stringify(s.slice(0,3)), v);
  } catch (e) {
    console.log('err', JSON.stringify(s.slice(0,3)), e.name + ': ' + e.message);
  }
}
JS

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 5341


Strip the BOM from these locale JSON files. JSON.parse rejects a leading \uFEFF, so these package.nls.*.json files can break consumers that read them as plain JSON.

  • src/package.nls.ja.json#L1-L1
  • src/package.nls.ko.json#L1-L1
  • src/package.nls.nl.json#L1-L1
  • src/package.nls.pl.json#L1-L1
  • src/package.nls.pt-BR.json#L1-L1
📍 Affects 5 files
  • src/package.nls.ja.json#L1-L1 (this comment)
  • src/package.nls.ko.json#L1-L1
  • src/package.nls.nl.json#L1-L1
  • src/package.nls.pl.json#L1-L1
  • src/package.nls.pt-BR.json#L1-L1
🤖 Prompt for 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.

In `@src/package.nls.ja.json` at line 1, Remove the leading BOM character from
src/package.nls.ja.json#L1-L1, src/package.nls.ko.json#L1-L1,
src/package.nls.nl.json#L1-L1, src/package.nls.pl.json#L1-L1, and
src/package.nls.pt-BR.json#L1-L1, leaving each file as valid JSON beginning
directly with its opening brace.

Comment on lines +436 to +451
it("should sort category buckets by totalTokens descending then name ascending", () => {
const events = [
makeEvent({ eventId: "evt-1", idempotencyKey: "idem-1", provider: "openai", usage: { inputTokens: { value: 1000, source: "provider" } } }),
makeEvent({ eventId: "evt-2", idempotencyKey: "idem-2", provider: "anthropic", usage: { inputTokens: { value: 3000, source: "provider" } } }),
makeEvent({ eventId: "evt-3", idempotencyKey: "idem-3", provider: "google", usage: { inputTokens: { value: 2000, source: "provider" } } }),
]
const query = makeQuery({ groupBy: ["provider"] })

const result = aggregator.query(events, query)

expect(result.buckets).toHaveLength(3)
// totalTokens 내림차순: anthropic(3000) > google(2000) > openai(1000)
expect(result.buckets[0].key.provider).toBe("anthropic")
expect(result.buckets[1].key.provider).toBe("google")
expect(result.buckets[2].key.provider).toBe("openai")
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern 'extractValue($_) {
  $$$
}' --lang typescript src/services/stats/UsageAggregator.ts

rg -n -A5 'private extractValue' src/services/stats/UsageAggregator.ts
rg -n -A15 'private sortBuckets' src/services/stats/UsageAggregator.ts

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1315


Populate usage.totalTokens in this sort test

The fixture only sets usage.inputTokens, so each bucket ends up with totalTokens === 0 and the order comes from the alphabetical tie-break instead of the intended totalTokens sort. Set usage.totalTokens on these events so the test covers the named behavior.

🤖 Prompt for 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.

In `@src/services/stats/__tests__/UsageAggregator.spec.ts` around lines 436 - 451,
Update the events in the “should sort category buckets by totalTokens descending
then name ascending” test to populate usage.totalTokens with values matching the
intended order: anthropic 3000, google 2000, and openai 1000. Keep the existing
assertions and inputTokens fixtures unchanged.

Source: Coding guidelines

Comment on lines +176 to +180
if (message.type === "usageStatsChanged") {
// Data changed externally — refetch with debounce
const timer = setTimeout(() => fetchStats(preset, groupBy), 300)
return () => clearTimeout(timer)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Debounce cleanup is dead code; timer never cleared.

The return () => clearTimeout(timer) is returned from the handleMessage event-listener callback, whose return value is discarded — so the cleanup never runs. Rapid usageStatsChanged messages each schedule an independent refetch (no coalescing), and a pending timer can fire after unmount. Track the timer in a ref and clear it on the effect's cleanup.

🔧 Ref-based debounce
+	const refetchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
+
 	useEffect(() => {
 		const handleMessage = (e: MessageEvent) => {
 			const message: ExtensionMessage = e.data
@@
 			if (message.type === "usageStatsChanged") {
-				// Data changed externally — refetch with debounce
-				const timer = setTimeout(() => fetchStats(preset, groupBy), 300)
-				return () => clearTimeout(timer)
+				// Data changed externally — refetch with debounce
+				if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current)
+				refetchTimerRef.current = setTimeout(() => fetchStats(preset, groupBy), 300)
 			}
@@
 		window.addEventListener("message", handleMessage)
-		return () => window.removeEventListener("message", handleMessage)
+		return () => {
+			window.removeEventListener("message", handleMessage)
+			if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current)
+		}
 	}, [t, preset, groupBy, fetchStats])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (message.type === "usageStatsChanged") {
// Data changed externally — refetch with debounce
const timer = setTimeout(() => fetchStats(preset, groupBy), 300)
return () => clearTimeout(timer)
}
const refetchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
const handleMessage = (e: MessageEvent) => {
const message: ExtensionMessage = e.data
if (message.type === "usageStatsChanged") {
// Data changed externally — refetch with debounce
if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current)
refetchTimerRef.current = setTimeout(() => fetchStats(preset, groupBy), 300)
}
}
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
if (refetchTimerRef.current) clearTimeout(refetchTimerRef.current)
}
}, [t, preset, groupBy, fetchStats])
🤖 Prompt for 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.

In `@webview-ui/src/components/stats/StatsView.tsx` around lines 176 - 180, Update
the usageStatsChanged handling in the message-listener effect to store the
debounce timer in a ref instead of returning cleanup from handleMessage. Clear
any existing timer before scheduling a new fetchStats call, and clear the
ref-held timer in the effect cleanup so pending refetches cannot run after
unmount.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/services/stats/__tests__/UsageAggregator.spec.ts (1)

514-518: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Week-grouping test under-asserts its documented behavior.

The comment states three events fall into weeks 28, 29, 29 (i.e. 2 buckets), but the assertions only check >= 1 bucket and key format — a regression collapsing everything into one bucket or producing wrong keys would still pass. Consider asserting the exact bucket count/keys to actually cover the grouping behavior.

♻️ Suggested strengthening
-			expect(result.buckets.length).toBeGreaterThanOrEqual(1)
-			const weekKeys = result.buckets.map((b) => b.key.week)
+			expect(result.buckets).toHaveLength(2)
+			const weekKeys = result.buckets.map((b) => b.key.week)
 			weekKeys.forEach((key) => {
 				expect(key).toMatch(/^\d{4}-W\d{2}$/)
 			})
🤖 Prompt for 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.

In `@src/services/stats/__tests__/UsageAggregator.spec.ts` around lines 514 - 518,
Strengthen the week-grouping test assertions around result.buckets by requiring
exactly two buckets and verifying their keys are the documented weeks 28 and 29
in the expected year. Retain the key-format validation only if useful, but
replace the insufficient greater-than-or-equal count check so collapsed or
incorrect grouping fails.
🤖 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 `@src/services/stats/__tests__/UsageStatsService.spec.ts`:
- Line 178: Replace the tautological assertion in the relevant UsageStatsService
test with a type check that verifies result is not a CSV string, matching the
existing assertions used near the other JsonExport checks at lines 260 and 308.

---

Nitpick comments:
In `@src/services/stats/__tests__/UsageAggregator.spec.ts`:
- Around line 514-518: Strengthen the week-grouping test assertions around
result.buckets by requiring exactly two buckets and verifying their keys are the
documented weeks 28 and 29 in the expected year. Retain the key-format
validation only if useful, but replace the insufficient greater-than-or-equal
count check so collapsed or incorrect grouping fails.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aee3c05-f62b-4c50-abc4-d086171b1c8a

📥 Commits

Reviewing files that changed from the base of the PR and between 8b47ba1 and 9462e8a.

📒 Files selected for processing (4)
  • src/services/stats/__tests__/UsageAggregator.spec.ts
  • src/services/stats/__tests__/UsageStatsService.spec.ts
  • webview-ui/src/components/stats/__tests__/StatsView.spec.tsx
  • webview-ui/src/components/stats/__tests__/UsageHeatmap.spec.tsx

const query = makeQuery({ preset: "all" })
const result = await service.exportStats(query, "json")

expect(result).not.toBe("string")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tautological assertion — provides no coverage.

result is a JsonExport object, so expect(result).not.toBe("string") compares an object against the literal string "string" and always passes. The intent (verify it isn't a CSV string) requires checking the type, as done correctly at Lines 260 and 308.

💚 Proposed fix
-			expect(result).not.toBe("string")
+			expect(typeof result).not.toBe("string")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expect(result).not.toBe("string")
expect(typeof result).not.toBe("string")
🤖 Prompt for 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.

In `@src/services/stats/__tests__/UsageStatsService.spec.ts` at line 178, Replace
the tautological assertion in the relevant UsageStatsService test with a type
check that verifies result is not a CSV string, matching the existing assertions
used near the other JsonExport checks at lines 260 and 308.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@webview-ui/src/i18n/locales/de/stats.json`:
- Around line 33-34: Update the cacheReadTokens and cacheWriteTokens entries in
the German stats translations to use complete cache operation labels, replacing
the truncated wording with “Cache-Lesevorgänge” and “Cache-Schreibvorgänge” or
the established equivalent terminology.

In `@webview-ui/src/i18n/locales/fr/stats.json`:
- Around line 33-34: Update the cacheReadTokens and cacheWriteTokens
translations in the French stats locale to use the complete labels “Lecture du
cache” and “Écriture du cache”, or the project’s established equivalent
terminology.

In `@webview-ui/src/i18n/locales/vi/stats.json`:
- Around line 51-52: Update the Vietnamese coverage labels in the stats locale,
including the related label at the additional referenced entry: replace “Ghi từ”
with “Bắt đầu ghi từ”, replace “Sự kiện bổ sung ngược” with “Sự kiện được bổ
sung hồi tố”, and use “Được bổ sung hồi tố” for the corresponding related value.

In `@webview-ui/src/i18n/locales/zh-TW/stats.json`:
- Around line 11-14: Update the totalTokens, inputTokens, outputTokens, and
cacheTokens labels in the Traditional Chinese stats locale to use appropriate
terminology for “token,” replacing “權杖” with “詞元” consistently across all four
labels.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 033af2f3-8f50-47dc-80d9-712d6e878d7c

📥 Commits

Reviewing files that changed from the base of the PR and between 9462e8a and 00ecf4b.

📒 Files selected for processing (17)
  • webview-ui/src/i18n/locales/ca/stats.json
  • webview-ui/src/i18n/locales/de/stats.json
  • webview-ui/src/i18n/locales/es/stats.json
  • webview-ui/src/i18n/locales/fr/stats.json
  • webview-ui/src/i18n/locales/hi/stats.json
  • webview-ui/src/i18n/locales/id/stats.json
  • webview-ui/src/i18n/locales/it/stats.json
  • webview-ui/src/i18n/locales/ja/stats.json
  • webview-ui/src/i18n/locales/ko/stats.json
  • webview-ui/src/i18n/locales/nl/stats.json
  • webview-ui/src/i18n/locales/pl/stats.json
  • webview-ui/src/i18n/locales/pt-BR/stats.json
  • webview-ui/src/i18n/locales/ru/stats.json
  • webview-ui/src/i18n/locales/tr/stats.json
  • webview-ui/src/i18n/locales/vi/stats.json
  • webview-ui/src/i18n/locales/zh-CN/stats.json
  • webview-ui/src/i18n/locales/zh-TW/stats.json

Comment thread webview-ui/src/i18n/locales/de/stats.json Outdated
Comment thread webview-ui/src/i18n/locales/fr/stats.json Outdated
Comment thread webview-ui/src/i18n/locales/vi/stats.json Outdated
Comment thread webview-ui/src/i18n/locales/zh-TW/stats.json Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/package.nls.ca.json (1)

19-19: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate command.openUsageStats.title in each locale file.

The new command title is English in all six locale overrides, so users of those locales will see untranslated UI text.

  • src/package.nls.ca.json#L19-L19: add the Catalan translation.
  • src/package.nls.fr.json#L19-L19: add the French translation.
  • src/package.nls.hi.json#L19-L19: add the Hindi translation.
  • src/package.nls.id.json#L28-L28: add the Indonesian translation.
  • src/package.nls.zh-CN.json#L19-L19: add the Simplified Chinese translation.
  • src/package.nls.zh-TW.json#L19-L19: add the Traditional Chinese translation.
🤖 Prompt for 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.

In `@src/package.nls.ca.json` at line 19, Translate the
command.openUsageStats.title value in each affected locale:
src/package.nls.ca.json#19-19, src/package.nls.fr.json#19-19,
src/package.nls.hi.json#19-19, src/package.nls.id.json#28-28,
src/package.nls.zh-CN.json#19-19, and src/package.nls.zh-TW.json#19-19. Replace
the English title with the appropriate Catalan, French, Hindi, Indonesian,
Simplified Chinese, and Traditional Chinese translations respectively.
src/package.nls.it.json (1)

19-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Localize command.openUsageStats.title in each locale.

The new key is present, so these English values are used directly instead of falling back to English only when a locale file is missing.

  • src/package.nls.it.json#L19-L19: add the reviewed Italian translation.
  • src/package.nls.ja.json#L28-L28: add the reviewed Japanese translation.
  • src/package.nls.ko.json#L19-L19: add the reviewed Korean translation.
  • src/package.nls.nl.json#L28-L28: add the reviewed Dutch translation.
  • src/package.nls.pl.json#L19-L19: add the reviewed Polish translation.
  • src/package.nls.pt-BR.json#L19-L19: add the reviewed Brazilian Portuguese translation.
  • src/package.nls.ru.json#L28-L28: add the reviewed Russian translation.
🤖 Prompt for 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.

In `@src/package.nls.it.json` at line 19, Localize the
command.openUsageStats.title entry in each affected locale file, replacing the
English value with the reviewed translation: src/package.nls.it.json#L19-L19,
src/package.nls.ja.json#L28-L28, src/package.nls.ko.json#L19-L19,
src/package.nls.nl.json#L28-L28, src/package.nls.pl.json#L19-L19,
src/package.nls.pt-BR.json#L19-L19, and src/package.nls.ru.json#L28-L28.
🤖 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.

Outside diff comments:
In `@src/package.nls.ca.json`:
- Line 19: Translate the command.openUsageStats.title value in each affected
locale: src/package.nls.ca.json#19-19, src/package.nls.fr.json#19-19,
src/package.nls.hi.json#19-19, src/package.nls.id.json#28-28,
src/package.nls.zh-CN.json#19-19, and src/package.nls.zh-TW.json#19-19. Replace
the English title with the appropriate Catalan, French, Hindi, Indonesian,
Simplified Chinese, and Traditional Chinese translations respectively.

In `@src/package.nls.it.json`:
- Line 19: Localize the command.openUsageStats.title entry in each affected
locale file, replacing the English value with the reviewed translation:
src/package.nls.it.json#L19-L19, src/package.nls.ja.json#L28-L28,
src/package.nls.ko.json#L19-L19, src/package.nls.nl.json#L28-L28,
src/package.nls.pl.json#L19-L19, src/package.nls.pt-BR.json#L19-L19, and
src/package.nls.ru.json#L28-L28.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c053aebe-829f-4e24-98ea-544a11684a91

📥 Commits

Reviewing files that changed from the base of the PR and between 00ecf4b and 8ae2c52.

📒 Files selected for processing (21)
  • src/package.nls.ca.json
  • src/package.nls.de.json
  • src/package.nls.es.json
  • src/package.nls.fr.json
  • src/package.nls.hi.json
  • src/package.nls.id.json
  • src/package.nls.it.json
  • src/package.nls.ja.json
  • src/package.nls.ko.json
  • src/package.nls.nl.json
  • src/package.nls.pl.json
  • src/package.nls.pt-BR.json
  • src/package.nls.ru.json
  • src/package.nls.tr.json
  • src/package.nls.vi.json
  • src/package.nls.zh-CN.json
  • src/package.nls.zh-TW.json
  • webview-ui/src/i18n/locales/de/stats.json
  • webview-ui/src/i18n/locales/fr/stats.json
  • webview-ui/src/i18n/locales/vi/stats.json
  • webview-ui/src/i18n/locales/zh-TW/stats.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • webview-ui/src/i18n/locales/zh-TW/stats.json
  • src/package.nls.tr.json
  • src/package.nls.es.json
  • src/package.nls.de.json
  • webview-ui/src/i18n/locales/de/stats.json
  • src/package.nls.vi.json
  • webview-ui/src/i18n/locales/fr/stats.json

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 19, 2026
Comment thread src/core/task/Task.ts Outdated
/**
* Usage 이벤트 기록기. API attempt의 terminal finalize에서만 호출된다.
* store 초기화 실패 시 null이며, 이 경우 기록을 조용히 건너뛴다.
* (아키텍처 보고서 섹션 5.5-5.8, rollback: writer를 optional service로 주입)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use English for comments.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Please wait!! I am adding some new feature here, and i will translate everything into English and upload it again. It's my first time making a PR, so I'm still quite inexperienced!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/services/stats/UsageEventStore.ts (4)

388-394: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

getManifest() can write the manifest without holding the cross-process lock.

getManifest() calls loadOrCreateManifest() (Line 393) directly, but loadOrCreateManifest() performs a write via writeManifestAtomic() (Lines 514, 520-521) whenever the file is missing or fails basic validation. Every other write path (appendInternal, clear) acquires acquireManifestLock() first, but this one doesn't. If the manifest happens to be missing/corrupt at the exact moment getManifest() runs concurrently with a locked append()/clear() writer, this unguarded write can race with and clobber the lock holder's in-flight update.

🔒 Proposed fix
 	async getManifest(): Promise<UsageStatsManifest> {
 		await this.ensureInitialized()
-		return this.loadOrCreateManifest()
+		let releaseLock: (() => Promise<void>) = async () => {}
+		try {
+			releaseLock = await this.acquireManifestLock()
+			return await this.loadOrCreateManifest()
+		} finally {
+			try {
+				await releaseLock()
+			} catch {
+				// ignore
+			}
+		}
 	}
🤖 Prompt for 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.

In `@src/services/stats/UsageEventStore.ts` around lines 388 - 394, Update
getManifest() to acquire and release the same cross-process manifest lock used
by appendInternal and clear before calling loadOrCreateManifest(), ensuring the
lock is released in all outcomes while preserving the existing initialization
and return behavior.

273-293: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Schema-invalid (but syntactically complete) lines on the last line are silently dropped without quarantine.

For the last line, when JSON.parse succeeds but UsageEventV1Schema.safeParse fails, the entry is pushed to quarantineEntries and then immediately popped (Lines 280-284), meaning the corrupt record is neither retained as an event nor recorded to quarantine. The "crash tail" heuristic should only apply to lines that fail JSON.parse (truly truncated writes); a line that parses as valid JSON but fails schema validation is a complete, non-truncated record and represents real corruption that the PR objectives explicitly require to be quarantined.

🐛 Proposed fix
 					if (result.success) {
 						events.push(result.data)
 					} else {
-						// zod validation failed: corrupt line
-						quarantineEntries.push(this.makeQuarantineEntry(segmentFile, lineNum, line))
-						// Validation failure of the last line may be a crash tail, so exclude from quarantine
-						if (isLastLine) {
-							quarantineEntries.pop()
-						}
+						// zod validation failed: the line parsed as JSON but doesn't match the schema,
+						// so it is not a crash tail (a truncated write would fail JSON.parse instead) —
+						// always quarantine it.
+						quarantineEntries.push(this.makeQuarantineEntry(segmentFile, lineNum, line))
 					}
🤖 Prompt for 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.

In `@src/services/stats/UsageEventStore.ts` around lines 273 - 293, Update the
schema-validation failure branch in the UsageEventStore parsing loop so a last
line that passes JSON.parse but fails UsageEventV1Schema.safeParse remains in
quarantineEntries. Remove the isLastLine-based quarantineEntries.pop() behavior
from that branch, while preserving the existing crash-tail exclusion only in the
JSON.parse catch block.

336-357: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Partial clear() failures can mix old (should-be-cleared) events into the new generation, and backups are never cleaned up.

getSegmentPath() (Lines 701-704) builds segment filenames from only the segment number, not the generation. When a rename in this loop fails (Line 351-356), the error is logged and swallowed, execution continues, and the new manifest with the incremented generation is still written (Line 360). The old, un-moved events-000001.ndjson file remains at the same path the new generation will resolve to via getSegmentPath(1), so subsequent appends land in — and readAll()/checkTotalSize() still count — the file that "clear" was supposed to remove. Separately, even on full success, moved files under old-generation-{N}/ are never deleted; each clear() call permanently grows disk usage outside the TOTAL_MAX_BYTES accounting, undermining both the hard-cap design and the user's expectation that "clear" actually deletes data.

Simplify by deleting segment files directly instead of moving them to a backup directory — this removes the collision risk and the unbounded backup growth in one change:

♻️ Proposed fix
-			// Move existing segment files to a new generation directory (backup)
-			// Or simply replace with a new manifest and ignore existing files
-			// Design: "Replace existing segments with a new empty generation"
-			// Implementation: Move existing segment files under old-generation-{N}
-			const oldGenDir = path.join(this.statsDir, `old-generation-${manifest.generation}`)
-			await fs.mkdir(oldGenDir, { recursive: true })
-
-			const allFiles = await fs.readdir(this.statsDir)
+			const allFiles = await fs.readdir(this.statsDir)
 			const segmentFiles = allFiles.filter(
 				(f) => f.startsWith(SEGMENT_PREFIX) && f.endsWith(SEGMENT_EXT),
 			)
 
 			for (const file of segmentFiles) {
-				const oldPath = path.join(this.statsDir, file)
-				const newPath = path.join(oldGenDir, file)
+				const oldPath = path.join(this.statsDir, file)
 				try {
-					await fs.rename(oldPath, newPath)
+					await fs.unlink(oldPath)
 				} catch (err) {
-					// Log move failures and continue
-					console.warn(`[UsageEventStore] failed to move old segment ${file}:`, err)
+					// Delete failure: fail the clear so the caller/UI can retry rather than
+					// silently mixing stale data with the new generation.
+					throw err
 				}
 			}

If preserving old-generation events indefinitely for audit purposes is intentional, then at minimum incorporate the generation number into getSegmentPath() so collisions can't occur, and add cleanup/retention for old-generation-* directories.

🤖 Prompt for 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.

In `@src/services/stats/UsageEventStore.ts` around lines 336 - 357, Update the
clear() implementation to delete matching segment files directly from statsDir
instead of creating old-generation-{N} backup directories and renaming files.
Remove the backup-directory creation and move loop, and ensure deletion failures
are handled consistently so clear does not leave old segments at paths resolved
by getSegmentPath().

577-581: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid rethrowing from onCompromised. A compromised lock here can bubble out as an unhandled exception and take down the extension host. Log it and return instead of throwing so this optional stats path stays non-fatal.

🤖 Prompt for 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.

In `@src/services/stats/UsageEventStore.ts` around lines 577 - 581, Update the
onCompromised callback in UsageEventStore so it logs the compromised-lock error
and returns without rethrowing it. Keep the existing diagnostic logging,
ensuring this optional stats path cannot propagate the exception to the
extension host.
🤖 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.

Outside diff comments:
In `@src/services/stats/UsageEventStore.ts`:
- Around line 388-394: Update getManifest() to acquire and release the same
cross-process manifest lock used by appendInternal and clear before calling
loadOrCreateManifest(), ensuring the lock is released in all outcomes while
preserving the existing initialization and return behavior.
- Around line 273-293: Update the schema-validation failure branch in the
UsageEventStore parsing loop so a last line that passes JSON.parse but fails
UsageEventV1Schema.safeParse remains in quarantineEntries. Remove the
isLastLine-based quarantineEntries.pop() behavior from that branch, while
preserving the existing crash-tail exclusion only in the JSON.parse catch block.
- Around line 336-357: Update the clear() implementation to delete matching
segment files directly from statsDir instead of creating old-generation-{N}
backup directories and renaming files. Remove the backup-directory creation and
move loop, and ensure deletion failures are handled consistently so clear does
not leave old segments at paths resolved by getSegmentPath().
- Around line 577-581: Update the onCompromised callback in UsageEventStore so
it logs the compromised-lock error and returns without rethrowing it. Keep the
existing diagnostic logging, ensuring this optional stats path cannot propagate
the exception to the extension host.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e949947b-7e79-46a2-a92d-8e27b57db58d

📥 Commits

Reviewing files that changed from the base of the PR and between 8ae2c52 and b5b0ef2.

📒 Files selected for processing (13)
  • packages/types/src/usage-stats.ts
  • src/core/task/Task.ts
  • src/core/task/__tests__/Task.usage-stats.spec.ts
  • src/services/stats/UsageAggregator.ts
  • src/services/stats/UsageEventStore.ts
  • src/services/stats/UsageRecorder.ts
  • src/services/stats/UsageStatsService.ts
  • src/services/stats/__tests__/UsageAggregator.spec.ts
  • src/services/stats/__tests__/UsageEventStore.spec.ts
  • src/services/stats/__tests__/UsageStatsService.spec.ts
  • src/services/stats/index.ts
  • webview-ui/src/components/stats/__tests__/StatsView.spec.tsx
  • webview-ui/src/components/stats/__tests__/UsageHeatmap.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/services/stats/index.ts
  • src/services/stats/tests/UsageEventStore.spec.ts
  • packages/types/src/usage-stats.ts
  • src/core/task/tests/Task.usage-stats.spec.ts
  • webview-ui/src/components/stats/tests/UsageHeatmap.spec.tsx
  • src/services/stats/UsageRecorder.ts
  • src/services/stats/tests/UsageStatsService.spec.ts
  • src/services/stats/UsageStatsService.ts
  • src/core/task/Task.ts
  • webview-ui/src/components/stats/tests/StatsView.spec.tsx
  • src/services/stats/UsageAggregator.ts

@zoomote

zoomote Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

4 issues outstanding (1 high, 4 medium, 5 low total).

Code review — /stats usage dashboard

Reviewed the full diff (95 files, ~12k insertions) against main: stats types, UsageEventStore/UsageAggregator/UsageRecorder/UsageStatsService, the webview message handlers, Task.ts instrumentation, provider cost changes, and the dashboard UI. Storage design (segmented NDJSON, manifest lock, idempotency, quarantine) is solid. Findings below, highest impact first.

🔴 High

  1. Usage heatmap can never display data. DashboardView renders <UsageHeatmap buckets={buckets} /> (webview-ui/src/components/dashboard/DashboardView.tsx:720) with buckets from a query whose groupBy is only model | provider | mode (DashboardView.tsx:178). But UsageHeatmap only reads bucket.key.day (webview-ui/src/components/stats/UsageHeatmap.tsx:75-77), which never exists in those buckets — so the heatmap permanently renders the "no data" state. Either issue a separate groupBy: ["day"] query for the heatmap or derive daily activity from the sessions/events data.

🟡 Medium

  1. Estimated costs are recorded as provider-sourced. The provider changes compute totalCost locally from user-configured pricing (calculateApiCostOpenAI/calculateApiCostAnthropic), i.e. an estimate — but the recording context in Task.ts hardcodes costSource: "provider" and tokenSource: "provider" (src/core/task/Task.ts:3163-3167 and 3305-3312). The event schema and the aggregator's source axis exist precisely to distinguish this; as written, every event claims provider-reported cost and the source grouping/coverage is meaningless.

  2. Session detail gets stuck in loading forever if collapsed mid-fetch. The dashboardSessionDetailResponse handler correlates via expandedTaskId and returns early when nothing is expanded (DashboardView.tsx:418-419), so the response is dropped without clearing sessionDetailLoading. The re-expand guard !sessionDetailLoading.has(taskId) (DashboardView.tsx:285) then blocks any refetch — re-expanding that row shows a spinner permanently. Clear loading keyed by the response's task (consider echoing taskId in the response) rather than gating on expandedTaskId.

  3. Session filter dropdowns collapse to the filtered values. uniqueModels/uniqueProviders are derived from the already-filtered sessions list (webview-ui/src/components/dashboard/SessionList.tsx:271-281), and the host applies the filters before responding. After filtering by model X, the model dropdown only offers X + "All" — you can't switch directly to model Y without going through "All" first. Derive the option lists from the unfiltered session set.

  4. Sequential per-session file reads on the sessions path. buildSessionSummaries awaits deriveSessionTitlereadTaskMessages (disk I/O per task) serially for every session in range (src/core/webview/usageStatsMessageHandler.ts:482-514). With a few hundred sessions this makes getDashboardSessions noticeably slow; consider bounded concurrency (or caching titles per taskId).

🟢 Low

  1. Dead IPC surface. getDashboardStats / dashboardStatsResponse are declared in packages/types/src/vscode-extension-host.ts but have no handler case and no producer; the "stats" value added to the tab union is likewise unused (App.tsx only handles "dashboard").
  2. Timezone helpers in the aggregator. toTimezoneDate (src/services/stats/UsageAggregator.ts:184-212) is a no-op — the offset math cancels out and it returns the input instant; startOfDay computes an unused tzDate (line 251). Also, resolveTimeRange (and the duplicated resolvePresetRange in UsageStatsService.ts:293-324) advances range ends with host-local setDate(+1) rather than the next midnight in the query timezone, which is off by an hour on DST-transition days.
  3. Cancel-path recording race. On cancellation, both the catch block (src/core/task/Task.ts:3286-3324) and the background usage drain call finalizeUsageEvent with the same request key; whichever runs first wins the idempotency check, so the sparser partial-token event from the catch path can suppress the drain's more complete one.
  4. Telemetry cost fallback defeated in the edge case. With providers now always emitting totalCost (0 when modelInfo is unavailable), tokens.total ?? costResult.totalCost (src/core/task/Task.ts:3135) can record 0 instead of the computed fallback cost.
  5. Stale PR description. The body describes a /stats slash command, StatsView/StatsSummary components, ChatView interception, and missing non-English locales — all superseded by the dashboard implementation in the later commits. Worth refreshing so reviewers and release notes match the code.
  • 🔴 DashboardView.tsx:720 / UsageHeatmap.tsx:75 — heatmap never receives day-grouped buckets; always renders empty
  • 🟡 src/core/task/Task.ts:3163 — locally estimated cost recorded with costSource: "provider"
  • 🟡 DashboardView.tsx:418 — session detail response dropped when collapsed; row stuck loading permanently
  • 🟡 SessionList.tsx:271 — filter dropdown options derived from already-filtered sessions
  • 🟡 usageStatsMessageHandler.ts:501 — serial per-session readTaskMessages I/O in sessions handler
  • 🟢 vscode-extension-host.ts — unused getDashboardStats/dashboardStatsResponse/"stats" tab types
  • 🟢 UsageAggregator.ts:184 — no-op toTimezoneDate, unused tzDate, host-local setDate range math (DST edge)
  • 🟢 src/core/task/Task.ts:3286 — cancel-path vs background-drain recording race; sparser event can win
  • 🟢 src/core/task/Task.ts:3135 — telemetry cost fallback defeated when provider emits totalCost: 0
  • 🟢 PR body — stale description of /stats command and superseded components

Reviewed 509346d

@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 19, 2026
@myk1yt myk1yt changed the title feat(stats): add /stats local usage statistics command feat(dashboard): add local usage statistics dashboard with session detail Jul 20, 2026
@myk1yt
myk1yt force-pushed the feature/local-usage-stats branch from 852be5d to b3e90bf Compare July 20, 2026 00:13
@taltas

taltas commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@myk1yt Thanks for the PR.
Can you include some screenshots in the PR description when this PR is ready?

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Local Usage Statistics Dashboard

4 participants