@tokentop/ttop 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +32 -32
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +66 -57
  34. package/src/plugins/registry.ts +47 -43
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,20 +1,28 @@
1
- import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
2
- import type { ProviderPlugin, ProviderUsageData, Credentials } from '@/plugins/types/provider.ts';
3
- import type { ThemePlugin } from '@/plugins/types/theme.ts';
4
- import type { NotificationPlugin } from '@/plugins/types/notification.ts';
5
- import { pluginRegistry } from '@/plugins/registry.ts';
6
- import { createSandboxedHttpClient, createPluginLogger } from '@/plugins/sandbox.ts';
7
- import { createPluginContext } from '@/plugins/plugin-context-factory.ts';
8
- import { safeInvoke, safeInvokeSync } from '@/plugins/plugin-host.ts';
9
- import { notificationBus } from '@/plugins/notification-bus.ts';
10
- import { pluginLifecycle } from '@/plugins/lifecycle.ts';
11
- import { installGlobalFetchGuard, runInPluginGuard } from '@/plugins/sandbox-guard.ts';
12
- import { initPricingFromPlugins } from '@/pricing/index.ts';
13
- import { useLogs } from './LogContext.tsx';
14
- import { useStorage } from './StorageContext.tsx';
15
- import type { ProviderSnapshotInsert } from '@/storage/types.ts';
16
- import { useDemoMode } from './DemoModeContext.tsx';
17
- import { useConfig } from './ConfigContext.tsx';
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { pluginLifecycle } from "@/plugins/lifecycle.ts";
11
+ import { notificationBus } from "@/plugins/notification-bus.ts";
12
+ import { createPluginContext } from "@/plugins/plugin-context-factory.ts";
13
+ import { safeInvoke, safeInvokeSync } from "@/plugins/plugin-host.ts";
14
+ import { pluginRegistry } from "@/plugins/registry.ts";
15
+ import { createPluginLogger, createSandboxedHttpClient } from "@/plugins/sandbox.ts";
16
+ import { installGlobalFetchGuard, runInPluginGuard } from "@/plugins/sandbox-guard.ts";
17
+ import type { NotificationPlugin } from "@/plugins/types/notification.ts";
18
+ import type { Credentials, ProviderPlugin, ProviderUsageData } from "@/plugins/types/provider.ts";
19
+ import type { ThemePlugin } from "@/plugins/types/theme.ts";
20
+ import { initPricingFromPlugins } from "@/pricing/index.ts";
21
+ import type { ProviderSnapshotInsert } from "@/storage/types.ts";
22
+ import { useConfig } from "./ConfigContext.tsx";
23
+ import { useDemoMode } from "./DemoModeContext.tsx";
24
+ import { useLogs } from "./LogContext.tsx";
25
+ import { useStorage } from "./StorageContext.tsx";
18
26
 
19
27
  export interface UsageSnapshot {
20
28
  timestamp: number;
@@ -46,16 +54,16 @@ const PluginContext = createContext<PluginContextValue | null>(null);
46
54
 
47
55
  function getMaxUsagePercent(usage: ProviderUsageData): number | null {
48
56
  if (!usage.limits) return null;
49
-
57
+
50
58
  const items = usage.limits.items ?? [];
51
59
  if (items.length > 0) {
52
60
  const percents = items.map((item) => item.usedPercent).filter((p): p is number => p !== null);
53
61
  return percents.length > 0 ? Math.max(...percents) : null;
54
62
  }
55
-
63
+
56
64
  const primary = usage.limits.primary?.usedPercent;
57
65
  const secondary = usage.limits.secondary?.usedPercent;
58
-
66
+
59
67
  if (primary !== null && primary !== undefined && secondary !== null && secondary !== undefined) {
60
68
  return Math.max(primary, secondary);
61
69
  }
@@ -88,7 +96,7 @@ export function PluginProvider({ children, cliPlugins }: PluginProviderProps) {
88
96
  useEffect(() => {
89
97
  async function initialize() {
90
98
  installGlobalFetchGuard();
91
- debug('Initializing plugin registry...', undefined, 'plugins');
99
+ debug("Initializing plugin registry...", undefined, "plugins");
92
100
 
93
101
  try {
94
102
  if (!demoMode) {
@@ -102,44 +110,54 @@ export function PluginProvider({ children, cliPlugins }: PluginProviderProps) {
102
110
  } else {
103
111
  await pluginRegistry.loadBuiltinPlugins();
104
112
  }
105
- info('Plugin registry initialized', undefined, 'plugins');
113
+ info("Plugin registry initialized", undefined, "plugins");
106
114
  } catch (err) {
107
- logError('Failed to initialize plugin registry', { error: String(err) }, 'plugins');
115
+ logError("Failed to initialize plugin registry", { error: String(err) }, "plugins");
108
116
  }
109
117
 
110
- const providerPlugins = pluginRegistry.getAll('provider');
111
- const themePlugins = pluginRegistry.getAll('theme');
112
- const notificationPlugins = pluginRegistry.getAll('notification');
118
+ const providerPlugins = pluginRegistry.getAll("provider");
119
+ const themePlugins = pluginRegistry.getAll("theme");
120
+ const notificationPlugins = pluginRegistry.getAll("notification");
113
121
 
114
122
  initPricingFromPlugins(providerPlugins, {
115
- 'google-gemini': 'google',
123
+ "google-gemini": "google",
116
124
  });
117
125
 
118
- info(`Loaded plugins`, {
119
- providers: providerPlugins.length,
120
- themes: themePlugins.length,
121
- notifications: notificationPlugins.length,
122
- }, 'plugins');
126
+ info(
127
+ `Loaded plugins`,
128
+ {
129
+ providers: providerPlugins.length,
130
+ themes: themePlugins.length,
131
+ notifications: notificationPlugins.length,
132
+ },
133
+ "plugins",
134
+ );
123
135
 
124
136
  await pluginLifecycle.initializeAll();
125
137
  await pluginLifecycle.startAll();
126
- debug('Plugin lifecycle: all plugins initialized and started', undefined, 'plugins');
138
+ debug("Plugin lifecycle: all plugins initialized and started", undefined, "plugins");
127
139
 
128
- debug('Discovering credentials...', undefined, 'credentials');
140
+ debug("Discovering credentials...", undefined, "credentials");
129
141
 
130
142
  const credentials = new Map<string, Credentials>();
131
143
  if (!demoMode) {
132
- await Promise.all(providerPlugins.map(async (p) => {
133
- const ctx = createPluginContext(p.id, p.permissions);
134
- const result = await safeInvoke(p.id, 'auth.discover', () =>
135
- runInPluginGuard(p.id, p.permissions, () => p.auth.discover(ctx)),
136
- );
137
- if (result.ok && result.value.ok && result.value.credentials) {
138
- credentials.set(p.id, result.value.credentials);
139
- } else if (!result.ok) {
140
- logError(`Credential discovery failed for ${p.id}`, { error: result.error.message }, 'credentials');
141
- }
142
- }));
144
+ await Promise.all(
145
+ providerPlugins.map(async (p) => {
146
+ const ctx = createPluginContext(p.id, p.permissions);
147
+ const result = await safeInvoke(p.id, "auth.discover", () =>
148
+ runInPluginGuard(p.id, p.permissions, () => p.auth.discover(ctx)),
149
+ );
150
+ if (result.ok && result.value.ok && result.value.credentials) {
151
+ credentials.set(p.id, result.value.credentials);
152
+ } else if (!result.ok) {
153
+ logError(
154
+ `Credential discovery failed for ${p.id}`,
155
+ { error: result.error.message },
156
+ "credentials",
157
+ );
158
+ }
159
+ }),
160
+ );
143
161
  }
144
162
 
145
163
  const providerStates = new Map<string, ProviderState>();
@@ -152,7 +170,9 @@ export function PluginProvider({ children, cliPlugins }: PluginProviderProps) {
152
170
  if (demoMode) {
153
171
  configured = true;
154
172
  } else if (creds) {
155
- const check = safeInvokeSync(plugin.id, 'auth.isConfigured', () => plugin.auth.isConfigured(creds));
173
+ const check = safeInvokeSync(plugin.id, "auth.isConfigured", () =>
174
+ plugin.auth.isConfigured(creds),
175
+ );
156
176
  configured = check.ok ? check.value : false;
157
177
  }
158
178
  providerStates.set(plugin.id, {
@@ -171,10 +191,14 @@ export function PluginProvider({ children, cliPlugins }: PluginProviderProps) {
171
191
  }
172
192
  }
173
193
 
174
- info('Credential discovery complete', {
175
- configured: configuredIds,
176
- unconfigured: unconfiguredIds,
177
- }, 'credentials');
194
+ info(
195
+ "Credential discovery complete",
196
+ {
197
+ configured: configuredIds,
198
+ unconfigured: unconfiguredIds,
199
+ },
200
+ "credentials",
201
+ );
178
202
 
179
203
  if (demoMode && simulator) {
180
204
  const snapshot = simulator.tick();
@@ -216,175 +240,204 @@ export function PluginProvider({ children, cliPlugins }: PluginProviderProps) {
216
240
  const prevStr = JSON.stringify(prev[pluginId] ?? {});
217
241
  const nextStr = JSON.stringify(next[pluginId] ?? {});
218
242
  if (prevStr !== nextStr) {
219
- debug(`Plugin config changed for ${pluginId}`, undefined, 'plugins');
243
+ debug(`Plugin config changed for ${pluginId}`, undefined, "plugins");
220
244
  pluginLifecycle.notifyConfigChange(pluginId, next[pluginId] ?? {});
221
245
  }
222
246
  }
223
247
  prevPluginConfigRef.current = { ...next };
224
248
  }, [config.pluginConfig, isInitialized]);
225
249
 
226
- const refreshProvider = useCallback(async (providerId: string) => {
227
- const state = providers.get(providerId);
228
- if (!state || !state.configured) {
229
- debug(`Skipping refresh for ${providerId}: not configured`, undefined, 'refresh');
230
- return;
231
- }
250
+ const refreshProvider = useCallback(
251
+ async (providerId: string) => {
252
+ const state = providers.get(providerId);
253
+ if (!state || !state.configured) {
254
+ debug(`Skipping refresh for ${providerId}: not configured`, undefined, "refresh");
255
+ return;
256
+ }
232
257
 
233
- info(`Refreshing ${providerId}...`, undefined, 'refresh');
258
+ info(`Refreshing ${providerId}...`, undefined, "refresh");
234
259
 
235
- setProviders((prev) => {
236
- const next = new Map(prev);
237
- const current = next.get(providerId);
238
- if (current) {
239
- next.set(providerId, { ...current, loading: true });
240
- }
241
- return next;
242
- });
260
+ setProviders((prev) => {
261
+ const next = new Map(prev);
262
+ const current = next.get(providerId);
263
+ if (current) {
264
+ next.set(providerId, { ...current, loading: true });
265
+ }
266
+ return next;
267
+ });
243
268
 
244
- try {
245
- if (demoMode && simulator) {
246
- const snapshot = simulator.tick();
247
- const usage = snapshot.providerUsage.get(providerId) ?? {
248
- fetchedAt: Date.now(),
249
- error: 'Demo provider data missing',
250
- };
269
+ try {
270
+ if (demoMode && simulator) {
271
+ const snapshot = simulator.tick();
272
+ const usage = snapshot.providerUsage.get(providerId) ?? {
273
+ fetchedAt: Date.now(),
274
+ error: "Demo provider data missing",
275
+ };
251
276
 
252
- if (storageReady) {
253
- recordProviderSnapshots([
254
- {
277
+ if (storageReady) {
278
+ recordProviderSnapshots([
279
+ {
280
+ timestamp: Date.now(),
281
+ provider: providerId,
282
+ usedPercent: getMaxUsagePercent(usage),
283
+ limitReached: usage.limitReached ?? false,
284
+ resetsAt: usage.limits?.primary?.resetsAt ?? null,
285
+ rawJson: JSON.stringify(usage),
286
+ },
287
+ ]);
288
+ }
289
+
290
+ setProviders((prev) => {
291
+ const next = new Map(prev);
292
+ const current = next.get(providerId);
293
+ const currentHistory = current?.history ?? [];
294
+ const snapshotEntry: UsageSnapshot = {
255
295
  timestamp: Date.now(),
296
+ usedPercent: getMaxUsagePercent(usage),
297
+ limitReached: usage.limitReached,
298
+ };
299
+ next.set(providerId, {
300
+ ...state,
301
+ usage,
302
+ loading: false,
303
+ lastFetchAt: Date.now(),
304
+ history: addToHistory(currentHistory, snapshotEntry),
305
+ });
306
+ return next;
307
+ });
308
+
309
+ return;
310
+ }
311
+
312
+ const ctx = createPluginContext(providerId, state.plugin.permissions);
313
+ const discoverResult = await safeInvoke(providerId, "auth.discover", () =>
314
+ runInPluginGuard(providerId, state.plugin.permissions, () =>
315
+ state.plugin.auth.discover(ctx),
316
+ ),
317
+ );
318
+
319
+ if (!discoverResult.ok) {
320
+ throw discoverResult.error;
321
+ }
322
+ const creds = discoverResult.value.ok ? discoverResult.value.credentials : undefined;
323
+
324
+ if (!creds) {
325
+ throw new Error("Credentials not found");
326
+ }
327
+
328
+ const http = createSandboxedHttpClient(providerId, state.plugin.permissions);
329
+ const logger = createPluginLogger(providerId);
330
+
331
+ const fetchResult = await safeInvoke(providerId, "fetchUsage", () =>
332
+ runInPluginGuard(providerId, state.plugin.permissions, () =>
333
+ state.plugin.fetchUsage({
334
+ credentials: creds,
335
+ http,
336
+ logger,
337
+ config: {},
338
+ signal: AbortSignal.timeout(30_000),
339
+ }),
340
+ ),
341
+ );
342
+
343
+ if (!fetchResult.ok) {
344
+ throw fetchResult.error;
345
+ }
346
+
347
+ const usage = fetchResult.value;
348
+
349
+ if (usage.error) {
350
+ warn(`${providerId} returned error: ${usage.error}`, undefined, "refresh");
351
+ } else {
352
+ info(
353
+ `${providerId} refreshed successfully`,
354
+ {
355
+ limitReached: usage.limitReached,
356
+ primaryUsage: usage.limits?.primary?.usedPercent,
357
+ },
358
+ "refresh",
359
+ );
360
+
361
+ if (storageReady) {
362
+ const now = Date.now();
363
+ const snapshotInsert: ProviderSnapshotInsert = {
364
+ timestamp: now,
256
365
  provider: providerId,
257
366
  usedPercent: getMaxUsagePercent(usage),
258
367
  limitReached: usage.limitReached ?? false,
259
368
  resetsAt: usage.limits?.primary?.resetsAt ?? null,
260
369
  rawJson: JSON.stringify(usage),
261
- },
262
- ]);
370
+ };
371
+ recordProviderSnapshots([snapshotInsert]);
372
+ }
373
+
374
+ notificationBus.checkProviderUsage(providerId, state.plugin.name, usage);
263
375
  }
264
376
 
265
377
  setProviders((prev) => {
266
378
  const next = new Map(prev);
267
379
  const current = next.get(providerId);
268
380
  const currentHistory = current?.history ?? [];
269
- const snapshotEntry: UsageSnapshot = {
381
+
382
+ const snapshot: UsageSnapshot = {
270
383
  timestamp: Date.now(),
271
384
  usedPercent: getMaxUsagePercent(usage),
272
385
  limitReached: usage.limitReached,
273
386
  };
387
+
274
388
  next.set(providerId, {
275
389
  ...state,
276
390
  usage,
277
391
  loading: false,
278
392
  lastFetchAt: Date.now(),
279
- history: addToHistory(currentHistory, snapshotEntry),
393
+ history: addToHistory(currentHistory, snapshot),
280
394
  });
281
395
  return next;
282
396
  });
397
+ } catch (err) {
398
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
399
+ logError(`Failed to refresh ${providerId}: ${errorMsg}`, undefined, "refresh");
283
400
 
284
- return;
285
- }
286
-
287
- const ctx = createPluginContext(providerId, state.plugin.permissions);
288
- const discoverResult = await safeInvoke(providerId, 'auth.discover', () =>
289
- runInPluginGuard(providerId, state.plugin.permissions, () => state.plugin.auth.discover(ctx)),
290
- );
291
-
292
- if (!discoverResult.ok) {
293
- throw discoverResult.error;
294
- }
295
- const creds = discoverResult.value.ok ? discoverResult.value.credentials : undefined;
296
-
297
- if (!creds) {
298
- throw new Error('Credentials not found');
299
- }
300
-
301
- const http = createSandboxedHttpClient(providerId, state.plugin.permissions);
302
- const logger = createPluginLogger(providerId);
303
-
304
- const fetchResult = await safeInvoke(providerId, 'fetchUsage', () =>
305
- runInPluginGuard(providerId, state.plugin.permissions, () =>
306
- state.plugin.fetchUsage({ credentials: creds, http, logger, config: {}, signal: AbortSignal.timeout(30_000) }),
307
- ),
308
- );
309
-
310
- if (!fetchResult.ok) {
311
- throw fetchResult.error;
312
- }
313
-
314
- const usage = fetchResult.value;
315
-
316
- if (usage.error) {
317
- warn(`${providerId} returned error: ${usage.error}`, undefined, 'refresh');
318
- } else {
319
- info(`${providerId} refreshed successfully`, {
320
- limitReached: usage.limitReached,
321
- primaryUsage: usage.limits?.primary?.usedPercent,
322
- }, 'refresh');
323
-
324
- if (storageReady) {
325
- const now = Date.now();
326
- const snapshotInsert: ProviderSnapshotInsert = {
327
- timestamp: now,
328
- provider: providerId,
329
- usedPercent: getMaxUsagePercent(usage),
330
- limitReached: usage.limitReached ?? false,
331
- resetsAt: usage.limits?.primary?.resetsAt ?? null,
332
- rawJson: JSON.stringify(usage),
333
- };
334
- recordProviderSnapshots([snapshotInsert]);
335
- }
336
-
337
- notificationBus.checkProviderUsage(providerId, state.plugin.name, usage);
338
- }
339
-
340
- setProviders((prev) => {
341
- const next = new Map(prev);
342
- const current = next.get(providerId);
343
- const currentHistory = current?.history ?? [];
344
-
345
- const snapshot: UsageSnapshot = {
346
- timestamp: Date.now(),
347
- usedPercent: getMaxUsagePercent(usage),
348
- limitReached: usage.limitReached,
349
- };
350
-
351
- next.set(providerId, {
352
- ...state,
353
- usage,
354
- loading: false,
355
- lastFetchAt: Date.now(),
356
- history: addToHistory(currentHistory, snapshot),
357
- });
358
- return next;
359
- });
360
- } catch (err) {
361
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
362
- logError(`Failed to refresh ${providerId}: ${errorMsg}`, undefined, 'refresh');
363
-
364
- setProviders((prev) => {
365
- const next = new Map(prev);
366
- const current = next.get(providerId);
367
- next.set(providerId, {
368
- ...state,
369
- usage: {
370
- fetchedAt: Date.now(),
371
- error: errorMsg,
372
- },
373
- loading: false,
374
- lastFetchAt: Date.now(),
375
- history: current?.history ?? [],
401
+ setProviders((prev) => {
402
+ const next = new Map(prev);
403
+ const current = next.get(providerId);
404
+ next.set(providerId, {
405
+ ...state,
406
+ usage: {
407
+ fetchedAt: Date.now(),
408
+ error: errorMsg,
409
+ },
410
+ loading: false,
411
+ lastFetchAt: Date.now(),
412
+ history: current?.history ?? [],
413
+ });
414
+ return next;
376
415
  });
377
- return next;
378
- });
379
- }
380
- }, [providers, debug, info, warn, logError, storageReady, recordProviderSnapshots, demoMode, simulator]);
416
+ }
417
+ },
418
+ [
419
+ providers,
420
+ debug,
421
+ info,
422
+ warn,
423
+ logError,
424
+ storageReady,
425
+ recordProviderSnapshots,
426
+ demoMode,
427
+ simulator,
428
+ ],
429
+ );
381
430
 
382
431
  const refreshAllProviders = useCallback(async () => {
383
432
  const configuredProviders = Array.from(providers.entries())
384
433
  .filter(([_, state]) => state.configured)
385
434
  .map(([id]) => id);
386
435
 
387
- info(`Refreshing ${configuredProviders.length} providers`, { providers: configuredProviders }, 'refresh');
436
+ info(
437
+ `Refreshing ${configuredProviders.length} providers`,
438
+ { providers: configuredProviders },
439
+ "refresh",
440
+ );
388
441
 
389
442
  await Promise.all(configuredProviders.map(refreshProvider));
390
443
  }, [providers, refreshProvider, info]);
@@ -398,17 +451,13 @@ export function PluginProvider({ children, cliPlugins }: PluginProviderProps) {
398
451
  refreshAllProviders,
399
452
  };
400
453
 
401
- return (
402
- <PluginContext.Provider value={value}>
403
- {children}
404
- </PluginContext.Provider>
405
- );
454
+ return <PluginContext.Provider value={value}>{children}</PluginContext.Provider>;
406
455
  }
407
456
 
408
457
  export function usePlugins(): PluginContextValue {
409
458
  const context = useContext(PluginContext);
410
459
  if (!context) {
411
- throw new Error('usePlugins must be used within PluginProvider');
460
+ throw new Error("usePlugins must be used within PluginProvider");
412
461
  }
413
462
  return context;
414
463
  }