Skip to content

feat: user dashboard#1354

Merged
zerob13 merged 7 commits intodevfrom
codex/user-summary
Mar 17, 2026
Merged

feat: user dashboard#1354
zerob13 merged 7 commits intodevfrom
codex/user-summary

Conversation

@zerob13
Copy link
Copy Markdown
Collaborator

@zerob13 zerob13 commented Mar 17, 2026

image image

Summary by CodeRabbit

  • New Features

    • Settings › Dashboard added: token usage overview with input/output/cached tokens, estimated cost, 365-day calendar, provider & model breakdowns, and a refresh action.
    • Live recording and one-time historical backfill to populate dashboard data; backfill status shown (idle/running/completed/failed).
    • New dashboard UI pages and charts (donut hero, area/candles, stacked bars) integrated into Settings.
  • Dependencies

    • Added Unovis charting libraries.
  • Internationalization

    • Dashboard translations added for all supported locales.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 17, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a Settings Dashboard feature: records per-message usage (including cached tokens), introduces a deepchat_usage_stats table, backfills historical messages, records live usage on finalize/error, exposes presenter queries, and ships a Vue dashboard UI with charts, calendar heatmap, and provider/model breakdowns.

Changes

Cohort / File(s) Summary
Docs & Tasks
docs/specs/settings-dashboard/plan.md, docs/specs/settings-dashboard/spec.md, docs/specs/settings-dashboard/tasks.md
Feature plan, spec, and task list describing data model, backfill, live recording, dashboard queries, UI layout, and implementation tasks.
Database & Tables
src/main/presenter/sqlitePresenter/tables/deepchatUsageStats.ts, src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts, src/main/presenter/sqlitePresenter/index.ts
Add deepchat_usage_stats table, analytics queries, upsert by message_id, candidate listing from messages, and integrate table lifecycle into SQLitePresenter (migrations, cleanup).
Usage Utilities
src/main/presenter/usageStats.ts, src/shared/types/agent-interface.d.ts
New usage utilities for parsing metadata, building usage records, cost estimation, calendar/breakdown builders, backfill status helpers; add dashboard-related types and cachedInputTokens metadata field.
Presenter Backfill & Queries
src/main/presenter/newAgentPresenter/index.ts, src/main/presenter/lifecyclePresenter/hooks/...
New NewAgentPresenter methods: startUsageStatsBackfill() and getUsageDashboard() plus backfill status management; lifecycle hook to trigger non-blocking backfill after start.
Message Recording & Accumulation
src/main/presenter/deepchatAgentPresenter/messageStore.ts, src/main/presenter/deepchatAgentPresenter/accumulator.ts, src/main/presenter/deepchatAgentPresenter/process.ts
Persist usage stats on assistant finalize/error (upsert live rows); capture and store cached token counts in message metadata and snapshots.
LLM Provider Changes
src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts, src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts, src/shared/types/core/llm-events.ts
Extract cached_tokens from OpenAI responses and propagate it in usage stream events; update types to include optional cached_tokens.
Renderer / UI
src/renderer/settings/components/DashboardSettings.vue, src/renderer/settings/main.ts
New DashboardSettings Vue component and route with charts, calendar heatmap, summary cards, breakdowns, loading/error/backfill states, and i18n usage.
Charting Primitives
src/shadcn/components/ui/chart/*, src/shadcn/components/ui/chart/index.ts, src/shadcn/components/ui/chart/utils.ts
New chart container, legend, tooltip, style components, chart context/composables, and component-to-string utility; exports Unovis wrappers.
i18n
src/renderer/src/i18n/*/*/routes.json, src/renderer/src/i18n/*/*/settings.json
Add settings-dashboard route label and large dashboard translation blocks across multiple locales; adjust a notifications/hooks title to Hooks.
Types & Presenter API
src/shared/types/presenters/new-agent.presenter.d.ts, src/shared/types/agent-interface.d.ts
Add getUsageDashboard() to INewAgentPresenter and expose UsageDashboardData / backfill status types.
Tests
test/**/*usageDashboard*.test.ts, test/renderer/components/DashboardSettings.test.ts, others
Unit tests added/updated: backfill flow, live upserts, dashboard aggregation, UI rendering, chart/component tests, and cached token assertions.
Deps
package.json
Add devDependencies @unovis/ts@1.6.4 and @unovis/vue@1.6.4 for charting.

Sequence Diagram(s)

sequenceDiagram
    participant App as App / UI
    participant Lifecycle as Lifecycle Manager
    participant NAP as NewAgentPresenter
    participant SQLite as SQLitePresenter
    participant Messages as deepchat_messages
    participant Stats as deepchat_usage_stats

    App->>Lifecycle: App starts
    Lifecycle->>NAP: startUsageStatsBackfill() (async, non-blocking)
    activate NAP
    NAP->>SQLite: listAssistantUsageCandidates()
    SQLite->>Messages: query assistant messages + sessions
    Messages-->>SQLite: return rows
    SQLite-->>NAP: return candidates
    loop per message candidate
      NAP->>NAP: parse metadata, resolve provider/model
      NAP->>NAP: build usage record
      NAP->>SQLite: upsert(record)
      SQLite->>Stats: INSERT ON CONFLICT
      Stats-->>SQLite: OK
    end
    NAP->>NAP: set backfill status completed
    deactivate NAP

    App->>NAP: getUsageDashboard()
    activate NAP
    NAP->>SQLite: getSummary(), getDailyCalendarRows(), getProviderBreakdownRows()
    SQLite->>Stats: aggregate queries
    Stats-->>SQLite: results
    SQLite-->>NAP: aggregated data
    NAP-->>App: UsageDashboardData
    deactivate NAP
Loading
sequenceDiagram
    participant LLM as LLM Provider
    participant Stream as Event Stream
    participant Acc as Accumulator
    participant Store as MessageStore
    participant SQLite as SQLitePresenter

    LLM->>Stream: emit usage event (input_tokens, output_tokens, cached_tokens...)
    Stream->>Acc: forward usage
    Acc->>Acc: store cachedInputTokens in metadata
    Acc->>Store: finalize assistant message with metadata
    activate Store
    Store->>Store: parse metadata, resolve provider/model
    Store->>SQLite: upsert deepchat_usage_stats (source='live')
    SQLite-->>Store: OK
    deactivate Store
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • deepinfect

Poem

🐰 Tokens hopped in, cached and bright,
I tallied them all through day and night.
Charts and calendars, colors that gleam,
A dashboard to show the usage dream.
Hooray — now metrics dance in moonlight!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: user dashboard' is vague and does not clearly describe the main change. It uses the generic term 'user dashboard' without specifying what functionality or feature is being added. Consider a more descriptive title that specifies the dashboard's purpose, such as 'feat: add usage dashboard with token statistics and contribution calendar' or 'feat: implement token usage and activity dashboard'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/user-summary
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed336bf56f

ℹ️ 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".

return
}

void newAgentPresenter.startUsageStatsBackfill().catch((error) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serialize usage backfill after legacy import completion

In the AFTER_START phase, this launches startUsageStatsBackfill() as fire-and-forget, but the existing legacyImportHook (priority 20) also starts import asynchronously and returns immediately; this means both jobs run concurrently on first upgrade. Since runUsageStatsBackfill() takes a one-time snapshot with deepchatMessagesTable.listAssistantUsageCandidates() and then marks backfill completed, any legacy messages imported after that snapshot never get rows in deepchat_usage_stats, so migrated users can see permanently undercounted dashboard totals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

🧹 Nitpick comments (8)
src/renderer/src/i18n/ko-KR/routes.json (1)

19-19: Consider using Korean for "Dashboard".

Other route labels in this file are translated to Korean. For consistency, consider using "대시보드" (Korean transliteration) instead of leaving it in English.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/i18n/ko-KR/routes.json` at line 19, The value for the route
key "settings-dashboard" is in English; update the JSON entry for
"settings-dashboard" in src/renderer/src/i18n/ko-KR/routes.json to use the
Korean translation "대시보드" so it matches the other route labels (i.e., replace
"Dashboard" with "대시보드" for the "settings-dashboard" key).
src/renderer/src/i18n/ja-JP/routes.json (1)

19-19: Consider using Japanese katakana for "Dashboard".

Other English loanwords in this file are transliterated to katakana (e.g., "プレイグラウンド" for Playground, "プロバイダー" for Provider). For consistency, consider using "ダッシュボード" instead of "Dashboard".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/i18n/ja-JP/routes.json` at line 19, Replace the English
string value for the "settings-dashboard" key in the routes.json i18n file with
the katakana transliteration "ダッシュボード" to match the other loanword translations
(e.g., "プレイグラウンド", "プロバイダー") and maintain consistency across Japanese locale
strings.
src/renderer/src/i18n/he-IL/routes.json (1)

19-19: Consider translating "Dashboard" to Hebrew for consistency.

Other route labels in this file are translated to Hebrew (e.g., "הגדרות", "ספקים", "נתונים"), but "Dashboard" remains in English. Consider using a Hebrew equivalent like "לוח בקרה" or "לוח מחוונים" for a consistent user experience.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/i18n/he-IL/routes.json` at line 19, The "settings-dashboard"
route label is left in English; update the translation value for the key
"settings-dashboard" to a Hebrew equivalent (e.g., "לוח בקרה" or "לוח מחוונים")
so it matches the other translated route labels; edit the value for the JSON key
"settings-dashboard" in the routes localization file (the entry currently
"settings-dashboard": "Dashboard") and replace "Dashboard" with the chosen
Hebrew string, ensuring valid JSON quoting and encoding.
src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts (1)

197-215: Consider making usage-candidate scanning index-backed and chunked.

Line 197 currently returns all assistant rows sorted by created_at in one shot. This can get expensive as message history grows. Consider adding an index on (role, created_at) and iterating in batches for backfill paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts` around lines
197 - 215, The current listAssistantUsageCandidates method returns all assistant
rows in one shot which will not scale; add a DB index on (role, created_at)
(e.g., via migration or table DDL for deepchat_messages) and change
listAssistantUsageCandidates to scan in chunks using a deterministic cursor (use
created_at and id) with ORDER BY created_at ASC and a LIMIT batchSize, iterating
until no more rows (or expose a paginated API that accepts last_seen_created_at
and last_seen_id); reference the method listAssistantUsageCandidates, table
deepchat_messages, and the (role, created_at) index when implementing these
changes.
src/shadcn/components/ui/chart/utils.ts (1)

7-7: Consider bounding the cache size to prevent unbounded memory growth.

A process-wide Map without eviction can grow for long-running sessions with high-cardinality tooltip payloads.

♻️ Suggested bounded cache pattern
 const cache = new Map<string, string>()
+const MAX_CACHE_ENTRIES = 500
@@
-    cache.set(serializedKey, div.innerHTML)
+    if (cache.size >= MAX_CACHE_ENTRIES) {
+      const oldestKey = cache.keys().next().value as string | undefined
+      if (oldestKey) cache.delete(oldestKey)
+    }
+    cache.set(serializedKey, div.innerHTML)

Also applies to: 41-41

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shadcn/components/ui/chart/utils.ts` at line 7, The global Map named
cache in utils.ts can grow without bound; replace it with a bounded cache
implementation (e.g., an LRU or simple capped Map) and introduce a
MAX_CACHE_SIZE constant, evicting the oldest entry when size > MAX_CACHE_SIZE to
prevent unbounded memory growth; update/reuse the new bounded cache API wherever
the original cache is referenced (and apply the same change to the other Map
instance noted at the second occurrence) so callers (same get/set/delete
semantics) continue to work.
src/main/presenter/usageStats.ts (1)

150-168: Edge case: cachedInputTokens may exceed inputTokens when inputTokens is 0.

Line 160 only clamps cachedInputTokens when inputTokens > 0. If inputTokens is 0 but rawCached has a positive value (due to data inconsistency), cachedInputTokens will be unclamped.

Consider always clamping to ensure cachedInputTokens <= inputTokens:

♻️ Proposed fix
 const rawCached = toNonNegativeInteger(metadata.cachedInputTokens) ?? 0
-const cachedInputTokens = inputTokens > 0 ? Math.min(rawCached, inputTokens) : rawCached
+const cachedInputTokens = Math.min(rawCached, inputTokens)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/usageStats.ts` around lines 150 - 168, The clamp for
cachedInputTokens in normalizeUsageCounts is currently only applied when
inputTokens > 0, which allows rawCached to exceed inputTokens when inputTokens
is 0; change the logic in normalizeUsageCounts so cachedInputTokens is always
clamped to be no greater than inputTokens (e.g., set cachedInputTokens =
Math.min(rawCached, inputTokens) after computing inputTokens and rawCached)
while keeping the existing non-negative conversions via toNonNegativeInteger.
src/main/presenter/newAgentPresenter/index.ts (1)

1417-1425: Side effect in getter method may cause unexpected behavior.

getUsageStatsBackfillStatus has a hidden side effect on lines 1421-1423 where it writes back to config when the status is 'failed' with a timeout error. This violates the principle of least surprise for a getter method.

Consider either:

  1. Moving this persistence to normalizeUsageStatsBackfillStatus with a callback
  2. Making this a separate recovery/normalization step called explicitly
♻️ Proposed refactor to separate concerns
 private getUsageStatsBackfillStatus(): UsageStatsBackfillStatus {
-  const normalized = this.normalizeUsageStatsBackfillStatus(
+  const raw = this.configPresenter.getSetting<UsageStatsBackfillStatus>(DASHBOARD_STATS_BACKFILL_KEY)
+  const normalized = this.normalizeUsageStatsBackfillStatus(raw)
+
+  // Persist timeout recovery as a separate explicit step
+  if (normalized.status === 'failed' && normalized.error === 'Usage stats backfill timed out') {
+    this.setUsageStatsBackfillStatus(normalized)
+  }
+
+  return normalized
+}
-    this.configPresenter.getSetting<UsageStatsBackfillStatus>(DASHBOARD_STATS_BACKFILL_KEY)
-  )
-  if (normalized.status === 'failed' && normalized.error === 'Usage stats backfill timed out') {
-    this.configPresenter.setSetting(DASHBOARD_STATS_BACKFILL_KEY, normalized)
-  }
-  return normalized
-}

Actually, the current code is already structured this way. The concern is that calling getSetting followed by setSetting in a getter is a side effect. Consider adding a comment to document this intentional behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/newAgentPresenter/index.ts` around lines 1417 - 1425,
getUsageStatsBackfillStatus currently performs an unexpected write via
configPresenter.setSetting when normalizeUsageStatsBackfillStatus returns a
timed-out failed status; remove that side effect from the getter by extracting
the persistence into a dedicated recovery/normalization function (e.g., create
recoverOrPersistUsageStatsBackfillStatus or extend
normalizeUsageStatsBackfillStatus to accept a callback) and have callers invoke
that explicit recovery step instead of relying on getUsageStatsBackfillStatus;
ensure references to normalizeUsageStatsBackfillStatus,
configPresenter.setSetting, and DASHBOARD_STATS_BACKFILL_KEY are updated to the
new method so the getter becomes side-effect free.
src/main/presenter/sqlitePresenter/tables/deepchatUsageStats.ts (1)

118-165: Upsert overwrites created_at on conflict - consider preserving the original value.

The ON CONFLICT DO UPDATE clause currently replaces created_at with the new value. Typically, created_at should remain immutable to reflect when the record was first inserted. Only updated_at should change on updates.

♻️ Proposed fix to preserve original created_at
         ON CONFLICT(message_id) DO UPDATE SET
           session_id = excluded.session_id,
           usage_date = excluded.usage_date,
           provider_id = excluded.provider_id,
           model_id = excluded.model_id,
           input_tokens = excluded.input_tokens,
           output_tokens = excluded.output_tokens,
           total_tokens = excluded.total_tokens,
           cached_input_tokens = excluded.cached_input_tokens,
           estimated_cost_usd = excluded.estimated_cost_usd,
           source = excluded.source,
-          created_at = excluded.created_at,
           updated_at = excluded.updated_at`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/sqlitePresenter/tables/deepchatUsageStats.ts` around lines
118 - 165, The upsert currently overwrites created_at on conflict in the upsert
method of deepchat_usage_stats; change the ON CONFLICT(...) DO UPDATE clause in
upsert (function upsert) so created_at is preserved by either removing
created_at from the SET list or explicitly setting created_at =
deepchat_usage_stats.created_at (the existing table value), while keeping
updated_at = excluded.updated_at and the other fields updated from excluded;
update the SQL string used in upsert to implement this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/deepchatAgentPresenter/messageStore.ts`:
- Around line 388-432: persistUsageStats currently lets exceptions from
parseMessageMetadata, resolveUsageProviderId/ModelId, buildUsageStatsRecord, or
usageStatsTable.upsert bubble up and potentially break message lifecycle writes;
wrap the body of persistUsageStats in a try/catch that catches any error, logs
it (without throwing) and returns early so analytics failures are swallowed.
Specifically, surround calls to parseMessageMetadata, resolveUsageProviderId,
resolveUsageModelId, buildUsageStatsRecord and usageStatsTable.upsert inside the
try block, and on catch use the presenter logger to record the error and exit
the function gracefully. Ensure existing early-return checks (e.g.,
deepchatUsageStatsTable, deepchatMessagesTable, role check, compaction check,
missing provider/model, and falsy usageRecord) remain in place before or inside
the guarded block so normal skips still occur.

In `@src/renderer/settings/components/DashboardSettings.vue`:
- Around line 882-894: The finally block in loadDashboard() calls
syncNostalgiaRotation() and scheduleRefresh() even if the component has been
unmounted (getUsageDashboard() may resolve after unmount); add a mounted check
and skip scheduling when unmounted: introduce a boolean (e.g., isUnmounted or
isMounted) set via onUnmounted/onMounted, then in loadDashboard()’s finally
return early if unmounted before calling syncNostalgiaRotation() and
scheduleRefresh(); apply the same guard to the other similar async function
referenced around lines 1157-1166.

In `@src/renderer/src/i18n/da-DK/settings.json`:
- Around line 135-208: The dashboard i18n block contains several untranslated
English strings; update the values for keys under "dashboard" (e.g., "badge",
"actions.refresh", "error.title", "error.description", "empty.title",
"empty.description", "summary.recordingStartedAt",
"summary.recordingStartedAtDescription", "calendar.title",
"calendar.description", "calendar.legend", "calendar.tooltip", and any other
English text in "summary" and "calendar") to proper Danish translations so the
entire "dashboard" section is fully localized and consistent with surrounding
Danish entries.

In `@src/renderer/src/i18n/fa-IR/routes.json`:
- Line 19: Replace the English label for the dashboard route by updating the
"settings-dashboard" value in the fa-IR routes JSON to its Persian translation
(e.g., "داشبورد") so the navigation label is localized consistently; locate the
"settings-dashboard" key and change its value from "Dashboard" to the Persian
string.

In `@src/renderer/src/i18n/fa-IR/settings.json`:
- Around line 189-262: Several dashboard strings remain in English; translate
all English values under the "dashboard" object to fa-IR to ensure consistency.
Specifically update keys including "badge", "title", "actions.refresh",
"error.title", "error.description", all "summary" labels and descriptions (e.g.,
"totalTokens", "cachedTokens", "cacheHitRate", "estimatedCost",
"recordingStartedAt", "withDeepChatDays*", "tokenUsage", "nostalgia*", etc.),
the "calendar" object ("title", "description", "legend", "tooltip"), "breakdown"
object ("providerTitle", "providerDescription", "modelTitle",
"modelDescription", "messages", "empty"), and "unavailable" — replace the
English phrases with proper Persian translations while preserving placeholders
like {days}, {date}, {count}, and {tokens}.

In `@src/renderer/src/i18n/fr-FR/routes.json`:
- Line 19: Replace the English value for the JSON key "settings-dashboard" with
the French translation "Tableau de bord" so the entry becomes localized
consistently with the rest of the file; update the value for
"settings-dashboard" in routes.json (the i18n entry) to "Tableau de bord".

In `@src/renderer/src/i18n/fr-FR/settings.json`:
- Around line 189-262: The dashboard French locale contains many untranslated
keys—update the values for dashboard.badge and dashboard.title to "Tableau de
bord d'utilisation", dashboard.actions.refresh to "Actualiser",
dashboard.error.title/description to French equivalents, dashboard.empty.title
to "Pas encore de statistiques d'utilisation" and translate the untranslated
summary keys (e.g., summary.totalTokens, summary.cachedTokens,
summary.cacheHitRate, summary.estimatedCost, summary.recordingStartedAt,
withDeepChatDaysSentence, nostalgia* labels/details) and calendar keys
(calendar.title, calendar.description, calendar.legend, calendar.tooltip) so all
strings in the dashboard block are proper French translations consistent with
the file's style.

In `@src/renderer/src/i18n/he-IL/settings.json`:
- Around line 189-262: The dashboard locale block contains untranslated English
keys (e.g., dashboard.badge, dashboard.title, dashboard.actions.refresh,
dashboard.error.title, dashboard.error.description, dashboard.empty.title,
dashboard.summary.totalTokens, dashboard.summary.cachedTokens,
dashboard.summary.cacheHitRate, dashboard.summary.estimatedCost,
dashboard.summary.recordingStartedAt, dashboard.calendar.title,
dashboard.calendar.description, dashboard.calendar.legend,
dashboard.calendar.tooltip); update these values to Hebrew translations
consistent with the surrounding file (translate the badge/title "Usage
Dashboard", "Refresh", error and empty titles/descriptions, all summary
labels/descriptions, and calendar strings) so the dashboard section is fully
localized for he-IL while preserving interpolation tokens like {days}, {count},
and {date}.

In `@src/renderer/src/i18n/ja-JP/settings.json`:
- Around line 190-261: The locale file contains untranslated English strings
(e.g., keys "badge", "title", "actions.refresh", "error.title",
"error.description", "summary.totalTokens" and related "summary.*" labels,
"calendar.title"/"calendar.description", "breakdown.*", "unavailable") causing
inconsistent UX; translate those values into Japanese, keeping placeholders
intact (e.g., "{days}", "{count}", "{date}", "{tokens}"), and ensure phrasing
matches existing Japanese style in other entries (use consistent terminology for
"token", "推定コスト", "キャッシュ", "リフレッシュ"/"更新" etc.) so each key above is replaced
with an appropriate Japanese string while preserving interpolation tokens and
punctuation.

In `@src/renderer/src/i18n/ko-KR/settings.json`:
- Around line 189-262: The dashboard localization mixes English and Korean;
update the ko-KR "dashboard" block to provide Korean translations (or remove the
English keys to rely on fallback) for the untranslated keys: "badge", "title",
"actions.refresh", "error.title", "error.description", "calendar.title",
"calendar.description", "calendar.legend", "calendar.tooltip", and
"unavailable", and verify summary labels like "totalTokens", "cachedTokens",
"cacheHitRate", "estimatedCost", "recordingStartedAt" are all properly
translated or intentionally left out; edit the JSON entries under the
"dashboard" object (e.g., dashboard.badge, dashboard.actions.refresh,
dashboard.error.title, dashboard.calendar.title, dashboard.unavailable) to
supply consistent Korean text.

In `@src/renderer/src/i18n/pt-BR/settings.json`:
- Around line 189-262: Translate the remaining English strings in the pt-BR
localization under the JSON object keys in "dashboard" (e.g., dashboard.badge,
dashboard.title, dashboard.actions.refresh, dashboard.error.title,
dashboard.error.description, dashboard.empty.title,
dashboard.summary.totalTokens, dashboard.summary.cachedTokens,
dashboard.summary.cachedTokensDescription, dashboard.summary.cacheHitRate,
dashboard.summary.cacheHitRateDescription, dashboard.summary.recordingStartedAt,
dashboard.calendar.title, dashboard.calendar.description,
dashboard.calendar.legend, dashboard.calendar.tooltip) into Brazilian Portuguese
(for example "Usage Dashboard" -> "Painel de Uso", "Refresh" -> "Atualizar",
"Failed to load dashboard" -> "Falha ao carregar o painel", etc.), ensuring
phrasing matches existing translated entries and keeping placeholder tokens like
{days}, {count}, and {date} intact.

In `@src/renderer/src/i18n/ru-RU/routes.json`:
- Line 19: The "settings-dashboard" route label in ru-RU locale is still in
English; open src/renderer/src/i18n/ru-RU/routes.json and replace the value for
the "settings-dashboard" key ("Dashboard") with a Russian translation (e.g.,
"Панель" or "Панель управления") so the navigation text is fully localized.

In `@src/renderer/src/i18n/ru-RU/settings.json`:
- Around line 189-262: Several dashboard locale keys remain in English; update
the listed keys to Russian to match the rest of the locale. Replace "badge" and
"title" with a Russian equivalent (e.g., "Панель использования"), change
"actions.refresh" to "Обновить", translate "error.title" and
"error.description", "empty.title", and all summary keys such as "totalTokens",
"cachedTokens", "cachedTokensDescription", "cacheHitRate", "estimatedCost" and
their descriptions, plus the calendar keys ("calendar.title",
"calendar.description", "calendar.legend", "calendar.tooltip") into Russian;
ensure you update the exact JSON keys shown in the diff (badge, title,
actions.refresh, error.title, error.description, empty.title, summary.* keys,
calendar.*) so the ru-RU settings.json contains Russian strings for those
entries.

In `@src/renderer/src/i18n/zh-CN/settings.json`:
- Around line 248-257: Replace the remaining English headings in the zh-CN
locale: change the "title" value "Contribution Calendar" under the contribution
calendar block to a Chinese equivalent (e.g., "贡献日历"), and update
"breakdown.providerTitle" ("Provider 排行") and "breakdown.modelTitle" ("Model
排行") to fully Chinese terms (e.g., "提供者排行" and "模型排行" or preferred translations)
so the keys "title", "breakdown.providerTitle", and "breakdown.modelTitle" are
fully localized.

In `@src/renderer/src/i18n/zh-TW/settings.json`:
- Around line 189-262: The dashboard locale includes English text in the
calendar block; replace the English values for keys calendar.title,
calendar.description and calendar.tooltip (and any other remaining English
strings in the dashboard object such as any stray "Contribution Calendar"
occurrences) with proper zh-TW translations so the UI is fully localized (e.g.,
use a zh-TW title like "貢獻日曆", a description like "最近 365 天按日統計的 token 消耗。", and
a tooltip format like "{date}:{tokens} token" or its localized equivalent),
ensuring all dashboard.* keys contain only zh-TW strings.

In `@src/shadcn/components/ui/chart/ChartContainer.vue`:
- Around line 30-33: The context and slot are exposing the raw uniqueId while
the DOM and selectors use the computed chartId; update the call to
provideChartContext and the slot props so they receive chartId instead of
uniqueId (use the computed chartId value generated from props.id ||
uniqueId.replace(/:/g, "")). Specifically, change the argument passed to
provideChartContext (and the value emitted to default slot) from uniqueId to
chartId so useChart().id matches the DOM data-chart attribute and downstream
components like ChartLegendContent can build selectors successfully.

In `@src/shadcn/components/ui/chart/ChartLegendContent.vue`:
- Around line 19-24: The computed payload mapping in the payload constant
currently overrides each item's unique key with props.nameKey when provided,
which causes duplicate v-for keys; update the payload computation (the computed
named payload that reads config.value) to keep the original entry key as the
unique key (remove the fallback to props.nameKey so key remains the entry key)
and still include props.nameKey only where needed (e.g., as a separate
displayName field if desired) so v-for keys remain unique and DOM patching is
correct.

In `@src/shadcn/components/ui/chart/ChartStyle.vue`:
- Around line 22-29: The template builds CSS rules using an unquoted attribute
selector ([data-chart=${id}]) and doesn't guard against a missing id; update the
conditional and the template string so the style is only rendered when id is
present and the selector value is quoted. Specifically, change the v-if to check
both id and colorConfig.length (e.g., v-if="id && colorConfig.length") and
update the mapping that produces `${prefix} [data-chart=${id}] {` to include
quotes around the id value (e.g., `${prefix} [data-chart="${id}"] {`) so valid
runtime IDs and empty/undefined id are handled safely.

In `@src/shadcn/components/ui/chart/ChartTooltipContent.vue`:
- Around line 97-99: The span in ChartTooltipContent.vue uses v-if="value",
which hides valid zero values; update the condition on the span that renders {{
value.toLocaleString() }} (the element referencing the component prop/variable
value) to check for null/undefined instead (e.g., use value != null) so 0 will
render while still excluding absent values.

In `@src/shadcn/components/ui/chart/utils.ts`:
- Around line 10-12: serializeKey currently uses JSON.stringify with
Object.keys(key).sort(), which only sorts top-level properties and produces
unstable cache keys for nested objects/arrays; replace it by creating a
deterministic deep-sort routine (e.g., deepSortObject / stableSerialize) that
recursively sorts object keys and normalizes arrays before serialization, then
JSON.stringify the normalized structure in serializeKey so nested payloads
produce stable keys; update any other callers or duplicate logic at the other
spots noted (lines ~33-34) to use the same deep-sort helper.

---

Nitpick comments:
In `@src/main/presenter/newAgentPresenter/index.ts`:
- Around line 1417-1425: getUsageStatsBackfillStatus currently performs an
unexpected write via configPresenter.setSetting when
normalizeUsageStatsBackfillStatus returns a timed-out failed status; remove that
side effect from the getter by extracting the persistence into a dedicated
recovery/normalization function (e.g., create
recoverOrPersistUsageStatsBackfillStatus or extend
normalizeUsageStatsBackfillStatus to accept a callback) and have callers invoke
that explicit recovery step instead of relying on getUsageStatsBackfillStatus;
ensure references to normalizeUsageStatsBackfillStatus,
configPresenter.setSetting, and DASHBOARD_STATS_BACKFILL_KEY are updated to the
new method so the getter becomes side-effect free.

In `@src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts`:
- Around line 197-215: The current listAssistantUsageCandidates method returns
all assistant rows in one shot which will not scale; add a DB index on (role,
created_at) (e.g., via migration or table DDL for deepchat_messages) and change
listAssistantUsageCandidates to scan in chunks using a deterministic cursor (use
created_at and id) with ORDER BY created_at ASC and a LIMIT batchSize, iterating
until no more rows (or expose a paginated API that accepts last_seen_created_at
and last_seen_id); reference the method listAssistantUsageCandidates, table
deepchat_messages, and the (role, created_at) index when implementing these
changes.

In `@src/main/presenter/sqlitePresenter/tables/deepchatUsageStats.ts`:
- Around line 118-165: The upsert currently overwrites created_at on conflict in
the upsert method of deepchat_usage_stats; change the ON CONFLICT(...) DO UPDATE
clause in upsert (function upsert) so created_at is preserved by either removing
created_at from the SET list or explicitly setting created_at =
deepchat_usage_stats.created_at (the existing table value), while keeping
updated_at = excluded.updated_at and the other fields updated from excluded;
update the SQL string used in upsert to implement this change.

In `@src/main/presenter/usageStats.ts`:
- Around line 150-168: The clamp for cachedInputTokens in normalizeUsageCounts
is currently only applied when inputTokens > 0, which allows rawCached to exceed
inputTokens when inputTokens is 0; change the logic in normalizeUsageCounts so
cachedInputTokens is always clamped to be no greater than inputTokens (e.g., set
cachedInputTokens = Math.min(rawCached, inputTokens) after computing inputTokens
and rawCached) while keeping the existing non-negative conversions via
toNonNegativeInteger.

In `@src/renderer/src/i18n/he-IL/routes.json`:
- Line 19: The "settings-dashboard" route label is left in English; update the
translation value for the key "settings-dashboard" to a Hebrew equivalent (e.g.,
"לוח בקרה" or "לוח מחוונים") so it matches the other translated route labels;
edit the value for the JSON key "settings-dashboard" in the routes localization
file (the entry currently "settings-dashboard": "Dashboard") and replace
"Dashboard" with the chosen Hebrew string, ensuring valid JSON quoting and
encoding.

In `@src/renderer/src/i18n/ja-JP/routes.json`:
- Line 19: Replace the English string value for the "settings-dashboard" key in
the routes.json i18n file with the katakana transliteration "ダッシュボード" to match
the other loanword translations (e.g., "プレイグラウンド", "プロバイダー") and maintain
consistency across Japanese locale strings.

In `@src/renderer/src/i18n/ko-KR/routes.json`:
- Line 19: The value for the route key "settings-dashboard" is in English;
update the JSON entry for "settings-dashboard" in
src/renderer/src/i18n/ko-KR/routes.json to use the Korean translation "대시보드" so
it matches the other route labels (i.e., replace "Dashboard" with "대시보드" for the
"settings-dashboard" key).

In `@src/shadcn/components/ui/chart/utils.ts`:
- Line 7: The global Map named cache in utils.ts can grow without bound; replace
it with a bounded cache implementation (e.g., an LRU or simple capped Map) and
introduce a MAX_CACHE_SIZE constant, evicting the oldest entry when size >
MAX_CACHE_SIZE to prevent unbounded memory growth; update/reuse the new bounded
cache API wherever the original cache is referenced (and apply the same change
to the other Map instance noted at the second occurrence) so callers (same
get/set/delete semantics) continue to work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a35b7c47-fecf-496f-be8c-d01330f61641

📥 Commits

Reviewing files that changed from the base of the PR and between 5035c95 and ed336bf.

📒 Files selected for processing (57)
  • docs/specs/settings-dashboard/plan.md
  • docs/specs/settings-dashboard/spec.md
  • docs/specs/settings-dashboard/tasks.md
  • package.json
  • src/main/presenter/deepchatAgentPresenter/accumulator.ts
  • src/main/presenter/deepchatAgentPresenter/messageStore.ts
  • src/main/presenter/deepchatAgentPresenter/process.ts
  • src/main/presenter/lifecyclePresenter/hooks/after-start/usageStatsBackfillHook.ts
  • src/main/presenter/lifecyclePresenter/hooks/index.ts
  • src/main/presenter/llmProviderPresenter/providers/openAICompatibleProvider.ts
  • src/main/presenter/llmProviderPresenter/providers/openAIResponsesProvider.ts
  • src/main/presenter/newAgentPresenter/index.ts
  • src/main/presenter/sqlitePresenter/index.ts
  • src/main/presenter/sqlitePresenter/tables/deepchatMessages.ts
  • src/main/presenter/sqlitePresenter/tables/deepchatUsageStats.ts
  • src/main/presenter/usageStats.ts
  • src/renderer/settings/components/DashboardSettings.vue
  • src/renderer/settings/main.ts
  • src/renderer/src/i18n/da-DK/routes.json
  • src/renderer/src/i18n/da-DK/settings.json
  • src/renderer/src/i18n/en-US/routes.json
  • src/renderer/src/i18n/en-US/settings.json
  • src/renderer/src/i18n/fa-IR/routes.json
  • src/renderer/src/i18n/fa-IR/settings.json
  • src/renderer/src/i18n/fr-FR/routes.json
  • src/renderer/src/i18n/fr-FR/settings.json
  • src/renderer/src/i18n/he-IL/routes.json
  • src/renderer/src/i18n/he-IL/settings.json
  • src/renderer/src/i18n/ja-JP/routes.json
  • src/renderer/src/i18n/ja-JP/settings.json
  • src/renderer/src/i18n/ko-KR/routes.json
  • src/renderer/src/i18n/ko-KR/settings.json
  • src/renderer/src/i18n/pt-BR/routes.json
  • src/renderer/src/i18n/pt-BR/settings.json
  • src/renderer/src/i18n/ru-RU/routes.json
  • src/renderer/src/i18n/ru-RU/settings.json
  • src/renderer/src/i18n/zh-CN/routes.json
  • src/renderer/src/i18n/zh-CN/settings.json
  • src/renderer/src/i18n/zh-HK/routes.json
  • src/renderer/src/i18n/zh-HK/settings.json
  • src/renderer/src/i18n/zh-TW/routes.json
  • src/renderer/src/i18n/zh-TW/settings.json
  • src/shadcn/components/ui/chart/ChartContainer.vue
  • src/shadcn/components/ui/chart/ChartLegendContent.vue
  • src/shadcn/components/ui/chart/ChartStyle.vue
  • src/shadcn/components/ui/chart/ChartTooltipContent.vue
  • src/shadcn/components/ui/chart/index.ts
  • src/shadcn/components/ui/chart/utils.ts
  • src/shared/types/agent-interface.d.ts
  • src/shared/types/core/llm-events.ts
  • src/shared/types/presenters/new-agent.presenter.d.ts
  • test/main/presenter/deepchatAgentPresenter/accumulator.test.ts
  • test/main/presenter/deepchatAgentPresenter/messageStore.test.ts
  • test/main/presenter/llmProviderPresenter/coreEvents.test.ts
  • test/main/presenter/llmProviderPresenter/openAIResponsesProvider.test.ts
  • test/main/presenter/newAgentPresenter/usageDashboard.test.ts
  • test/renderer/components/DashboardSettings.test.ts

@zerob13 zerob13 merged commit ac62fe6 into dev Mar 17, 2026
1 of 2 checks passed
@zerob13 zerob13 deleted the codex/user-summary branch March 29, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant