@tokentop/ttop 0.4.0 → 0.6.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.
- package/README.md +7 -6
- package/package.json +12 -5
- package/src/agents/aggregator.test.ts +306 -0
- package/src/agents/aggregator.ts +44 -2
- package/src/agents/types.ts +3 -0
- package/src/cli.ts +16 -2
- package/src/demo/simulator.ts +2 -2
- package/src/plugins/agents/index.ts +3 -0
- package/src/plugins/auth-sources.ts +2 -3
- package/src/plugins/lifecycle.ts +2 -2
- package/src/plugins/loader.test.ts +1 -1
- package/src/plugins/loader.ts +15 -7
- package/src/plugins/notification-bus.ts +5 -2
- package/src/plugins/notifications/terminal-bell.ts +1 -1
- package/src/plugins/notifications/visual-flash.ts +1 -1
- package/src/plugins/plugin-context-factory.ts +4 -19
- package/src/plugins/plugin-host.test.ts +197 -0
- package/src/plugins/plugin-host.ts +45 -1
- package/src/plugins/providers/anthropic.test.ts +202 -0
- package/src/plugins/providers/anthropic.ts +21 -2
- package/src/plugins/providers/antigravity.ts +17 -1
- package/src/plugins/providers/chutes.ts +205 -0
- package/src/plugins/providers/codex.ts +17 -1
- package/src/plugins/providers/gemini.ts +17 -1
- package/src/plugins/providers/github-copilot.ts +17 -1
- package/src/plugins/providers/index.ts +1 -0
- package/src/plugins/providers/minimax.ts +17 -1
- package/src/plugins/providers/openai-api.ts +53 -1
- package/src/plugins/providers/opencode-zen.ts +1 -1
- package/src/plugins/providers/perplexity.ts +11 -2
- package/src/plugins/providers/zai.ts +17 -1
- package/src/plugins/registry.ts +9 -9
- package/src/plugins/sandbox-guard.test.ts +1 -1
- package/src/plugins/sandbox-guard.ts +1 -1
- package/src/plugins/sandbox.ts +1 -1
- package/src/plugins/themes/catppuccin-latte.ts +1 -1
- package/src/plugins/themes/catppuccin-mocha.ts +1 -1
- package/src/plugins/themes/claude-code.ts +1 -1
- package/src/plugins/themes/dracula.ts +1 -1
- package/src/plugins/themes/github-light.ts +1 -1
- package/src/plugins/themes/gruvbox-dark.ts +1 -1
- package/src/plugins/themes/gruvbox-light.ts +1 -1
- package/src/plugins/themes/kanagawa.ts +1 -1
- package/src/plugins/themes/nord.ts +1 -1
- package/src/plugins/themes/one-dark.ts +1 -1
- package/src/plugins/themes/opencode.ts +1 -1
- package/src/plugins/themes/rose-pine-dawn.ts +1 -1
- package/src/plugins/themes/rose-pine.ts +1 -1
- package/src/plugins/themes/solarized-light.ts +1 -1
- package/src/plugins/themes/tokyo-night.ts +1 -1
- package/src/plugins/update-checker.ts +2 -2
- package/src/pricing/estimator.test.ts +1 -1
- package/src/pricing/estimator.ts +1 -1
- package/src/pricing/fallback.ts +5 -1
- package/src/pricing/index.ts +1 -1
- package/src/pricing/models-dev.ts +1 -1
- package/src/storage/index.ts +6 -0
- package/src/storage/migrations/index.ts +17 -0
- package/src/storage/repos/pluginStorage.ts +35 -0
- package/src/tui/App.tsx +17 -9
- package/src/tui/components/HelpOverlay.tsx +1 -1
- package/src/tui/components/LimitGauge.tsx +12 -4
- package/src/tui/components/ProviderCard.tsx +39 -23
- package/src/tui/components/ProviderDetailPanel.tsx +1 -1
- package/src/tui/components/ProviderLimitsPanel.tsx +5 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +10 -0
- package/src/tui/components/SessionsTable.tsx +62 -37
- package/src/tui/components/SettingsModal.tsx +1 -1
- package/src/tui/components/Skeleton.tsx +9 -6
- package/src/tui/components/SortOverlay.tsx +167 -0
- package/src/tui/components/Spinner.tsx +16 -11
- package/src/tui/components/ThemePicker.tsx +1 -1
- package/src/tui/contexts/AgentSessionContext.tsx +70 -17
- package/src/tui/contexts/PluginContext.tsx +72 -14
- package/src/tui/contexts/RealTimeActivityContext.tsx +33 -20
- package/src/tui/contexts/ThemeContext.tsx +7 -4
- package/src/tui/createApp.tsx +2 -0
- package/src/tui/hooks/index.ts +1 -0
- package/src/tui/hooks/useAnimatedValue.ts +29 -26
- package/src/tui/hooks/useAnimationTick.ts +93 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +11 -4
- package/src/tui/hooks/useEntranceAnimation.ts +16 -10
- package/src/tui/hooks/usePulse.ts +13 -7
- package/src/tui/hooks/useSessionEnrichedProviders.ts +1 -1
- package/src/tui/hooks/useValueFlash.ts +25 -33
- package/src/tui/index.tsx +4 -0
- package/src/tui/types/projectSort.ts +35 -0
- package/src/tui/types/sort.ts +61 -0
- package/src/tui/views/ProjectsView.tsx +104 -38
- package/src/tui/views/RealTimeDashboard.tsx +91 -8
- package/src/tui/views/SettingsView.tsx +1 -1
- package/src/utils/__tests__/error-category.test.ts +147 -0
- package/src/utils/currency.ts +4 -2
- package/src/utils/error-category.ts +91 -0
- package/src/version.ts +1 -1
- package/src/plugins/types/agent.ts +0 -108
- package/src/plugins/types/base.ts +0 -278
- package/src/plugins/types/index.ts +0 -19
- package/src/plugins/types/notification.ts +0 -51
- package/src/plugins/types/provider.ts +0 -237
- package/src/plugins/types/theme.ts +0 -78
- package/src/storage/database.ts +0 -623
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* skipped because they have no registry entry to compare against.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { PluginType } from "
|
|
12
|
+
import type { PluginType } from "@tokentop/plugin-sdk";
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Types
|
|
@@ -60,7 +60,7 @@ function isNewer(current: string, latest: string): boolean {
|
|
|
60
60
|
* Uses the abbreviated metadata endpoint for minimal payload.
|
|
61
61
|
*/
|
|
62
62
|
async function fetchLatestVersion(packageName: string): Promise<string> {
|
|
63
|
-
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).
|
|
63
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName).replaceAll("%40", "@")}`;
|
|
64
64
|
const res = await fetch(url, {
|
|
65
65
|
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
66
66
|
signal: AbortSignal.timeout(8_000),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ModelPricing } from "
|
|
2
|
+
import type { ModelPricing } from "@tokentop/plugin-sdk";
|
|
3
3
|
import { estimateCost, estimateSessionCost, formatCost, formatTokenCount } from "./estimator.ts";
|
|
4
4
|
|
|
5
5
|
describe("estimateCost()", () => {
|
package/src/pricing/estimator.ts
CHANGED
package/src/pricing/fallback.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelPricing } from "
|
|
1
|
+
import type { ModelPricing } from "@tokentop/plugin-sdk";
|
|
2
2
|
|
|
3
3
|
//TODO: get more up to date fallback pricing
|
|
4
4
|
export const FALLBACK_PRICING: Record<string, Record<string, ModelPricing>> = {
|
|
@@ -57,6 +57,10 @@ export const FALLBACK_PRICING: Record<string, Record<string, ModelPricing>> = {
|
|
|
57
57
|
"gemini-2.0-flash-lite": { input: 0.075, output: 0.3, source: "fallback" },
|
|
58
58
|
"gemini-2.5-pro": { input: 1.25, output: 10, source: "fallback" },
|
|
59
59
|
"gemini-2.5-flash": { input: 0.15, output: 0.6, source: "fallback" },
|
|
60
|
+
"gemini-3-flash-preview": { input: 0.5, output: 3, cacheRead: 0.05, source: "fallback" },
|
|
61
|
+
"gemini-3-pro-preview": { input: 2, output: 12, cacheRead: 0.2, source: "fallback" },
|
|
62
|
+
"gemini-3.1-pro-preview": { input: 2, output: 12, cacheRead: 0.2, source: "fallback" },
|
|
63
|
+
"gemini-3.1-flash-lite-preview": { input: 0.25, output: 1.5, source: "fallback" },
|
|
60
64
|
},
|
|
61
65
|
|
|
62
66
|
openrouter: {
|
package/src/pricing/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelPricing, ProviderPlugin, ProviderPricing } from "
|
|
1
|
+
import type { ModelPricing, ProviderPlugin, ProviderPricing } from "@tokentop/plugin-sdk";
|
|
2
2
|
import { getFallbackPricing, getFallbackProviderPricing } from "./fallback.ts";
|
|
3
3
|
import {
|
|
4
4
|
clearCache as clearModelsDevCache,
|
package/src/storage/index.ts
CHANGED
|
@@ -15,6 +15,12 @@ export {
|
|
|
15
15
|
insertAgentSessionSnapshot,
|
|
16
16
|
upsertAgentSession,
|
|
17
17
|
} from "./repos/agentSessions.ts";
|
|
18
|
+
export {
|
|
19
|
+
pluginStorageDelete,
|
|
20
|
+
pluginStorageGet,
|
|
21
|
+
pluginStorageHas,
|
|
22
|
+
pluginStorageSet,
|
|
23
|
+
} from "./repos/pluginStorage.ts";
|
|
18
24
|
export {
|
|
19
25
|
getLatestProviderSnapshot,
|
|
20
26
|
insertProviderSnapshot,
|
|
@@ -164,6 +164,23 @@ export const migrations: Migration[] = [
|
|
|
164
164
|
`);
|
|
165
165
|
},
|
|
166
166
|
},
|
|
167
|
+
{
|
|
168
|
+
version: 2,
|
|
169
|
+
up: (db: Database) => {
|
|
170
|
+
db.exec(`
|
|
171
|
+
CREATE TABLE IF NOT EXISTS plugin_storage (
|
|
172
|
+
plugin_id TEXT NOT NULL,
|
|
173
|
+
key TEXT NOT NULL,
|
|
174
|
+
value TEXT NOT NULL,
|
|
175
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
|
176
|
+
PRIMARY KEY (plugin_id, key)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_storage_plugin
|
|
180
|
+
ON plugin_storage(plugin_id);
|
|
181
|
+
`);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
167
184
|
];
|
|
168
185
|
|
|
169
186
|
export function getCurrentVersion(db: Database): number {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getDatabase } from "../db.ts";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Plugin KV storage
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
export function pluginStorageGet(pluginId: string, key: string): string | null {
|
|
8
|
+
const db = getDatabase();
|
|
9
|
+
const row = db
|
|
10
|
+
.prepare("SELECT value FROM plugin_storage WHERE plugin_id = ? AND key = ?")
|
|
11
|
+
.get(pluginId, key) as { value: string } | null;
|
|
12
|
+
return row?.value ?? null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function pluginStorageSet(pluginId: string, key: string, value: string): void {
|
|
16
|
+
const db = getDatabase();
|
|
17
|
+
db.prepare(
|
|
18
|
+
`INSERT INTO plugin_storage (plugin_id, key, value, updated_at)
|
|
19
|
+
VALUES (?, ?, ?, ?)
|
|
20
|
+
ON CONFLICT(plugin_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
|
|
21
|
+
).run(pluginId, key, value, Math.floor(Date.now() / 1000));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function pluginStorageDelete(pluginId: string, key: string): void {
|
|
25
|
+
const db = getDatabase();
|
|
26
|
+
db.prepare("DELETE FROM plugin_storage WHERE plugin_id = ? AND key = ?").run(pluginId, key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function pluginStorageHas(pluginId: string, key: string): boolean {
|
|
30
|
+
const db = getDatabase();
|
|
31
|
+
const row = db
|
|
32
|
+
.prepare("SELECT 1 FROM plugin_storage WHERE plugin_id = ? AND key = ?")
|
|
33
|
+
.get(pluginId, key);
|
|
34
|
+
return row !== null && row !== undefined;
|
|
35
|
+
}
|
package/src/tui/App.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import type { ThemePlugin } from "@tokentop/plugin-sdk";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
4
|
import * as builtinThemeExports from "@/plugins/themes/index.ts";
|
|
4
|
-
import type { ThemePlugin } from "@/plugins/types/theme.ts";
|
|
5
5
|
import { resolveTheme, ThemeProvider, useColors, useTheme } from "./contexts/ThemeContext.tsx";
|
|
6
6
|
import {
|
|
7
7
|
type BurstRecorder,
|
|
@@ -48,6 +48,7 @@ interface AppProps {
|
|
|
48
48
|
demoSeed?: number;
|
|
49
49
|
demoPreset?: DemoPreset;
|
|
50
50
|
cliPlugins?: string[];
|
|
51
|
+
cliTheme?: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
type View = "dashboard" | "providers" | "trends" | "projects";
|
|
@@ -55,7 +56,7 @@ type View = "dashboard" | "providers" | "trends" | "projects";
|
|
|
55
56
|
function AppContent() {
|
|
56
57
|
const renderer = useSafeRenderer();
|
|
57
58
|
const colors = useColors();
|
|
58
|
-
const { theme, setTheme } = useTheme();
|
|
59
|
+
const { theme, setTheme, cliTheme } = useTheme();
|
|
59
60
|
const { refreshAllProviders, isInitialized, themes } = usePlugins();
|
|
60
61
|
const { info } = useLogs();
|
|
61
62
|
const { toast, showToast, dismissToast } = useToastContext();
|
|
@@ -292,8 +293,10 @@ function AppContent() {
|
|
|
292
293
|
|
|
293
294
|
useEffect(() => {
|
|
294
295
|
if (themes.length === 0) return;
|
|
296
|
+
// CLI --theme flag takes priority over config
|
|
297
|
+
const themeId = cliTheme ?? config.display.theme;
|
|
295
298
|
const resolved = resolveTheme(
|
|
296
|
-
|
|
299
|
+
themeId,
|
|
297
300
|
config.display.colorScheme,
|
|
298
301
|
themes,
|
|
299
302
|
renderer?.themeMode ?? null,
|
|
@@ -301,7 +304,7 @@ function AppContent() {
|
|
|
301
304
|
if (resolved.id !== theme.id) {
|
|
302
305
|
setTheme(resolved);
|
|
303
306
|
}
|
|
304
|
-
}, [themes, config.display.theme, config.display.colorScheme, renderer]);
|
|
307
|
+
}, [themes, cliTheme, config.display.theme, config.display.colorScheme, renderer]);
|
|
305
308
|
|
|
306
309
|
useEffect(() => {
|
|
307
310
|
if (isInitialized) {
|
|
@@ -364,23 +367,24 @@ function AppContent() {
|
|
|
364
367
|
);
|
|
365
368
|
}
|
|
366
369
|
|
|
367
|
-
function ConfiguredApp({ cliPlugins }: { cliPlugins?: string[] }) {
|
|
370
|
+
function ConfiguredApp({ cliPlugins, cliTheme }: { cliPlugins?: string[]; cliTheme?: string }) {
|
|
368
371
|
const { config, isLoading } = useConfig();
|
|
369
372
|
|
|
370
373
|
if (isLoading) {
|
|
371
374
|
return null;
|
|
372
375
|
}
|
|
373
376
|
|
|
374
|
-
const
|
|
377
|
+
const themeId = cliTheme ?? config.display.theme;
|
|
378
|
+
const initialTheme = builtinThemeList.find((t) => t.id === themeId);
|
|
375
379
|
const themeProps = initialTheme ? { initialTheme } : {};
|
|
376
380
|
|
|
377
381
|
return (
|
|
378
|
-
<ThemeProvider {...themeProps}>
|
|
382
|
+
<ThemeProvider {...themeProps} {...(cliTheme ? { cliTheme } : {})}>
|
|
379
383
|
<TimeWindowProvider defaultWindow={config.display.defaultTimeWindow}>
|
|
380
384
|
<ToastProvider>
|
|
381
385
|
<PluginProvider {...(cliPlugins ? { cliPlugins } : {})}>
|
|
382
386
|
<RealTimeActivityProvider>
|
|
383
|
-
<AgentSessionProvider autoRefresh={true}
|
|
387
|
+
<AgentSessionProvider autoRefresh={true}>
|
|
384
388
|
<DashboardRuntimeProvider>
|
|
385
389
|
<DrawerProvider>
|
|
386
390
|
<AppContent />
|
|
@@ -401,6 +405,7 @@ export function App({
|
|
|
401
405
|
demoSeed,
|
|
402
406
|
demoPreset,
|
|
403
407
|
cliPlugins,
|
|
408
|
+
cliTheme,
|
|
404
409
|
}: AppProps) {
|
|
405
410
|
const demoProviderProps: { demoMode: boolean; demoSeed?: number; demoPreset?: DemoPreset } = {
|
|
406
411
|
demoMode,
|
|
@@ -414,7 +419,10 @@ export function App({
|
|
|
414
419
|
<InputProvider>
|
|
415
420
|
<StorageProvider>
|
|
416
421
|
<ConfigProvider>
|
|
417
|
-
<ConfiguredApp
|
|
422
|
+
<ConfiguredApp
|
|
423
|
+
{...(cliPlugins ? { cliPlugins } : {})}
|
|
424
|
+
{...(cliTheme ? { cliTheme } : {})}
|
|
425
|
+
/>
|
|
418
426
|
</ConfigProvider>
|
|
419
427
|
</StorageProvider>
|
|
420
428
|
</InputProvider>
|
|
@@ -59,7 +59,7 @@ export function HelpOverlay() {
|
|
|
59
59
|
<text width={10} height={1} fg={colors.textMuted}>
|
|
60
60
|
s
|
|
61
61
|
</text>
|
|
62
|
-
<text height={1}>Sort</text>
|
|
62
|
+
<text height={1}>Sort menu</text>
|
|
63
63
|
</box>
|
|
64
64
|
<box flexDirection="row" height={1}>
|
|
65
65
|
<text width={10} height={1} fg={colors.textMuted}>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { getErrorCategoryDisplay, type ProviderErrorCategory } from "@/utils/error-category.ts";
|
|
2
3
|
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
3
4
|
import { usePulse } from "../hooks/usePulse.ts";
|
|
4
5
|
|
|
@@ -8,6 +9,7 @@ interface LimitGaugeProps {
|
|
|
8
9
|
color: string;
|
|
9
10
|
ghost?: boolean;
|
|
10
11
|
error?: string;
|
|
12
|
+
errorCategory?: ProviderErrorCategory;
|
|
11
13
|
resetTime?: string;
|
|
12
14
|
compact?: boolean;
|
|
13
15
|
labelWidth?: number;
|
|
@@ -89,6 +91,7 @@ export function LimitGauge({
|
|
|
89
91
|
color,
|
|
90
92
|
ghost = false,
|
|
91
93
|
error,
|
|
94
|
+
errorCategory,
|
|
92
95
|
resetTime,
|
|
93
96
|
compact = false,
|
|
94
97
|
labelWidth = 12,
|
|
@@ -152,7 +155,12 @@ export function LimitGauge({
|
|
|
152
155
|
|
|
153
156
|
if (error) {
|
|
154
157
|
const displayLabel = truncateLabel(label, labelWidth);
|
|
155
|
-
|
|
158
|
+
|
|
159
|
+
const display = errorCategory ? getErrorCategoryDisplay(errorCategory) : undefined;
|
|
160
|
+
const errColor = display?.isWarning ? colors.warning : colors.error;
|
|
161
|
+
const errLabel = display?.label ?? "ERR";
|
|
162
|
+
const errorIcon = selected ? "\u258C" : (display?.icon ?? "\u2717");
|
|
163
|
+
|
|
156
164
|
return (
|
|
157
165
|
<box
|
|
158
166
|
height={1}
|
|
@@ -160,10 +168,10 @@ export function LimitGauge({
|
|
|
160
168
|
{...(selected ? { backgroundColor: colors.borderMuted } : {})}
|
|
161
169
|
>
|
|
162
170
|
<text height={1}>
|
|
163
|
-
<span fg={selected ? colors.primary :
|
|
171
|
+
<span fg={selected ? colors.primary : errColor}>{errorIcon} </span>
|
|
164
172
|
<span fg={selected ? colors.text : colors.text}>{displayLabel} </span>
|
|
165
|
-
<span fg={
|
|
166
|
-
<span fg={
|
|
173
|
+
<span fg={errColor}>{"\u2500".repeat(barWidth)}</span>
|
|
174
|
+
<span fg={errColor}> {errLabel}</span>
|
|
167
175
|
</text>
|
|
168
176
|
</box>
|
|
169
177
|
);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { BoxRenderable } from "@opentui/core";
|
|
2
2
|
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import type { ProviderUsageData } from "@tokentop/plugin-sdk";
|
|
4
|
+
import { forwardRef, useCallback } from "react";
|
|
5
|
+
import { classifyProviderError, getErrorCategoryDisplay } from "@/utils/error-category.ts";
|
|
5
6
|
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
7
|
+
import { usePulse } from "../hooks/usePulse.ts";
|
|
6
8
|
import { SkeletonProviderContent } from "./Skeleton.tsx";
|
|
7
|
-
import {
|
|
9
|
+
import { Spinner } from "./Spinner.tsx";
|
|
8
10
|
import { UsageGauge } from "./UsageGauge.tsx";
|
|
9
11
|
|
|
10
12
|
interface CompactGaugeProps {
|
|
@@ -60,9 +62,11 @@ export const ProviderCard = forwardRef<BoxRenderable, ProviderCardProps>(
|
|
|
60
62
|
const colors = useColors();
|
|
61
63
|
const { width: termWidth } = useTerminalDimensions();
|
|
62
64
|
const providerColor = color ?? colors.primary;
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const pulseStep = usePulse({
|
|
66
|
+
enabled: usage?.limitReached ?? false,
|
|
67
|
+
intervalMs: 150,
|
|
68
|
+
steps: 12,
|
|
69
|
+
});
|
|
66
70
|
|
|
67
71
|
const rawItems = usage?.limits?.items;
|
|
68
72
|
const hasItems = rawItems && rawItems.length > 0;
|
|
@@ -78,36 +82,34 @@ export const ProviderCard = forwardRef<BoxRenderable, ProviderCardProps>(
|
|
|
78
82
|
sortedItems.length > maxVisibleGauges ? sortedItems.slice(0, maxVisibleGauges) : sortedItems;
|
|
79
83
|
const useCompactMode = visibleItems.length > 3;
|
|
80
84
|
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (!usage?.limitReached) {
|
|
83
|
-
setPulseStep(0);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const timer = setInterval(() => {
|
|
88
|
-
setPulseStep((p) => (p + 1) % 12);
|
|
89
|
-
}, 150);
|
|
90
|
-
|
|
91
|
-
return () => clearInterval(timer);
|
|
92
|
-
}, [usage?.limitReached]);
|
|
93
|
-
|
|
94
85
|
const handleClick = useCallback(() => {
|
|
95
86
|
if (onFocus) {
|
|
96
87
|
onFocus();
|
|
97
88
|
}
|
|
98
89
|
}, [onFocus]);
|
|
99
90
|
|
|
91
|
+
const errorCategory = usage?.error ? classifyProviderError(usage.error) : undefined;
|
|
92
|
+
const errorDisplay = errorCategory ? getErrorCategoryDisplay(errorCategory) : undefined;
|
|
93
|
+
|
|
100
94
|
const statusColor = !configured
|
|
101
95
|
? colors.textSubtle
|
|
102
96
|
: loading
|
|
103
97
|
? colors.info
|
|
104
98
|
: usage?.error
|
|
105
|
-
?
|
|
99
|
+
? errorDisplay?.isWarning
|
|
100
|
+
? colors.warning
|
|
101
|
+
: colors.error
|
|
106
102
|
: usage?.limitReached
|
|
107
103
|
? colors.warning
|
|
108
104
|
: colors.success;
|
|
109
105
|
|
|
110
|
-
const statusIcon = !configured
|
|
106
|
+
const statusIcon = !configured
|
|
107
|
+
? "○"
|
|
108
|
+
: usage?.error
|
|
109
|
+
? (errorDisplay?.icon ?? "✗")
|
|
110
|
+
: usage?.limitReached
|
|
111
|
+
? "!"
|
|
112
|
+
: "●";
|
|
111
113
|
|
|
112
114
|
const isInitialLoad = loading && !usage;
|
|
113
115
|
|
|
@@ -154,7 +156,11 @@ export const ProviderCard = forwardRef<BoxRenderable, ProviderCardProps>(
|
|
|
154
156
|
<strong>{name}</strong>
|
|
155
157
|
</span>
|
|
156
158
|
</text>
|
|
157
|
-
|
|
159
|
+
{configured && loading ? (
|
|
160
|
+
<Spinner color={statusColor} />
|
|
161
|
+
) : (
|
|
162
|
+
<text fg={statusColor}>{statusIcon}</text>
|
|
163
|
+
)}
|
|
158
164
|
</box>
|
|
159
165
|
|
|
160
166
|
{!configured && <text fg={colors.textSubtle}>Not configured</text>}
|
|
@@ -162,7 +168,17 @@ export const ProviderCard = forwardRef<BoxRenderable, ProviderCardProps>(
|
|
|
162
168
|
{configured && isInitialLoad && <SkeletonProviderContent />}
|
|
163
169
|
|
|
164
170
|
{configured && !isInitialLoad && usage?.error && (
|
|
165
|
-
<
|
|
171
|
+
<box flexDirection="column" gap={0}>
|
|
172
|
+
<text fg={errorDisplay?.isWarning ? colors.warning : colors.error}>
|
|
173
|
+
<span>
|
|
174
|
+
<bold>{errorDisplay?.label ?? "ERR"}</bold>
|
|
175
|
+
</span>
|
|
176
|
+
<span fg={colors.textMuted}> — </span>
|
|
177
|
+
<span fg={errorDisplay?.isWarning ? colors.warning : colors.error}>
|
|
178
|
+
{usage.error}
|
|
179
|
+
</span>
|
|
180
|
+
</text>
|
|
181
|
+
</box>
|
|
166
182
|
)}
|
|
167
183
|
|
|
168
184
|
{configured && !isInitialLoad && usage && !usage.error && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UsageLimit } from "
|
|
1
|
+
import type { UsageLimit } from "@tokentop/plugin-sdk";
|
|
2
2
|
import type { ProviderState } from "../contexts/PluginContext.tsx";
|
|
3
3
|
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
4
4
|
import { Sparkline } from "./Sparkline.tsx";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useTerminalDimensions } from "@opentui/react";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
|
+
import type { ProviderErrorCategory } from "@/utils/error-category.ts";
|
|
3
4
|
import { useConfig } from "../contexts/ConfigContext.tsx";
|
|
4
5
|
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
5
6
|
import { usePulse } from "../hooks/usePulse.ts";
|
|
@@ -34,6 +35,7 @@ interface ProviderData {
|
|
|
34
35
|
usedPercent: number;
|
|
35
36
|
color: string;
|
|
36
37
|
error?: string;
|
|
38
|
+
errorCategory?: ProviderErrorCategory;
|
|
37
39
|
resetTime?: string;
|
|
38
40
|
used?: number;
|
|
39
41
|
limit?: number;
|
|
@@ -186,6 +188,7 @@ export function ProviderLimitsPanel({
|
|
|
186
188
|
usedPercent={p.usedPercent}
|
|
187
189
|
color={p.color}
|
|
188
190
|
{...(p.error ? { error: p.error } : {})}
|
|
191
|
+
{...(p.errorCategory ? { errorCategory: p.errorCategory } : {})}
|
|
189
192
|
compact={true}
|
|
190
193
|
selected={focused && startIndex + idx === safeSelectedIndex}
|
|
191
194
|
warningThreshold={warningThreshold}
|
|
@@ -269,6 +272,7 @@ export function ProviderLimitsPanel({
|
|
|
269
272
|
usedPercent={p.usedPercent}
|
|
270
273
|
color={p.color}
|
|
271
274
|
{...(p.error ? { error: p.error } : {})}
|
|
275
|
+
{...(p.errorCategory ? { errorCategory: p.errorCategory } : {})}
|
|
272
276
|
{...(p.resetTime ? { resetTime: p.resetTime } : {})}
|
|
273
277
|
labelWidth={14}
|
|
274
278
|
barWidth={12}
|
|
@@ -326,6 +330,7 @@ export function ProviderLimitsPanel({
|
|
|
326
330
|
usedPercent={p.usedPercent}
|
|
327
331
|
color={p.color}
|
|
328
332
|
{...(p.error ? { error: p.error } : {})}
|
|
333
|
+
{...(p.errorCategory ? { errorCategory: p.errorCategory } : {})}
|
|
329
334
|
{...(p.resetTime ? { resetTime: p.resetTime } : {})}
|
|
330
335
|
labelWidth={12}
|
|
331
336
|
barWidth={10}
|
|
@@ -314,6 +314,16 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
|
|
|
314
314
|
</text>
|
|
315
315
|
</box>
|
|
316
316
|
|
|
317
|
+
{session.metadata?.isEstimated === true && (
|
|
318
|
+
<box flexDirection="row" height={1} marginBottom={0}>
|
|
319
|
+
<text height={1} overflow="hidden">
|
|
320
|
+
<span fg={colors.warning}>
|
|
321
|
+
≈ Token data is estimated — server enrichment pending
|
|
322
|
+
</span>
|
|
323
|
+
</text>
|
|
324
|
+
</box>
|
|
325
|
+
)}
|
|
326
|
+
|
|
317
327
|
{hasCacheData && (
|
|
318
328
|
<box flexDirection="row" height={1} marginBottom={0}>
|
|
319
329
|
<text height={1} overflow="hidden">
|