@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,186 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useColors } from '../contexts/ThemeContext.tsx';
|
|
3
|
+
import { usePulse } from '../hooks/usePulse.ts';
|
|
4
|
+
|
|
5
|
+
interface LimitGaugeProps {
|
|
6
|
+
label: string;
|
|
7
|
+
usedPercent: number | null;
|
|
8
|
+
color: string;
|
|
9
|
+
ghost?: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
resetTime?: string;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
labelWidth?: number;
|
|
14
|
+
barWidth?: number;
|
|
15
|
+
selected?: boolean;
|
|
16
|
+
warningThreshold?: number;
|
|
17
|
+
criticalThreshold?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const FRACTIONAL_BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
|
|
21
|
+
|
|
22
|
+
function renderFractionalBar(percent: number, width: number, filledColor: string, emptyColor: string): ReactNode[] {
|
|
23
|
+
const totalSegments = width * 8;
|
|
24
|
+
const filledSegments = Math.round((percent / 100) * totalSegments);
|
|
25
|
+
const fullBlocks = Math.floor(filledSegments / 8);
|
|
26
|
+
const remainder = filledSegments % 8;
|
|
27
|
+
const emptyBlocks = width - fullBlocks - (remainder > 0 ? 1 : 0);
|
|
28
|
+
|
|
29
|
+
const elements: ReactNode[] = [];
|
|
30
|
+
|
|
31
|
+
if (fullBlocks > 0) {
|
|
32
|
+
elements.push(<span key="full" fg={filledColor}>{'█'.repeat(fullBlocks)}</span>);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (remainder > 0) {
|
|
36
|
+
elements.push(<span key="partial" fg={filledColor}>{FRACTIONAL_BLOCKS[remainder]}</span>);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (emptyBlocks > 0) {
|
|
40
|
+
elements.push(<span key="empty" fg={emptyColor}>{'─'.repeat(emptyBlocks)}</span>);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return elements;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function interpolatePulseColor(intensity: number, baseColor: string, dimColor: string): string {
|
|
47
|
+
const dimAmount = 0.4;
|
|
48
|
+
const t = intensity * dimAmount;
|
|
49
|
+
|
|
50
|
+
const parseHex = (hex: string) => {
|
|
51
|
+
const h = hex.replace('#', '');
|
|
52
|
+
return {
|
|
53
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
54
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
55
|
+
b: parseInt(h.substring(4, 6), 16),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const base = parseHex(baseColor);
|
|
60
|
+
const dim = parseHex(dimColor);
|
|
61
|
+
|
|
62
|
+
const r = Math.round(base.r + (dim.r - base.r) * t);
|
|
63
|
+
const g = Math.round(base.g + (dim.g - base.g) * t);
|
|
64
|
+
const b = Math.round(base.b + (dim.b - base.b) * t);
|
|
65
|
+
|
|
66
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function LimitGauge({
|
|
70
|
+
label,
|
|
71
|
+
usedPercent,
|
|
72
|
+
color,
|
|
73
|
+
ghost = false,
|
|
74
|
+
error,
|
|
75
|
+
resetTime,
|
|
76
|
+
compact = false,
|
|
77
|
+
labelWidth = 12,
|
|
78
|
+
barWidth = 10,
|
|
79
|
+
selected = false,
|
|
80
|
+
warningThreshold = 90,
|
|
81
|
+
criticalThreshold = 95,
|
|
82
|
+
}: LimitGaugeProps) {
|
|
83
|
+
const colors = useColors();
|
|
84
|
+
|
|
85
|
+
const percent = usedPercent ?? 0;
|
|
86
|
+
const isCritical = percent >= criticalThreshold;
|
|
87
|
+
const isWarning = percent >= warningThreshold && percent < criticalThreshold;
|
|
88
|
+
|
|
89
|
+
// Critical: fast, intense pulse (80ms interval)
|
|
90
|
+
// Warning: moderate pulse (200ms interval) to draw attention
|
|
91
|
+
const criticalPulseStep = usePulse({ enabled: isCritical, intervalMs: 80 });
|
|
92
|
+
const warningPulseStep = usePulse({ enabled: isWarning, intervalMs: 200 });
|
|
93
|
+
|
|
94
|
+
const pulseIntensity = isCritical
|
|
95
|
+
? Math.abs(Math.sin(criticalPulseStep * 0.3))
|
|
96
|
+
: isWarning
|
|
97
|
+
? Math.abs(Math.sin(warningPulseStep * 0.25))
|
|
98
|
+
: 0;
|
|
99
|
+
|
|
100
|
+
const truncateLabel = (lbl: string, maxLen: number): string => {
|
|
101
|
+
if (lbl.length <= maxLen) return lbl.padEnd(maxLen);
|
|
102
|
+
return lbl.slice(0, maxLen - 1) + '…';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (compact) {
|
|
106
|
+
const shortLabel = truncateLabel(label, 10);
|
|
107
|
+
const percentStr = usedPercent !== null ? `${Math.round(percent)}%` : '--';
|
|
108
|
+
const statusIcon = isCritical ? '!!' : isWarning ? '!' : '';
|
|
109
|
+
const statusColor = isCritical ? colors.error : isWarning ? color : colors.textMuted;
|
|
110
|
+
const textColor = isCritical ? colors.error : isWarning ? color : colors.text;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<text height={1}>
|
|
114
|
+
{selected && <span fg={colors.primary}>▌</span>}
|
|
115
|
+
<span fg={selected ? colors.text : textColor}>{shortLabel}</span>
|
|
116
|
+
<span fg={statusColor}> {percentStr}</span>
|
|
117
|
+
{statusIcon && <span fg={statusColor}> {statusIcon}</span>}
|
|
118
|
+
</text>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (ghost) {
|
|
123
|
+
const ghostLabel = truncateLabel(label, labelWidth);
|
|
124
|
+
return (
|
|
125
|
+
<box height={1} overflow="hidden">
|
|
126
|
+
<text height={1}>
|
|
127
|
+
<span fg={colors.textSubtle}>○ </span>
|
|
128
|
+
<span fg={colors.textSubtle}>{ghostLabel} </span>
|
|
129
|
+
<span fg={colors.textSubtle}>{'─'.repeat(barWidth)}</span>
|
|
130
|
+
<span fg={colors.textSubtle}> --</span>
|
|
131
|
+
</text>
|
|
132
|
+
</box>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (error) {
|
|
137
|
+
const displayLabel = truncateLabel(label, labelWidth);
|
|
138
|
+
const errorIcon = selected ? '▌' : '✗';
|
|
139
|
+
return (
|
|
140
|
+
<box height={1} overflow="hidden" {...(selected ? { backgroundColor: colors.borderMuted } : {})}>
|
|
141
|
+
<text height={1}>
|
|
142
|
+
<span fg={selected ? colors.primary : colors.error}>{errorIcon} </span>
|
|
143
|
+
<span fg={selected ? colors.text : colors.text}>{displayLabel} </span>
|
|
144
|
+
<span fg={colors.error}>{'─'.repeat(barWidth)}</span>
|
|
145
|
+
<span fg={colors.error}> ERR</span>
|
|
146
|
+
</text>
|
|
147
|
+
</box>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const barColor = isCritical
|
|
152
|
+
? interpolatePulseColor(pulseIntensity, colors.error, colors.background)
|
|
153
|
+
: isWarning
|
|
154
|
+
? interpolatePulseColor(pulseIntensity, color, colors.background)
|
|
155
|
+
: color;
|
|
156
|
+
|
|
157
|
+
const statusIcon = isCritical ? '!!' : isWarning ? '!' : '●';
|
|
158
|
+
const statusColor = isCritical ? colors.error : isWarning ? color : colors.success;
|
|
159
|
+
|
|
160
|
+
const displayLabel = truncateLabel(label, labelWidth);
|
|
161
|
+
const percentStr = usedPercent !== null ? `${Math.round(percent)}%`.padStart(4) : ' --';
|
|
162
|
+
|
|
163
|
+
const percentDisplay = isCritical ? (
|
|
164
|
+
<span fg={colors.background} bg={colors.error}>{percentStr}</span>
|
|
165
|
+
) : (
|
|
166
|
+
<span fg={isWarning ? color : colors.textMuted}>{percentStr}</span>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const resetDisplay = resetTime ? (
|
|
170
|
+
<span fg={colors.textSubtle}> {resetTime}</span>
|
|
171
|
+
) : null;
|
|
172
|
+
|
|
173
|
+
const selectionIndicator = selected ? '▌' : statusIcon;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<box height={1} overflow="hidden" {...(selected ? { backgroundColor: colors.borderMuted } : {})}>
|
|
177
|
+
<text height={1}>
|
|
178
|
+
<span fg={selected ? colors.primary : statusColor}>{selectionIndicator} </span>
|
|
179
|
+
<span fg={selected ? colors.text : (isCritical ? colors.error : isWarning ? color : colors.textMuted)}>{displayLabel} </span>
|
|
180
|
+
{renderFractionalBar(percent, barWidth, barColor, colors.border)}
|
|
181
|
+
{percentDisplay}
|
|
182
|
+
{resetDisplay}
|
|
183
|
+
</text>
|
|
184
|
+
</box>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { RGBA } from '@opentui/core';
|
|
3
|
+
|
|
4
|
+
/** Standardized z-index tiers for overlay layering */
|
|
5
|
+
export const Z_INDEX = {
|
|
6
|
+
HUD: 10,
|
|
7
|
+
MODAL: 50,
|
|
8
|
+
MODAL_HIGH: 100,
|
|
9
|
+
TOAST: 200,
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
/** 50% black overlay for modal dimming */
|
|
13
|
+
export const OVERLAY_BG = RGBA.fromValues(0.0, 0.0, 0.0, 0.5);
|
|
14
|
+
|
|
15
|
+
interface ModalBackdropProps {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
zIndex?: number;
|
|
18
|
+
onBackdropClick?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ModalBackdrop({
|
|
22
|
+
children,
|
|
23
|
+
zIndex = Z_INDEX.MODAL,
|
|
24
|
+
onBackdropClick,
|
|
25
|
+
}: ModalBackdropProps) {
|
|
26
|
+
return (
|
|
27
|
+
<box
|
|
28
|
+
position="absolute"
|
|
29
|
+
left={0}
|
|
30
|
+
top={0}
|
|
31
|
+
width="100%"
|
|
32
|
+
height="100%"
|
|
33
|
+
justifyContent="center"
|
|
34
|
+
alignItems="center"
|
|
35
|
+
zIndex={zIndex}
|
|
36
|
+
backgroundColor={OVERLAY_BG}
|
|
37
|
+
{...(onBackdropClick ? { onClick: onBackdropClick } : {})}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useTerminalDimensions } from '@opentui/react';
|
|
3
|
+
import { useColors } from '../contexts/ThemeContext.tsx';
|
|
4
|
+
import type { ProviderState } from '../contexts/PluginContext.tsx';
|
|
5
|
+
|
|
6
|
+
function pad(str: string, len: number): string {
|
|
7
|
+
return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatCost(val: number): string {
|
|
11
|
+
if (val >= 1000) return `$${(val / 1000).toFixed(1)}k`;
|
|
12
|
+
if (val >= 100) return `$${Math.round(val)}`;
|
|
13
|
+
if (val >= 10) return `$${val.toFixed(1)}`;
|
|
14
|
+
return `$${val.toFixed(2)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatTokens(val: number): string {
|
|
18
|
+
if (val >= 1_000_000_000) return `${(val / 1_000_000_000).toFixed(1)}B`;
|
|
19
|
+
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`;
|
|
20
|
+
if (val >= 1_000) return `${(val / 1_000).toFixed(1)}K`;
|
|
21
|
+
return String(Math.round(val));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProviderAggregates {
|
|
25
|
+
okCount: number;
|
|
26
|
+
warnCount: number;
|
|
27
|
+
errCount: number;
|
|
28
|
+
costToday: number;
|
|
29
|
+
costTodayEstimated: boolean;
|
|
30
|
+
costMtd: number;
|
|
31
|
+
costMtdEstimated: boolean;
|
|
32
|
+
totalTokens24h: number;
|
|
33
|
+
tokens24hEstimated: boolean;
|
|
34
|
+
hottestProvider: string | null;
|
|
35
|
+
hottestPercent: number;
|
|
36
|
+
trendVsYesterday: number | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function computeAggregates(providers: ProviderState[]): ProviderAggregates {
|
|
40
|
+
let okCount = 0;
|
|
41
|
+
let warnCount = 0;
|
|
42
|
+
let errCount = 0;
|
|
43
|
+
let costToday = 0;
|
|
44
|
+
let costMtd = 0;
|
|
45
|
+
let costTodayEstimated = false;
|
|
46
|
+
let costMtdEstimated = false;
|
|
47
|
+
let totalTokens24h = 0;
|
|
48
|
+
let tokens24hEstimated = false;
|
|
49
|
+
let hottestProvider: string | null = null;
|
|
50
|
+
let hottestPercent = 0;
|
|
51
|
+
|
|
52
|
+
for (const p of providers) {
|
|
53
|
+
if (!p.configured) continue;
|
|
54
|
+
|
|
55
|
+
if (p.usage?.error) {
|
|
56
|
+
errCount++;
|
|
57
|
+
} else if (p.usage?.limitReached || getMaxUsage(p) >= 80) {
|
|
58
|
+
warnCount++;
|
|
59
|
+
} else {
|
|
60
|
+
okCount++;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (p.usage?.cost) {
|
|
64
|
+
const cost = p.usage.cost;
|
|
65
|
+
const todayTotal = cost.actual?.total ?? cost.estimated?.total ?? 0;
|
|
66
|
+
costToday += todayTotal;
|
|
67
|
+
costMtd += todayTotal;
|
|
68
|
+
if (cost.source === 'estimated') {
|
|
69
|
+
costTodayEstimated = true;
|
|
70
|
+
costMtdEstimated = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (p.usage?.tokens) {
|
|
75
|
+
const t = p.usage.tokens;
|
|
76
|
+
totalTokens24h += t.input + t.output + (t.cacheRead ?? 0) + (t.cacheWrite ?? 0);
|
|
77
|
+
tokens24hEstimated = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const maxUsage = getMaxUsage(p);
|
|
81
|
+
if (maxUsage > hottestPercent) {
|
|
82
|
+
hottestPercent = maxUsage;
|
|
83
|
+
hottestProvider = p.plugin.name;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
okCount,
|
|
89
|
+
warnCount,
|
|
90
|
+
errCount,
|
|
91
|
+
costToday,
|
|
92
|
+
costTodayEstimated,
|
|
93
|
+
costMtd,
|
|
94
|
+
costMtdEstimated,
|
|
95
|
+
totalTokens24h,
|
|
96
|
+
tokens24hEstimated,
|
|
97
|
+
hottestProvider,
|
|
98
|
+
hottestPercent,
|
|
99
|
+
trendVsYesterday: null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getMaxUsage(state: ProviderState): number {
|
|
104
|
+
if (!state.usage?.limits) return 0;
|
|
105
|
+
const items = state.usage.limits.items ?? [];
|
|
106
|
+
if (items.length > 0) {
|
|
107
|
+
return Math.max(...items.map((item) => item.usedPercent ?? 0));
|
|
108
|
+
}
|
|
109
|
+
const primary = state.usage.limits.primary?.usedPercent || 0;
|
|
110
|
+
const secondary = state.usage.limits.secondary?.usedPercent || 0;
|
|
111
|
+
return Math.max(primary, secondary);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface ProviderAggregateStripProps {
|
|
115
|
+
providers: ProviderState[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function ProviderAggregateStrip({ providers }: ProviderAggregateStripProps) {
|
|
119
|
+
const colors = useColors();
|
|
120
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
121
|
+
const isCompact = termWidth < 100;
|
|
122
|
+
|
|
123
|
+
const agg = useMemo(() => computeAggregates(providers), [providers]);
|
|
124
|
+
|
|
125
|
+
const costTodayStr = `${agg.costTodayEstimated ? '~' : ''}${formatCost(agg.costToday)}`;
|
|
126
|
+
const costMtdStr = `${agg.costMtdEstimated ? '~' : ''}${formatCost(agg.costMtd)}`;
|
|
127
|
+
const tokensStr = `${agg.tokens24hEstimated ? '~' : ''}${formatTokens(agg.totalTokens24h)}`;
|
|
128
|
+
|
|
129
|
+
const trendStr = agg.trendVsYesterday !== null
|
|
130
|
+
? (agg.trendVsYesterday >= 0 ? `+${agg.trendVsYesterday.toFixed(0)}%` : `${agg.trendVsYesterday.toFixed(0)}%`)
|
|
131
|
+
: '—';
|
|
132
|
+
const trendColor = agg.trendVsYesterday !== null
|
|
133
|
+
? (agg.trendVsYesterday > 10 ? colors.warning : agg.trendVsYesterday < -10 ? colors.success : colors.textMuted)
|
|
134
|
+
: colors.textSubtle;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<box flexDirection="column" flexShrink={0}>
|
|
138
|
+
<box flexDirection="row" height={1} gap={2} paddingX={1}>
|
|
139
|
+
<text height={1}>
|
|
140
|
+
<span fg={colors.textMuted}>HEALTH </span>
|
|
141
|
+
{agg.okCount > 0 && <span fg={colors.success}>{pad(`${agg.okCount} ok`, isCompact ? 5 : 5)}</span>}
|
|
142
|
+
{agg.warnCount > 0 && <span fg={colors.warning}> {pad(`${agg.warnCount} warn`, isCompact ? 7 : 7)}</span>}
|
|
143
|
+
{agg.errCount > 0 && <span fg={colors.error}> {pad(`${agg.errCount} err`, isCompact ? 6 : 6)}</span>}
|
|
144
|
+
{agg.okCount === 0 && agg.warnCount === 0 && agg.errCount === 0 && (
|
|
145
|
+
<span fg={colors.textSubtle}>no providers</span>
|
|
146
|
+
)}
|
|
147
|
+
</text>
|
|
148
|
+
|
|
149
|
+
<text fg={colors.textSubtle} height={1}>│</text>
|
|
150
|
+
|
|
151
|
+
<text height={1}>
|
|
152
|
+
<span fg={colors.textMuted}>TODAY </span>
|
|
153
|
+
<span fg={agg.costTodayEstimated ? colors.textMuted : colors.success}>{costTodayStr}</span>
|
|
154
|
+
</text>
|
|
155
|
+
|
|
156
|
+
{!isCompact && (
|
|
157
|
+
<>
|
|
158
|
+
<text fg={colors.textSubtle} height={1}>│</text>
|
|
159
|
+
<text height={1}>
|
|
160
|
+
<span fg={colors.textMuted}>MTD </span>
|
|
161
|
+
<span fg={agg.costMtdEstimated ? colors.textMuted : colors.success}>{costMtdStr}</span>
|
|
162
|
+
</text>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<text fg={colors.textSubtle} height={1}>│</text>
|
|
167
|
+
|
|
168
|
+
<text height={1}>
|
|
169
|
+
<span fg={colors.textMuted}>TOKENS </span>
|
|
170
|
+
<span fg={agg.tokens24hEstimated ? colors.textMuted : colors.primary}>{tokensStr}</span>
|
|
171
|
+
</text>
|
|
172
|
+
</box>
|
|
173
|
+
|
|
174
|
+
<box flexDirection="row" height={1} gap={2} paddingX={1}>
|
|
175
|
+
{agg.hottestProvider && (
|
|
176
|
+
<text height={1}>
|
|
177
|
+
<span fg={colors.textMuted}>HOTTEST </span>
|
|
178
|
+
<span fg={agg.hottestPercent >= 80 ? colors.warning : colors.text}>
|
|
179
|
+
{pad(agg.hottestProvider, isCompact ? 10 : 14)}
|
|
180
|
+
</span>
|
|
181
|
+
<span fg={agg.hottestPercent >= 80 ? colors.warning : colors.textMuted}>
|
|
182
|
+
{` ${Math.round(agg.hottestPercent)}%`}
|
|
183
|
+
</span>
|
|
184
|
+
</text>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
<text fg={colors.textSubtle} height={1}>│</text>
|
|
188
|
+
|
|
189
|
+
<text height={1}>
|
|
190
|
+
<span fg={colors.textMuted}>vs YESTERDAY </span>
|
|
191
|
+
<span fg={trendColor}>{trendStr}</span>
|
|
192
|
+
</text>
|
|
193
|
+
</box>
|
|
194
|
+
|
|
195
|
+
<box height={1} overflow="hidden">
|
|
196
|
+
<text fg={colors.border}>{'─'.repeat(300)}</text>
|
|
197
|
+
</box>
|
|
198
|
+
</box>
|
|
199
|
+
);
|
|
200
|
+
}
|