@tokentop/ttop 0.2.2
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/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/ttop.js +49 -0
- package/package.json +68 -0
- package/src/agents/aggregator.ts +176 -0
- package/src/agents/costing.ts +74 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/types.ts +71 -0
- package/src/cli.ts +167 -0
- package/src/config/schema.test.ts +166 -0
- package/src/config/schema.ts +165 -0
- package/src/demo/simulator.ts +745 -0
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +186 -0
- package/src/plugins/lifecycle.ts +177 -0
- package/src/plugins/loader.test.ts +195 -0
- package/src/plugins/loader.ts +282 -0
- package/src/plugins/notification-bus.ts +153 -0
- package/src/plugins/notifications/index.ts +2 -0
- package/src/plugins/notifications/terminal-bell.ts +66 -0
- package/src/plugins/notifications/visual-flash.ts +57 -0
- package/src/plugins/npm-installer.ts +164 -0
- package/src/plugins/plugin-context-factory.ts +67 -0
- package/src/plugins/plugin-host.ts +205 -0
- package/src/plugins/providers/anthropic.ts +325 -0
- package/src/plugins/providers/antigravity.ts +309 -0
- package/src/plugins/providers/codex.ts +250 -0
- package/src/plugins/providers/gemini.ts +391 -0
- package/src/plugins/providers/github-copilot.ts +194 -0
- package/src/plugins/providers/index.ts +12 -0
- package/src/plugins/providers/minimax.ts +223 -0
- package/src/plugins/providers/openai-api.ts +250 -0
- package/src/plugins/providers/opencode-zen.ts +125 -0
- package/src/plugins/providers/perplexity.ts +177 -0
- package/src/plugins/providers/zai.ts +225 -0
- package/src/plugins/registry.ts +251 -0
- package/src/plugins/sandbox-guard.test.ts +191 -0
- package/src/plugins/sandbox-guard.ts +155 -0
- package/src/plugins/sandbox.ts +138 -0
- package/src/plugins/themes/catppuccin-latte.ts +45 -0
- package/src/plugins/themes/catppuccin-mocha.ts +45 -0
- package/src/plugins/themes/claude-code.ts +45 -0
- package/src/plugins/themes/dracula.ts +45 -0
- package/src/plugins/themes/github-light.ts +45 -0
- package/src/plugins/themes/gruvbox-dark.ts +45 -0
- package/src/plugins/themes/gruvbox-light.ts +45 -0
- package/src/plugins/themes/index.ts +15 -0
- package/src/plugins/themes/kanagawa.ts +45 -0
- package/src/plugins/themes/nord.ts +45 -0
- package/src/plugins/themes/one-dark.ts +45 -0
- package/src/plugins/themes/opencode.ts +47 -0
- package/src/plugins/themes/rose-pine-dawn.ts +45 -0
- package/src/plugins/themes/rose-pine.ts +45 -0
- package/src/plugins/themes/solarized-light.ts +45 -0
- package/src/plugins/themes/tokyo-night.ts +49 -0
- package/src/plugins/types/agent.ts +108 -0
- package/src/plugins/types/base.ts +267 -0
- package/src/plugins/types/index.ts +19 -0
- package/src/plugins/types/notification.ts +51 -0
- package/src/plugins/types/provider.ts +233 -0
- package/src/plugins/types/theme.ts +70 -0
- package/src/plugins/update-checker.ts +177 -0
- package/src/pricing/estimator.test.ts +181 -0
- package/src/pricing/estimator.ts +80 -0
- package/src/pricing/fallback.test.ts +71 -0
- package/src/pricing/fallback.ts +62 -0
- package/src/pricing/index.ts +66 -0
- package/src/pricing/models-dev.ts +130 -0
- package/src/storage/database.ts +613 -0
- package/src/storage/db.ts +64 -0
- package/src/storage/index.ts +66 -0
- package/src/storage/migrations/index.ts +189 -0
- package/src/storage/paths.ts +24 -0
- package/src/storage/repos/agentSessions.ts +210 -0
- package/src/storage/repos/providerSnapshots.ts +116 -0
- package/src/storage/repos/usageEvents.ts +334 -0
- package/src/storage/types.ts +288 -0
- package/src/tui/App.tsx +381 -0
- package/src/tui/components/CommandPalette.tsx +166 -0
- package/src/tui/components/DebugConsole.tsx +161 -0
- package/src/tui/components/DebugPanel.tsx +660 -0
- package/src/tui/components/FooterHints.tsx +22 -0
- package/src/tui/components/GhostProviderCard.tsx +45 -0
- package/src/tui/components/Header.tsx +125 -0
- package/src/tui/components/HelpOverlay.tsx +45 -0
- package/src/tui/components/InlineGauge.tsx +34 -0
- package/src/tui/components/InlineSparkline.tsx +78 -0
- package/src/tui/components/KpiStrip.tsx +246 -0
- package/src/tui/components/LimitGauge.tsx +186 -0
- package/src/tui/components/ModalBackdrop.tsx +42 -0
- package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
- package/src/tui/components/ProviderCard.tsx +309 -0
- package/src/tui/components/ProviderDetailPanel.tsx +237 -0
- package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
- package/src/tui/components/ProvidersList.tsx +380 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
- package/src/tui/components/SessionsTable.tsx +401 -0
- package/src/tui/components/SettingsModal.tsx +587 -0
- package/src/tui/components/SidebarBreakdown.tsx +87 -0
- package/src/tui/components/Skeleton.tsx +102 -0
- package/src/tui/components/SmartSidebar.tsx +644 -0
- package/src/tui/components/Sparkline.tsx +180 -0
- package/src/tui/components/Spinner.tsx +36 -0
- package/src/tui/components/StatusBar.tsx +96 -0
- package/src/tui/components/ThemePicker.tsx +234 -0
- package/src/tui/components/Toast.tsx +61 -0
- package/src/tui/components/UsageGauge.tsx +80 -0
- package/src/tui/components/index.ts +5 -0
- package/src/tui/contexts/AgentSessionContext.tsx +307 -0
- package/src/tui/contexts/ConfigContext.tsx +141 -0
- package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
- package/src/tui/contexts/DemoModeContext.tsx +60 -0
- package/src/tui/contexts/DrawerContext.tsx +42 -0
- package/src/tui/contexts/InputContext.tsx +30 -0
- package/src/tui/contexts/LogContext.tsx +150 -0
- package/src/tui/contexts/PluginContext.tsx +424 -0
- package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
- package/src/tui/contexts/StorageContext.tsx +233 -0
- package/src/tui/contexts/ThemeContext.tsx +95 -0
- package/src/tui/contexts/TimeWindowContext.tsx +158 -0
- package/src/tui/contexts/ToastContext.tsx +40 -0
- package/src/tui/contexts/index.ts +5 -0
- package/src/tui/createApp.tsx +47 -0
- package/src/tui/debug/captureFrame.ts +262 -0
- package/src/tui/debug/snapshot.tsx +644 -0
- package/src/tui/driver/assertions.ts +279 -0
- package/src/tui/driver/cli.ts +717 -0
- package/src/tui/driver/coverage.ts +204 -0
- package/src/tui/driver/demo-snapshot.ts +66 -0
- package/src/tui/driver/diff.ts +166 -0
- package/src/tui/driver/driver.ts +270 -0
- package/src/tui/driver/index.ts +53 -0
- package/src/tui/driver/recorder.ts +303 -0
- package/src/tui/driver/test-workflow.ts +115 -0
- package/src/tui/driver/test.ts +47 -0
- package/src/tui/hooks/index.ts +20 -0
- package/src/tui/hooks/useAnimatedValue.ts +113 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
- package/src/tui/hooks/useDashboardState.ts +116 -0
- package/src/tui/hooks/useEmaActivity.ts +223 -0
- package/src/tui/hooks/useEntranceAnimation.ts +62 -0
- package/src/tui/hooks/useExitAnimation.ts +137 -0
- package/src/tui/hooks/usePulse.ts +138 -0
- package/src/tui/hooks/useSafeRenderer.ts +24 -0
- package/src/tui/hooks/useValueFlash.ts +111 -0
- package/src/tui/index.tsx +50 -0
- package/src/tui/utils/providerColor.ts +32 -0
- package/src/tui/views/Dashboard.tsx +340 -0
- package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
- package/src/tui/views/ProjectsView.tsx +1277 -0
- package/src/tui/views/RealTimeDashboard.tsx +393 -0
- package/src/tui/views/SettingsView.tsx +465 -0
- package/src/tui/views/index.ts +2 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/opentui-test-utils.d.ts +18 -0
- package/src/utils/clipboard.ts +118 -0
- package/src/utils/currency.test.ts +106 -0
- package/src/utils/currency.ts +79 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
4
|
+
|
|
5
|
+
export interface LogEntry {
|
|
6
|
+
id: number;
|
|
7
|
+
level: LogLevel;
|
|
8
|
+
message: string;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
source?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface LogContextValue {
|
|
15
|
+
logs: LogEntry[];
|
|
16
|
+
isConsoleOpen: boolean;
|
|
17
|
+
toggleConsole: () => void;
|
|
18
|
+
openConsole: () => void;
|
|
19
|
+
closeConsole: () => void;
|
|
20
|
+
log: (level: LogLevel, message: string, data?: Record<string, unknown>, source?: string) => void;
|
|
21
|
+
debug: (message: string, data?: Record<string, unknown>, source?: string) => void;
|
|
22
|
+
info: (message: string, data?: Record<string, unknown>, source?: string) => void;
|
|
23
|
+
warn: (message: string, data?: Record<string, unknown>, source?: string) => void;
|
|
24
|
+
error: (message: string, data?: Record<string, unknown>, source?: string) => void;
|
|
25
|
+
clearLogs: () => void;
|
|
26
|
+
exportLogs: () => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LogContext = createContext<LogContextValue | null>(null);
|
|
30
|
+
|
|
31
|
+
let logIdCounter = 0;
|
|
32
|
+
const MAX_LOGS = 500;
|
|
33
|
+
|
|
34
|
+
interface LogProviderProps {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
debugEnabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function LogProvider({ children, debugEnabled = true }: LogProviderProps) {
|
|
40
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
41
|
+
const [isConsoleOpen, setIsConsoleOpen] = useState(false);
|
|
42
|
+
|
|
43
|
+
const addLog = useCallback((level: LogLevel, message: string, data?: Record<string, unknown>, source?: string) => {
|
|
44
|
+
if (level === 'debug' && !debugEnabled) return;
|
|
45
|
+
|
|
46
|
+
const entry: LogEntry = {
|
|
47
|
+
id: ++logIdCounter,
|
|
48
|
+
level,
|
|
49
|
+
message,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
...(data !== undefined ? { data } : {}),
|
|
52
|
+
...(source !== undefined ? { source } : {}),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
setLogs((prev) => {
|
|
56
|
+
const next = [...prev, entry];
|
|
57
|
+
if (next.length > MAX_LOGS) {
|
|
58
|
+
return next.slice(-MAX_LOGS);
|
|
59
|
+
}
|
|
60
|
+
return next;
|
|
61
|
+
});
|
|
62
|
+
}, [debugEnabled]);
|
|
63
|
+
|
|
64
|
+
const log = useCallback((level: LogLevel, message: string, data?: Record<string, unknown>, source?: string) => {
|
|
65
|
+
addLog(level, message, data, source);
|
|
66
|
+
}, [addLog]);
|
|
67
|
+
|
|
68
|
+
const debug = useCallback((message: string, data?: Record<string, unknown>, source?: string) => {
|
|
69
|
+
addLog('debug', message, data, source);
|
|
70
|
+
}, [addLog]);
|
|
71
|
+
|
|
72
|
+
const info = useCallback((message: string, data?: Record<string, unknown>, source?: string) => {
|
|
73
|
+
addLog('info', message, data, source);
|
|
74
|
+
}, [addLog]);
|
|
75
|
+
|
|
76
|
+
const warn = useCallback((message: string, data?: Record<string, unknown>, source?: string) => {
|
|
77
|
+
addLog('warn', message, data, source);
|
|
78
|
+
}, [addLog]);
|
|
79
|
+
|
|
80
|
+
const error = useCallback((message: string, data?: Record<string, unknown>, source?: string) => {
|
|
81
|
+
addLog('error', message, data, source);
|
|
82
|
+
}, [addLog]);
|
|
83
|
+
|
|
84
|
+
const clearLogs = useCallback(() => {
|
|
85
|
+
setLogs([]);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const toggleConsole = useCallback(() => {
|
|
89
|
+
setIsConsoleOpen((prev) => !prev);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const openConsole = useCallback(() => {
|
|
93
|
+
setIsConsoleOpen(true);
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
const closeConsole = useCallback(() => {
|
|
97
|
+
setIsConsoleOpen(false);
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const exportLogs = useCallback(() => {
|
|
101
|
+
const lines = logs.map((entry) => {
|
|
102
|
+
const time = new Date(entry.timestamp).toISOString();
|
|
103
|
+
const src = entry.source ? `[${entry.source}]` : '';
|
|
104
|
+
const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
|
|
105
|
+
return `${time} ${entry.level.toUpperCase().padEnd(5)} ${src} ${entry.message}${dataStr}`;
|
|
106
|
+
});
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}, [logs]);
|
|
109
|
+
|
|
110
|
+
const value: LogContextValue = {
|
|
111
|
+
logs,
|
|
112
|
+
isConsoleOpen,
|
|
113
|
+
toggleConsole,
|
|
114
|
+
openConsole,
|
|
115
|
+
closeConsole,
|
|
116
|
+
log,
|
|
117
|
+
debug,
|
|
118
|
+
info,
|
|
119
|
+
warn,
|
|
120
|
+
error,
|
|
121
|
+
clearLogs,
|
|
122
|
+
exportLogs,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<LogContext.Provider value={value}>
|
|
127
|
+
{children}
|
|
128
|
+
</LogContext.Provider>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useLogs(): LogContextValue {
|
|
133
|
+
const context = useContext(LogContext);
|
|
134
|
+
if (!context) {
|
|
135
|
+
throw new Error('useLogs must be used within LogProvider');
|
|
136
|
+
}
|
|
137
|
+
return context;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function useLogger(source: string) {
|
|
141
|
+
const { log, debug, info, warn, error } = useLogs();
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
log: (level: LogLevel, message: string, data?: Record<string, unknown>) => log(level, message, data, source),
|
|
145
|
+
debug: (message: string, data?: Record<string, unknown>) => debug(message, data, source),
|
|
146
|
+
info: (message: string, data?: Record<string, unknown>) => info(message, data, source),
|
|
147
|
+
warn: (message: string, data?: Record<string, unknown>) => warn(message, data, source),
|
|
148
|
+
error: (message: string, data?: Record<string, unknown>) => error(message, data, source),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
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';
|
|
18
|
+
|
|
19
|
+
export interface UsageSnapshot {
|
|
20
|
+
timestamp: number;
|
|
21
|
+
usedPercent: number | null;
|
|
22
|
+
limitReached?: boolean | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const HISTORY_SIZE = 30; // Keep last 30 snapshots per provider
|
|
26
|
+
|
|
27
|
+
export interface ProviderState {
|
|
28
|
+
plugin: ProviderPlugin;
|
|
29
|
+
configured: boolean;
|
|
30
|
+
usage: ProviderUsageData | null;
|
|
31
|
+
loading: boolean;
|
|
32
|
+
lastFetchAt: number | null;
|
|
33
|
+
history: UsageSnapshot[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PluginContextValue {
|
|
37
|
+
providers: Map<string, ProviderState>;
|
|
38
|
+
themes: ThemePlugin[];
|
|
39
|
+
notifications: NotificationPlugin[];
|
|
40
|
+
isInitialized: boolean;
|
|
41
|
+
refreshProvider: (providerId: string) => Promise<void>;
|
|
42
|
+
refreshAllProviders: () => Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PluginContext = createContext<PluginContextValue | null>(null);
|
|
46
|
+
|
|
47
|
+
function getMaxUsagePercent(usage: ProviderUsageData): number | null {
|
|
48
|
+
if (!usage.limits) return null;
|
|
49
|
+
|
|
50
|
+
const items = usage.limits.items ?? [];
|
|
51
|
+
if (items.length > 0) {
|
|
52
|
+
const percents = items.map((item) => item.usedPercent).filter((p): p is number => p !== null);
|
|
53
|
+
return percents.length > 0 ? Math.max(...percents) : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const primary = usage.limits.primary?.usedPercent;
|
|
57
|
+
const secondary = usage.limits.secondary?.usedPercent;
|
|
58
|
+
|
|
59
|
+
if (primary !== null && primary !== undefined && secondary !== null && secondary !== undefined) {
|
|
60
|
+
return Math.max(primary, secondary);
|
|
61
|
+
}
|
|
62
|
+
return primary ?? secondary ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addToHistory(history: UsageSnapshot[], snapshot: UsageSnapshot): UsageSnapshot[] {
|
|
66
|
+
const newHistory = [...history, snapshot];
|
|
67
|
+
if (newHistory.length > HISTORY_SIZE) {
|
|
68
|
+
return newHistory.slice(-HISTORY_SIZE);
|
|
69
|
+
}
|
|
70
|
+
return newHistory;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface PluginProviderProps {
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
cliPlugins?: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function PluginProvider({ children, cliPlugins }: PluginProviderProps) {
|
|
79
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
80
|
+
const [providers, setProviders] = useState<Map<string, ProviderState>>(new Map());
|
|
81
|
+
const [themes, setThemes] = useState<ThemePlugin[]>([]);
|
|
82
|
+
const [notifications, setNotifications] = useState<NotificationPlugin[]>([]);
|
|
83
|
+
const { demoMode, simulator } = useDemoMode();
|
|
84
|
+
const { debug, info, warn, error: logError } = useLogs();
|
|
85
|
+
const { isReady: storageReady, recordProviderSnapshots } = useStorage();
|
|
86
|
+
const { config } = useConfig();
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
async function initialize() {
|
|
90
|
+
installGlobalFetchGuard();
|
|
91
|
+
debug('Initializing plugin registry...', undefined, 'plugins');
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (!demoMode) {
|
|
95
|
+
const initConfig: { plugins?: typeof config.plugins; cliPlugins?: string[] } = {
|
|
96
|
+
plugins: config.plugins,
|
|
97
|
+
};
|
|
98
|
+
if (cliPlugins) {
|
|
99
|
+
initConfig.cliPlugins = cliPlugins;
|
|
100
|
+
}
|
|
101
|
+
await pluginRegistry.initialize(initConfig);
|
|
102
|
+
} else {
|
|
103
|
+
await pluginRegistry.loadBuiltinPlugins();
|
|
104
|
+
}
|
|
105
|
+
info('Plugin registry initialized', undefined, 'plugins');
|
|
106
|
+
} catch (err) {
|
|
107
|
+
logError('Failed to initialize plugin registry', { error: String(err) }, 'plugins');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const providerPlugins = pluginRegistry.getAll('provider');
|
|
111
|
+
const themePlugins = pluginRegistry.getAll('theme');
|
|
112
|
+
const notificationPlugins = pluginRegistry.getAll('notification');
|
|
113
|
+
|
|
114
|
+
initPricingFromPlugins(providerPlugins, {
|
|
115
|
+
'google-gemini': 'google',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
info(`Loaded plugins`, {
|
|
119
|
+
providers: providerPlugins.length,
|
|
120
|
+
themes: themePlugins.length,
|
|
121
|
+
notifications: notificationPlugins.length,
|
|
122
|
+
}, 'plugins');
|
|
123
|
+
|
|
124
|
+
await pluginLifecycle.initializeAll();
|
|
125
|
+
await pluginLifecycle.startAll();
|
|
126
|
+
debug('Plugin lifecycle: all plugins initialized and started', undefined, 'plugins');
|
|
127
|
+
|
|
128
|
+
debug('Discovering credentials...', undefined, 'credentials');
|
|
129
|
+
|
|
130
|
+
const credentials = new Map<string, Credentials>();
|
|
131
|
+
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
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const providerStates = new Map<string, ProviderState>();
|
|
146
|
+
const configuredIds: string[] = [];
|
|
147
|
+
const unconfiguredIds: string[] = [];
|
|
148
|
+
|
|
149
|
+
for (const plugin of providerPlugins) {
|
|
150
|
+
const creds = credentials.get(plugin.id);
|
|
151
|
+
let configured = false;
|
|
152
|
+
if (demoMode) {
|
|
153
|
+
configured = true;
|
|
154
|
+
} else if (creds) {
|
|
155
|
+
const check = safeInvokeSync(plugin.id, 'auth.isConfigured', () => plugin.auth.isConfigured(creds));
|
|
156
|
+
configured = check.ok ? check.value : false;
|
|
157
|
+
}
|
|
158
|
+
providerStates.set(plugin.id, {
|
|
159
|
+
plugin,
|
|
160
|
+
configured,
|
|
161
|
+
usage: null,
|
|
162
|
+
loading: false,
|
|
163
|
+
lastFetchAt: null,
|
|
164
|
+
history: [],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (configured) {
|
|
168
|
+
configuredIds.push(plugin.id);
|
|
169
|
+
} else {
|
|
170
|
+
unconfiguredIds.push(plugin.id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
info('Credential discovery complete', {
|
|
175
|
+
configured: configuredIds,
|
|
176
|
+
unconfigured: unconfiguredIds,
|
|
177
|
+
}, 'credentials');
|
|
178
|
+
|
|
179
|
+
if (demoMode && simulator) {
|
|
180
|
+
const snapshot = simulator.tick();
|
|
181
|
+
for (const [providerId, usage] of snapshot.providerUsage.entries()) {
|
|
182
|
+
const state = providerStates.get(providerId);
|
|
183
|
+
if (!state) continue;
|
|
184
|
+
providerStates.set(providerId, {
|
|
185
|
+
...state,
|
|
186
|
+
usage,
|
|
187
|
+
lastFetchAt: Date.now(),
|
|
188
|
+
history: addToHistory(state.history, {
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
usedPercent: getMaxUsagePercent(usage),
|
|
191
|
+
limitReached: usage.limitReached,
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
notificationBus.registerPlugins(notificationPlugins);
|
|
198
|
+
|
|
199
|
+
setProviders(providerStates);
|
|
200
|
+
setThemes(themePlugins);
|
|
201
|
+
setNotifications(notificationPlugins);
|
|
202
|
+
setIsInitialized(true);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
initialize();
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
const prevPluginConfigRef = useRef<Record<string, Record<string, unknown>>>({});
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!isInitialized) return;
|
|
211
|
+
const prev = prevPluginConfigRef.current;
|
|
212
|
+
const next = config.pluginConfig ?? {};
|
|
213
|
+
const allIds = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
|
214
|
+
|
|
215
|
+
for (const pluginId of allIds) {
|
|
216
|
+
const prevStr = JSON.stringify(prev[pluginId] ?? {});
|
|
217
|
+
const nextStr = JSON.stringify(next[pluginId] ?? {});
|
|
218
|
+
if (prevStr !== nextStr) {
|
|
219
|
+
debug(`Plugin config changed for ${pluginId}`, undefined, 'plugins');
|
|
220
|
+
pluginLifecycle.notifyConfigChange(pluginId, next[pluginId] ?? {});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
prevPluginConfigRef.current = { ...next };
|
|
224
|
+
}, [config.pluginConfig, isInitialized]);
|
|
225
|
+
|
|
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
|
+
}
|
|
232
|
+
|
|
233
|
+
info(`Refreshing ${providerId}...`, undefined, 'refresh');
|
|
234
|
+
|
|
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
|
+
});
|
|
243
|
+
|
|
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
|
+
};
|
|
251
|
+
|
|
252
|
+
if (storageReady) {
|
|
253
|
+
recordProviderSnapshots([
|
|
254
|
+
{
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
provider: providerId,
|
|
257
|
+
usedPercent: getMaxUsagePercent(usage),
|
|
258
|
+
limitReached: usage.limitReached ?? false,
|
|
259
|
+
resetsAt: usage.limits?.primary?.resetsAt ?? null,
|
|
260
|
+
rawJson: JSON.stringify(usage),
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
setProviders((prev) => {
|
|
266
|
+
const next = new Map(prev);
|
|
267
|
+
const current = next.get(providerId);
|
|
268
|
+
const currentHistory = current?.history ?? [];
|
|
269
|
+
const snapshotEntry: UsageSnapshot = {
|
|
270
|
+
timestamp: Date.now(),
|
|
271
|
+
usedPercent: getMaxUsagePercent(usage),
|
|
272
|
+
limitReached: usage.limitReached,
|
|
273
|
+
};
|
|
274
|
+
next.set(providerId, {
|
|
275
|
+
...state,
|
|
276
|
+
usage,
|
|
277
|
+
loading: false,
|
|
278
|
+
lastFetchAt: Date.now(),
|
|
279
|
+
history: addToHistory(currentHistory, snapshotEntry),
|
|
280
|
+
});
|
|
281
|
+
return next;
|
|
282
|
+
});
|
|
283
|
+
|
|
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 ?? [],
|
|
376
|
+
});
|
|
377
|
+
return next;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}, [providers, debug, info, warn, logError, storageReady, recordProviderSnapshots, demoMode, simulator]);
|
|
381
|
+
|
|
382
|
+
const refreshAllProviders = useCallback(async () => {
|
|
383
|
+
const configuredProviders = Array.from(providers.entries())
|
|
384
|
+
.filter(([_, state]) => state.configured)
|
|
385
|
+
.map(([id]) => id);
|
|
386
|
+
|
|
387
|
+
info(`Refreshing ${configuredProviders.length} providers`, { providers: configuredProviders }, 'refresh');
|
|
388
|
+
|
|
389
|
+
await Promise.all(configuredProviders.map(refreshProvider));
|
|
390
|
+
}, [providers, refreshProvider, info]);
|
|
391
|
+
|
|
392
|
+
const value: PluginContextValue = {
|
|
393
|
+
providers,
|
|
394
|
+
themes,
|
|
395
|
+
notifications,
|
|
396
|
+
isInitialized,
|
|
397
|
+
refreshProvider,
|
|
398
|
+
refreshAllProviders,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<PluginContext.Provider value={value}>
|
|
403
|
+
{children}
|
|
404
|
+
</PluginContext.Provider>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function usePlugins(): PluginContextValue {
|
|
409
|
+
const context = useContext(PluginContext);
|
|
410
|
+
if (!context) {
|
|
411
|
+
throw new Error('usePlugins must be used within PluginProvider');
|
|
412
|
+
}
|
|
413
|
+
return context;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function useProvider(providerId: string): ProviderState | undefined {
|
|
417
|
+
const { providers } = usePlugins();
|
|
418
|
+
return providers.get(providerId);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function useConfiguredProviders(): ProviderState[] {
|
|
422
|
+
const { providers } = usePlugins();
|
|
423
|
+
return Array.from(providers.values()).filter((p) => p.configured);
|
|
424
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, useRef, useCallback, type ReactNode } from 'react';
|
|
2
|
+
import { pluginRegistry } from '@/plugins/registry.ts';
|
|
3
|
+
import type { ActivityUpdate } from '@/plugins/types/agent.ts';
|
|
4
|
+
import { createPluginContext } from '@/plugins/plugin-context-factory.ts';
|
|
5
|
+
import { usePlugins } from './PluginContext.tsx';
|
|
6
|
+
import { useLogs } from './LogContext.tsx';
|
|
7
|
+
import { useDemoMode } from './DemoModeContext.tsx';
|
|
8
|
+
|
|
9
|
+
type ActivityListener = (delta: number, timestamp: number) => void;
|
|
10
|
+
|
|
11
|
+
interface RealTimeActivityContextValue {
|
|
12
|
+
lastActivityAt: number | null;
|
|
13
|
+
isWatching: boolean;
|
|
14
|
+
subscribe: (listener: ActivityListener) => () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const RealTimeActivityContext = createContext<RealTimeActivityContextValue | null>(null);
|
|
18
|
+
|
|
19
|
+
export function RealTimeActivityProvider({ children }: { children: ReactNode }) {
|
|
20
|
+
const [lastActivityAt, setLastActivityAt] = useState<number | null>(null);
|
|
21
|
+
const [isWatching, setIsWatching] = useState(false);
|
|
22
|
+
const { isInitialized: pluginsInitialized } = usePlugins();
|
|
23
|
+
const { debug, info } = useLogs();
|
|
24
|
+
const { demoMode } = useDemoMode();
|
|
25
|
+
|
|
26
|
+
const lastSeenRef = useRef<Map<string, number>>(new Map());
|
|
27
|
+
const listenersRef = useRef<Set<ActivityListener>>(new Set());
|
|
28
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
29
|
+
|
|
30
|
+
const subscribe = useCallback((listener: ActivityListener) => {
|
|
31
|
+
listenersRef.current.add(listener);
|
|
32
|
+
return () => {
|
|
33
|
+
listenersRef.current.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const handleActivityUpdate = useCallback((update: ActivityUpdate) => {
|
|
38
|
+
const key = `${update.sessionId}:${update.messageId}`;
|
|
39
|
+
const prevTokens = lastSeenRef.current.get(key) ?? 0;
|
|
40
|
+
const newTokens = update.tokens.input + update.tokens.output + (update.tokens.reasoning ?? 0);
|
|
41
|
+
const delta = Math.max(0, newTokens - prevTokens);
|
|
42
|
+
|
|
43
|
+
if (delta > 0) {
|
|
44
|
+
lastSeenRef.current.set(key, newTokens);
|
|
45
|
+
setLastActivityAt(update.timestamp);
|
|
46
|
+
debug(`Real-time activity: +${delta} tokens`, { sessionId: update.sessionId }, 'realtime');
|
|
47
|
+
|
|
48
|
+
for (const listener of listenersRef.current) {
|
|
49
|
+
listener(delta, update.timestamp);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}, [debug]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (demoMode || !pluginsInitialized) return;
|
|
56
|
+
|
|
57
|
+
const agentPlugins = pluginRegistry.getAll('agent');
|
|
58
|
+
const cleanups: (() => void)[] = [];
|
|
59
|
+
|
|
60
|
+
for (const plugin of agentPlugins) {
|
|
61
|
+
if (plugin.capabilities.realTimeTracking && plugin.startActivityWatch) {
|
|
62
|
+
const ctx = createPluginContext(plugin.id, plugin.permissions);
|
|
63
|
+
info(`Starting real-time activity watch for ${plugin.id}`, undefined, 'realtime');
|
|
64
|
+
plugin.startActivityWatch(ctx, handleActivityUpdate);
|
|
65
|
+
|
|
66
|
+
if (plugin.stopActivityWatch) {
|
|
67
|
+
const stopFn = plugin.stopActivityWatch.bind(plugin, ctx);
|
|
68
|
+
cleanups.push(stopFn);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (cleanups.length > 0) {
|
|
74
|
+
setIsWatching(true);
|
|
75
|
+
cleanupRef.current = () => {
|
|
76
|
+
cleanups.forEach(fn => fn());
|
|
77
|
+
setIsWatching(false);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
if (cleanupRef.current) {
|
|
83
|
+
cleanupRef.current();
|
|
84
|
+
cleanupRef.current = null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}, [pluginsInitialized, demoMode, handleActivityUpdate, info]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const interval = setInterval(() => {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
if (lastActivityAt && now - lastActivityAt > 60000) {
|
|
93
|
+
lastSeenRef.current.clear();
|
|
94
|
+
}
|
|
95
|
+
}, 30000);
|
|
96
|
+
|
|
97
|
+
return () => clearInterval(interval);
|
|
98
|
+
}, [lastActivityAt]);
|
|
99
|
+
|
|
100
|
+
const value: RealTimeActivityContextValue = {
|
|
101
|
+
lastActivityAt,
|
|
102
|
+
isWatching,
|
|
103
|
+
subscribe,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<RealTimeActivityContext.Provider value={value}>
|
|
108
|
+
{children}
|
|
109
|
+
</RealTimeActivityContext.Provider>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function useRealTimeActivity(): RealTimeActivityContextValue {
|
|
114
|
+
const context = useContext(RealTimeActivityContext);
|
|
115
|
+
if (!context) {
|
|
116
|
+
throw new Error('useRealTimeActivity must be used within RealTimeActivityProvider');
|
|
117
|
+
}
|
|
118
|
+
return context;
|
|
119
|
+
}
|