diff --git a/desktop/backend/services.go b/desktop/backend/services.go index b8fd89f..9cf5d31 100644 --- a/desktop/backend/services.go +++ b/desktop/backend/services.go @@ -61,6 +61,8 @@ type AntigravityService struct { } type ClaudeCodeService struct { + autoSync *usageAutoSyncRuntime + settingsMu sync.Mutex application *app.Application changes *ChangeNotifier } @@ -106,24 +108,25 @@ type AgentService struct { } type Services struct { - App *AppService - Agent *AgentService - Antigravity *AntigravityService - ClaudeCode *ClaudeCodeService - Codex *CodexService - GrokBuild *GrokBuildService - Profile *ProfileService - Switch *SwitchService - Doctor *DoctorService - Backup *BackupService - Usage *UsageService - Settings *SettingsService - changes *ChangeNotifier - codexUsageSync *usageAutoSyncRuntime - grokBuildUsageSync *usageAutoSyncRuntime - quota *codexQuotaRuntime - runtimes *agentRuntimeManager - backups *applicationBackupRuntime + App *AppService + Agent *AgentService + Antigravity *AntigravityService + ClaudeCode *ClaudeCodeService + Codex *CodexService + GrokBuild *GrokBuildService + Profile *ProfileService + Switch *SwitchService + Doctor *DoctorService + Backup *BackupService + Usage *UsageService + Settings *SettingsService + changes *ChangeNotifier + codexUsageSync *usageAutoSyncRuntime + grokBuildUsageSync *usageAutoSyncRuntime + claudeCodeUsageSync *usageAutoSyncRuntime + quota *codexQuotaRuntime + runtimes *agentRuntimeManager + backups *applicationBackupRuntime } type DashboardResult struct { @@ -310,6 +313,18 @@ func NewServices(application *app.Application, info app.Info, env Environment, s return application.Usage().SyncProviderBackground(ctx, grokconfig.ProviderID, onWorkDetected) }, ) + claudeCodeUsageSync := newUsageAutoSyncRuntime( + claudecodeconfig.ProviderID, + func(ctx context.Context) (usage.ProviderSyncSettings, error) { + value, err := application.ClaudeCode().GetSettings(ctx) + return usage.ProviderSyncSettings{ + UsageSyncIntervalSeconds: value.UsageSyncIntervalSeconds, + }, err + }, + func(ctx context.Context, onWorkDetected func()) (usage.BackgroundSyncOutcome, error) { + return application.Usage().SyncProviderBackground(ctx, claudecodeconfig.ProviderID, onWorkDetected) + }, + ) quota := newCodexQuotaRuntime(application.Codex().ListAutomationTargets, application.Codex().RunCredentialJob) backups := newApplicationBackupRuntime( application.Settings().Get, @@ -323,7 +338,7 @@ func NewServices(application *app.Application, info app.Info, env Environment, s App: &AppService{application: application, info: info, env: env, startupErr: startupErr, changes: changes}, Agent: &AgentService{application: application, changes: changes}, Antigravity: &AntigravityService{application: application, changes: changes}, - ClaudeCode: &ClaudeCodeService{application: application, changes: changes}, + ClaudeCode: &ClaudeCodeService{application: application, changes: changes, autoSync: claudeCodeUsageSync}, Codex: &CodexService{application: application, changes: changes, autoSync: codexUsageSync, quota: quota}, GrokBuild: &GrokBuildService{application: application, changes: changes, autoSync: grokBuildUsageSync}, Profile: &ProfileService{application: application, changes: changes, quota: quota}, @@ -333,20 +348,23 @@ func NewServices(application *app.Application, info app.Info, env Environment, s Usage: &UsageService{ application: application, autoSync: map[string]*usageAutoSyncRuntime{ - codexconfig.ProviderID: codexUsageSync, - grokconfig.ProviderID: grokBuildUsageSync, + codexconfig.ProviderID: codexUsageSync, + grokconfig.ProviderID: grokBuildUsageSync, + claudecodeconfig.ProviderID: claudeCodeUsageSync, }, }, - Settings: &SettingsService{application: application}, - changes: changes, - codexUsageSync: codexUsageSync, - grokBuildUsageSync: grokBuildUsageSync, - quota: quota, - runtimes: runtimes, - backups: backups, + Settings: &SettingsService{application: application}, + changes: changes, + codexUsageSync: codexUsageSync, + grokBuildUsageSync: grokBuildUsageSync, + claudeCodeUsageSync: claudeCodeUsageSync, + quota: quota, + runtimes: runtimes, + backups: backups, } runtimes.Register(agent.Codex, codexUsageSync) runtimes.Register(agent.GrokBuild, grokBuildUsageSync) + runtimes.Register(agent.ClaudeCode, claudeCodeUsageSync) runtimes.Register(agent.Codex, quota) return services } @@ -358,13 +376,16 @@ func (s Services) SubscribeChanges(listener func(DesktopChangeEvent)) func() { func (s Services) StartUsageAutoSync(ctx context.Context, emitter func(UsageAutoSyncStatus)) { s.codexUsageSync.SetEmitter(emitter) s.grokBuildUsageSync.SetEmitter(emitter) + s.claudeCodeUsageSync.SetEmitter(emitter) s.runtimes.Activate(ctx, agent.Codex, s.codexUsageSync) s.runtimes.Activate(ctx, agent.GrokBuild, s.grokBuildUsageSync) + s.runtimes.Activate(ctx, agent.ClaudeCode, s.claudeCodeUsageSync) } func (s Services) StopUsageAutoSync() { s.runtimes.Deactivate(agent.Codex, s.codexUsageSync) s.runtimes.Deactivate(agent.GrokBuild, s.grokBuildUsageSync) + s.runtimes.Deactivate(agent.ClaudeCode, s.claudeCodeUsageSync) } func (s Services) StartCodexQuotaRuntime(ctx context.Context, emitter func(CodexQuotaRuntimeStatus)) { @@ -1349,3 +1370,25 @@ func FormatDesktopErrorPtr(err error) *DesktopError { payload := FormatDesktopError(err) return &payload } + +func (s *ClaudeCodeService) GetSettings(ctx context.Context) (claudecode.Settings, error) { + return s.application.ClaudeCode().GetSettings(ctx) +} + +func (s *ClaudeCodeService) UpdateSettings( + ctx context.Context, + req claudecode.UpdateSettingsRequest, +) (claudecode.Settings, error) { + // Persist and update only this Provider's runtime under one serial order so + // a slower older request cannot restore a stale interval. + s.settingsMu.Lock() + defer s.settingsMu.Unlock() + value, err := s.application.ClaudeCode().UpdateSettings(ctx, req) + if err != nil { + return claudecode.Settings{}, err + } + if req.UsageSyncIntervalSeconds != nil { + s.autoSync.SetInterval(value.UsageSyncIntervalSeconds) + } + return value, nil +} diff --git a/desktop/frontend/bindings/github.com/strahe/profiledeck/desktop/backend/claudecodeservice.ts b/desktop/frontend/bindings/github.com/strahe/profiledeck/desktop/backend/claudecodeservice.ts index 681a3ed..09df711 100644 --- a/desktop/frontend/bindings/github.com/strahe/profiledeck/desktop/backend/claudecodeservice.ts +++ b/desktop/frontend/bindings/github.com/strahe/profiledeck/desktop/backend/claudecodeservice.ts @@ -25,6 +25,10 @@ export function Detect(): $CancellablePromise { + return $Call.ByID(77725791); +} + export function ListProfiles(): $CancellablePromise { return $Call.ByID(2487267564); } @@ -40,3 +44,7 @@ export function ShowProfile(profileID: string): $CancellablePromise { return $Call.ByID(3263369448, req); } + +export function UpdateSettings(req: claudecode$0.UpdateSettingsRequest): $CancellablePromise { + return $Call.ByID(325034858, req); +} diff --git a/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/index.ts b/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/index.ts index 8118a36..b1ce362 100644 --- a/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/index.ts +++ b/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/index.ts @@ -6,5 +6,7 @@ export type { ClaudeCodeProfileDetail, ClaudeCodeProfileListResult, ClaudeCodeProfileSaveResult, - ClaudeCodeProfileSummary + ClaudeCodeProfileSummary, + Settings, + UpdateSettingsRequest } from "./models.js"; diff --git a/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/models.ts b/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/models.ts index 80eec66..be50d4f 100644 --- a/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/models.ts +++ b/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/claudecode/models.ts @@ -43,3 +43,11 @@ export interface ClaudeCodeProfileSummary { "updated_at_unix_ms": number; "warnings"?: string[] | null; } + +export interface Settings { + "usage_sync_interval_seconds": number; +} + +export interface UpdateSettingsRequest { + "usage_sync_interval_seconds"?: number | null; +} diff --git a/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/usage/models.ts b/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/usage/models.ts index a21f718..80714eb 100644 --- a/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/usage/models.ts +++ b/desktop/frontend/bindings/github.com/strahe/profiledeck/internal/usage/models.ts @@ -2,6 +2,13 @@ // This file is automatically generated. DO NOT EDIT export interface UsageAggregateSummary { + "missing_output_cost_event_count": number; + "missing_cache_ttl_event_count": number; + "missing_cache_rate_event_count": number; + "token_total_status": string; + "output_tokens_status": string; + "incomplete_event_count": number; + "conflicting_event_count": number; "event_count": number; "session_count": number; "fresh_input_tokens": number; @@ -28,6 +35,7 @@ export interface UsageAggregateSummary { } export interface UsageImportSummary { + "invalid_reasons": { [_ in string]?: number } | null; "tracked_files": number; "last_synced_at_unix_ms": number; "invalid_lines": number; @@ -85,6 +93,10 @@ export interface UsageResolvedRange { } export interface UsageSummaryResult { + "token_total_status": string; + "output_tokens_status": string; + "incomplete_event_count": number; + "conflicting_event_count": number; "provider_id": string; "source": string; "sources": string[] | null; @@ -94,6 +106,7 @@ export interface UsageSummaryResult { "output_tokens": number; "total_tokens": number; "estimated_cost_usd": string | null; + "known_estimated_cost_usd": string; "cost_status": string; "unknown_cost_event_count": number; "estimated_cost_event_count": number; diff --git a/desktop/frontend/src/features/settings/ClaudeCodeSettings.svelte b/desktop/frontend/src/features/settings/ClaudeCodeSettings.svelte new file mode 100644 index 0000000..db245d6 --- /dev/null +++ b/desktop/frontend/src/features/settings/ClaudeCodeSettings.svelte @@ -0,0 +1,124 @@ + + + + {#if !providerExists} + + + {$_("usage.providerUnavailableTitle")} + + {$_("usage.providerUnavailableDescription", { values: { provider: "Claude Code" } })} + + + + + + + {:else} + + {/if} + diff --git a/desktop/frontend/src/features/usage/UsageModelTable.svelte b/desktop/frontend/src/features/usage/UsageModelTable.svelte index 8f6a902..ab8e86b 100644 --- a/desktop/frontend/src/features/usage/UsageModelTable.svelte +++ b/desktop/frontend/src/features/usage/UsageModelTable.svelte @@ -1,4 +1,5 @@ @@ -300,20 +306,32 @@ {#if (loading && (!report || report.summary.event_count === 0)) || (report?.summary.event_count === 0 && initialSyncPending)}
{:else if report} - {#if autoSyncStatus?.outcome === "warning" || report.import.invalid_lines > 0 || report.import.unsupported_lines > 0 || report.summary.undated_event_count > 0 || report.summary.partial_cost_event_count > 0 || (report.summary.event_count > 0 && report.summary.unknown_cost_event_count > 0) || (providerID === "grok-build" && (report.summary.partial_reported_cost_event_count > 0 || (report.summary.event_count > 0 && report.summary.unknown_reported_cost_event_count > 0)))} + {#if autoSyncStatus?.outcome === "warning" || report.import.invalid_lines > 0 || report.import.unsupported_lines > 0 || report.summary.undated_event_count > 0 || report.summary.incomplete_event_count > 0 || report.summary.conflicting_event_count > 0 || report.summary.partial_cost_event_count > 0 || (report.summary.event_count > 0 && report.summary.unknown_cost_event_count > 0) || (providerID === "grok-build" && (report.summary.partial_reported_cost_event_count > 0 || (report.summary.event_count > 0 && report.summary.unknown_reported_cost_event_count > 0)))} {$_("usage.dataQuality.title")} -

{$_("usage.dataQuality.summary", { values: { count: dataQualityIssueCount(report) } })}

+ {#if report.summary.incomplete_event_count > 0} +

{$_(providerID === "claude-code" ? "usage.claudeIncompleteUsage" : "usage.incompleteUsage", { values: { count: formatInteger(report.summary.incomplete_event_count) } })}

+ {/if} + {#if report.summary.conflicting_event_count > 0} +

{$_("usage.conflictingUsage", { values: { count: formatInteger(report.summary.conflicting_event_count) } })}

+ {/if} + {#if hasDataQualityDetails(report)} {$_("usage.dataQuality.showDetails")}
    {#if autoSyncStatus?.outcome === "warning"}
  • {$_("usage.dataQuality.fileErrors", { values: { count: autoSyncStatus.import_error_count } })}
  • {/if} - {#if report.import.invalid_lines > 0 || report.import.unsupported_lines > 0}
  • {$_("usage.dataQuality.lines", { values: { invalid: formatInteger(report.import.invalid_lines), unsupported: formatInteger(report.import.unsupported_lines) } })}
  • {/if} + {#each Object.entries(report.import.invalid_reasons ?? {}) as [reason, count]} + {#if typeof count === "number" && count > 0}
  • {$_(`usage.dataQuality.invalidReasons.${reason}`, { values: { count: formatInteger(count) } })}
  • {/if} + {/each} + {#if unclassifiedInvalidLines(report) > 0}
  • {$_("usage.dataQuality.invalidLines", { values: { count: formatInteger(unclassifiedInvalidLines(report)) } })}
  • {/if} + {#if report.import.unsupported_lines > 0}
  • {$_("usage.dataQuality.unsupportedLines", { values: { count: formatInteger(report.import.unsupported_lines) } })}
  • {/if} {#if report.summary.undated_event_count > 0}
  • {$_("usage.dataQuality.undated", { values: { count: formatInteger(report.summary.undated_event_count) } })}
  • {/if} - {#if report.summary.partial_cost_event_count > 0}
  • {$_("usage.dataQuality.partialPricing", { values: { count: formatInteger(report.summary.partial_cost_event_count) } })}
  • {/if} + {#if report.summary.missing_cache_ttl_event_count > 0}
  • {$_("usage.dataQuality.missingCacheTTL", { values: { count: formatInteger(report.summary.missing_cache_ttl_event_count) } })}
  • {/if} + {#if report.summary.missing_cache_rate_event_count > 0}
  • {$_("usage.dataQuality.missingCacheRate", { values: { count: formatInteger(report.summary.missing_cache_rate_event_count) } })}
  • {/if} + {#if providerID !== "claude-code" && report.summary.partial_cost_event_count > 0}
  • {$_("usage.dataQuality.partialPricing", { values: { count: formatInteger(report.summary.partial_cost_event_count) } })}
  • {/if} {#if report.summary.event_count > 0 && report.summary.unknown_cost_event_count > 0}
  • {$_("usage.dataQuality.pricing", { values: { count: formatInteger(report.summary.unknown_cost_event_count), coverage: formatPercent(report.summary.pricing_coverage) } })}
  • {/if} {#if providerID === "grok-build" && report.summary.partial_reported_cost_event_count > 0}
  • {$_("usage.dataQuality.partialReportedCost", { values: { count: formatInteger(report.summary.partial_reported_cost_event_count) } })}
  • {/if} {#if providerID === "grok-build" && report.summary.event_count > 0 && report.summary.unknown_reported_cost_event_count > 0}
  • {$_("usage.dataQuality.reportedCost", { values: { count: formatInteger(report.summary.unknown_reported_cost_event_count) } })}
  • {/if} @@ -321,6 +339,7 @@ + {/if} {/if} @@ -388,7 +407,7 @@ {$_("usage.totalTokens")} - {formatCompact(report.summary.total_tokens)} + {usageTokenLabel(formatCompact(report.summary.total_tokens), report.summary.token_total_status)} diff --git a/desktop/frontend/src/features/usage/UsageTrendChart.svelte b/desktop/frontend/src/features/usage/UsageTrendChart.svelte index 3b416e8..b1b957a 100644 --- a/desktop/frontend/src/features/usage/UsageTrendChart.svelte +++ b/desktop/frontend/src/features/usage/UsageTrendChart.svelte @@ -1,4 +1,5 @@