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