@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.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +42 -38
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +36 -23
- package/src/plugins/notifications/visual-flash.ts +31 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +68 -57
- package/src/plugins/registry.ts +80 -71
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/persistence-service.ts +206 -0
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/retention.ts +146 -0
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +314 -183
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +320 -169
- package/src/tui/components/SettingsModal.tsx +593 -269
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +164 -151
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +238 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +95 -149
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +7 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +164 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +495 -243
- package/src/tui/views/RealTimeDashboard.tsx +176 -116
- package/src/tui/views/SettingsView.tsx +225 -189
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { useTerminalDimensions } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
2
|
+
import { useConfig } from "../contexts/ConfigContext.tsx";
|
|
3
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
4
|
+
import { type TimeWindow, useTimeWindow } from "../contexts/TimeWindowContext.tsx";
|
|
5
|
+
import { useAnimatedValue } from "../hooks/useAnimatedValue.ts";
|
|
6
|
+
import { interpolateColor, useValueFlash } from "../hooks/useValueFlash.ts";
|
|
7
|
+
import { Sparkline } from "./Sparkline.tsx";
|
|
8
8
|
|
|
9
|
-
type MetricType =
|
|
9
|
+
type MetricType = "cost" | "tokens" | "requests" | "rate" | "default";
|
|
10
10
|
|
|
11
11
|
interface KPICardProps {
|
|
12
12
|
title: string;
|
|
@@ -18,13 +18,13 @@ interface KPICardProps {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const TIME_WINDOW_EXPANDED: Record<TimeWindow, string> = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
"5m": "Last 5 Minutes",
|
|
22
|
+
"15m": "Last 15 Minutes",
|
|
23
|
+
"1h": "Last Hour",
|
|
24
|
+
"24h": "Last 24 Hours",
|
|
25
|
+
"7d": "Last 7 Days",
|
|
26
|
+
"30d": "Last 30 Days",
|
|
27
|
+
all: "All Time",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
interface TimeWindowCardProps {
|
|
@@ -35,56 +35,75 @@ function TimeWindowCard({ isCompact }: TimeWindowCardProps) {
|
|
|
35
35
|
const colors = useColors();
|
|
36
36
|
const { window: timeWindow } = useTimeWindow();
|
|
37
37
|
|
|
38
|
-
const title = isCompact ?
|
|
38
|
+
const title = isCompact ? "WINDOW" : "TIME WINDOW (t)";
|
|
39
39
|
const value = isCompact ? timeWindow.toUpperCase() : TIME_WINDOW_EXPANDED[timeWindow];
|
|
40
|
-
const hint = isCompact ?
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<text fg={colors.
|
|
51
|
-
|
|
40
|
+
const hint = isCompact ? "(t)" : "Press 't' to change";
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<box flexDirection="column" paddingX={1} flexShrink={0} width={isCompact ? 9 : 22}>
|
|
44
|
+
<text fg={colors.textMuted} height={1}>
|
|
45
|
+
{title}
|
|
46
|
+
</text>
|
|
47
|
+
<text fg={colors.info} height={1}>
|
|
48
|
+
<strong>{value}</strong>
|
|
49
|
+
</text>
|
|
50
|
+
<text fg={colors.textSubtle} height={1}>
|
|
51
|
+
{hint}
|
|
52
|
+
</text>
|
|
52
53
|
</box>
|
|
53
54
|
);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
function KPICard({
|
|
57
|
+
function KPICard({
|
|
58
|
+
title,
|
|
59
|
+
value,
|
|
60
|
+
delta,
|
|
61
|
+
subValue,
|
|
62
|
+
metric = "default",
|
|
63
|
+
flashIntensity = 0,
|
|
64
|
+
}: KPICardProps) {
|
|
57
65
|
const colors = useColors();
|
|
58
|
-
|
|
66
|
+
|
|
59
67
|
const getBaseMetricColor = (m: MetricType): string => {
|
|
60
68
|
switch (m) {
|
|
61
|
-
case
|
|
62
|
-
|
|
63
|
-
case
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
case "cost":
|
|
70
|
+
return colors.success;
|
|
71
|
+
case "tokens":
|
|
72
|
+
return colors.primary;
|
|
73
|
+
case "requests":
|
|
74
|
+
return colors.secondary;
|
|
75
|
+
case "rate":
|
|
76
|
+
return colors.warning;
|
|
77
|
+
default:
|
|
78
|
+
return colors.text;
|
|
66
79
|
}
|
|
67
80
|
};
|
|
68
|
-
|
|
81
|
+
|
|
69
82
|
const baseColor = getBaseMetricColor(metric);
|
|
70
|
-
const flashHighlight =
|
|
71
|
-
const valueColor =
|
|
72
|
-
? interpolateColor(flashIntensity, baseColor, flashHighlight)
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
const flashHighlight = "#ffffff";
|
|
84
|
+
const valueColor =
|
|
85
|
+
flashIntensity > 0 ? interpolateColor(flashIntensity, baseColor, flashHighlight) : baseColor;
|
|
86
|
+
|
|
75
87
|
const deltaColor = colors.success;
|
|
76
|
-
|
|
88
|
+
|
|
77
89
|
return (
|
|
78
|
-
<box
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
<box flexDirection="column" paddingLeft={1} paddingRight={2} flexGrow={1}>
|
|
91
|
+
<text fg={colors.textMuted} height={1}>
|
|
92
|
+
{title}
|
|
93
|
+
</text>
|
|
94
|
+
<text fg={valueColor} height={1}>
|
|
95
|
+
<strong>{value}</strong>
|
|
96
|
+
</text>
|
|
97
|
+
{delta && (
|
|
98
|
+
<text fg={deltaColor} height={1}>
|
|
99
|
+
{delta}
|
|
100
|
+
</text>
|
|
101
|
+
)}
|
|
102
|
+
{subValue && (
|
|
103
|
+
<text fg={colors.textMuted} height={1}>
|
|
104
|
+
{subValue}
|
|
105
|
+
</text>
|
|
106
|
+
)}
|
|
88
107
|
</box>
|
|
89
108
|
);
|
|
90
109
|
}
|
|
@@ -97,7 +116,7 @@ export interface ActivityStatus {
|
|
|
97
116
|
export interface BudgetInfo {
|
|
98
117
|
limit: number | null;
|
|
99
118
|
budgetCost: number;
|
|
100
|
-
budgetType:
|
|
119
|
+
budgetType: "daily" | "weekly" | "monthly" | "none";
|
|
101
120
|
budgetTypeLabel: string;
|
|
102
121
|
warningPercent: number;
|
|
103
122
|
criticalPercent: number;
|
|
@@ -131,51 +150,54 @@ export function KpiStrip({
|
|
|
131
150
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
132
151
|
const { config } = useConfig();
|
|
133
152
|
const { sparkline: sparklineConfig } = config.display;
|
|
134
|
-
|
|
153
|
+
|
|
135
154
|
const animatedCost = useAnimatedValue(totalCost, { durationMs: 400, precision: 2 });
|
|
136
155
|
const animatedTokens = useAnimatedValue(totalTokens, { durationMs: 400, precision: 0 });
|
|
137
156
|
const animatedRequests = useAnimatedValue(totalRequests, { durationMs: 300, precision: 0 });
|
|
138
|
-
|
|
139
|
-
const { intensity: costFlashIntensity } = useValueFlash(totalCost, {
|
|
140
|
-
durationMs: 500,
|
|
157
|
+
|
|
158
|
+
const { intensity: costFlashIntensity } = useValueFlash(totalCost, {
|
|
159
|
+
durationMs: 500,
|
|
141
160
|
increaseOnly: true,
|
|
142
|
-
threshold: 0.001
|
|
161
|
+
threshold: 0.001,
|
|
143
162
|
});
|
|
144
|
-
const { intensity: tokenFlashIntensity } = useValueFlash(totalTokens, {
|
|
145
|
-
durationMs: 400,
|
|
163
|
+
const { intensity: tokenFlashIntensity } = useValueFlash(totalTokens, {
|
|
164
|
+
durationMs: 400,
|
|
146
165
|
increaseOnly: true,
|
|
147
|
-
threshold: 10
|
|
166
|
+
threshold: 10,
|
|
148
167
|
});
|
|
149
|
-
|
|
168
|
+
|
|
150
169
|
const formatCurrency = (val: number) => `$${val.toFixed(2)}`;
|
|
151
|
-
const formatTokens = (val: number) =>
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
170
|
+
const formatTokens = (val: number) =>
|
|
171
|
+
val > 1000000 ? `${(val / 1000000).toFixed(1)}M` : `${(val / 1000).toFixed(1)}K`;
|
|
172
|
+
const formatRate = (val: number) =>
|
|
173
|
+
val >= 1000 ? `${(val / 1000).toFixed(1)}k` : `${Math.round(val)}`;
|
|
174
|
+
const formatBurnTokens = (val: number) =>
|
|
175
|
+
val >= 1000 ? `${(val / 1000).toFixed(1)}k` : `${Math.round(val)}`;
|
|
176
|
+
|
|
155
177
|
const formatWindowLabel = (sec: number): string => {
|
|
156
178
|
if (sec < 60) return `${Math.round(sec)}s`;
|
|
157
179
|
if (sec < 3600) return `${Math.round(sec / 60)}m`;
|
|
158
180
|
return `${(sec / 3600).toFixed(1)}h`;
|
|
159
181
|
};
|
|
160
|
-
|
|
182
|
+
|
|
161
183
|
const hasEnoughHistory = windowSec >= 30;
|
|
162
184
|
const burnRateCostPerHour = hasEnoughHistory ? deltaCost * (3600 / windowSec) : 0;
|
|
163
185
|
const burnRateTokensPerMin = hasEnoughHistory ? deltaTokens * (60 / windowSec) : 0;
|
|
164
186
|
const windowLabel = formatWindowLabel(windowSec);
|
|
165
|
-
|
|
187
|
+
|
|
166
188
|
const getActivityStatus = (): ActivityStatus => {
|
|
167
189
|
const { instantRate, avgRate, isSpike } = activity;
|
|
168
|
-
if (isSpike || instantRate >= 500) return { label:
|
|
169
|
-
if (avgRate >= 150) return { label:
|
|
170
|
-
if (avgRate >= 50) return { label:
|
|
171
|
-
if (avgRate >= 10) return { label:
|
|
172
|
-
return { label:
|
|
190
|
+
if (isSpike || instantRate >= 500) return { label: "SPIKE", color: colors.error };
|
|
191
|
+
if (avgRate >= 150) return { label: "HOT", color: colors.warning };
|
|
192
|
+
if (avgRate >= 50) return { label: "BUSY", color: colors.success };
|
|
193
|
+
if (avgRate >= 10) return { label: "LOW", color: colors.textMuted };
|
|
194
|
+
return { label: "IDLE", color: colors.textSubtle };
|
|
173
195
|
};
|
|
174
196
|
|
|
175
197
|
const activityStatus = getActivityStatus();
|
|
176
|
-
|
|
198
|
+
|
|
177
199
|
const isCompact = terminalWidth < 100;
|
|
178
|
-
const sparklineWidth = isCompact
|
|
200
|
+
const sparklineWidth = isCompact
|
|
179
201
|
? Math.min(25, Math.max(15, Math.floor(terminalWidth * 0.2)))
|
|
180
202
|
: Math.min(50, Math.max(20, Math.floor(terminalWidth * 0.25)));
|
|
181
203
|
|
|
@@ -188,36 +210,44 @@ export function KpiStrip({
|
|
|
188
210
|
<>
|
|
189
211
|
<box flexDirection="row" gap={0} height={4} flexShrink={0}>
|
|
190
212
|
<TimeWindowCard isCompact={isCompact} />
|
|
191
|
-
<KPICard
|
|
192
|
-
title="COST"
|
|
193
|
-
value={formatCurrency(animatedCost)}
|
|
194
|
-
delta={
|
|
213
|
+
<KPICard
|
|
214
|
+
title="COST"
|
|
215
|
+
value={formatCurrency(animatedCost)}
|
|
216
|
+
delta={
|
|
217
|
+
hasEnoughHistory ? `+${formatCurrency(deltaCost)} (${windowLabel})` : "gathering..."
|
|
218
|
+
}
|
|
195
219
|
metric="cost"
|
|
196
220
|
flashIntensity={costFlashIntensity}
|
|
197
221
|
/>
|
|
198
|
-
<KPICard
|
|
199
|
-
title="TOKENS"
|
|
200
|
-
value={formatTokens(animatedTokens)}
|
|
201
|
-
delta={
|
|
222
|
+
<KPICard
|
|
223
|
+
title="TOKENS"
|
|
224
|
+
value={formatTokens(animatedTokens)}
|
|
225
|
+
delta={
|
|
226
|
+
hasEnoughHistory ? `+${formatTokens(deltaTokens)} (${windowLabel})` : "gathering..."
|
|
227
|
+
}
|
|
202
228
|
metric="tokens"
|
|
203
229
|
flashIntensity={tokenFlashIntensity}
|
|
204
230
|
/>
|
|
205
|
-
<KPICard
|
|
206
|
-
title="REQUESTS"
|
|
207
|
-
value={Math.round(animatedRequests).toLocaleString()}
|
|
231
|
+
<KPICard
|
|
232
|
+
title="REQUESTS"
|
|
233
|
+
value={Math.round(animatedRequests).toLocaleString()}
|
|
208
234
|
subValue={`${activeCount} active`}
|
|
209
235
|
metric="requests"
|
|
210
236
|
/>
|
|
211
237
|
{!isCompact && (
|
|
212
|
-
<KPICard
|
|
213
|
-
title="BURN RATE"
|
|
214
|
-
value={hasEnoughHistory ? `${formatCurrency(burnRateCostPerHour)}/hr` :
|
|
215
|
-
subValue={
|
|
238
|
+
<KPICard
|
|
239
|
+
title="BURN RATE"
|
|
240
|
+
value={hasEnoughHistory ? `${formatCurrency(burnRateCostPerHour)}/hr` : "--"}
|
|
241
|
+
subValue={
|
|
242
|
+
hasEnoughHistory
|
|
243
|
+
? `${formatBurnTokens(burnRateTokensPerMin)} tok/min`
|
|
244
|
+
: "gathering..."
|
|
245
|
+
}
|
|
216
246
|
metric="rate"
|
|
217
247
|
/>
|
|
218
248
|
)}
|
|
219
|
-
|
|
220
|
-
|
|
249
|
+
|
|
250
|
+
<box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
|
221
251
|
<box flexDirection="row" justifyContent="space-between">
|
|
222
252
|
<text fg={colors.textMuted}>ACTIVITY</text>
|
|
223
253
|
<text>
|
|
@@ -225,10 +255,10 @@ export function KpiStrip({
|
|
|
225
255
|
<span fg={colors.textMuted}> {formatRate(activity.instantRate)}/s</span>
|
|
226
256
|
</text>
|
|
227
257
|
</box>
|
|
228
|
-
<Sparkline
|
|
229
|
-
data={sparkData}
|
|
230
|
-
width={sparklineWidth}
|
|
231
|
-
label="tok/s"
|
|
258
|
+
<Sparkline
|
|
259
|
+
data={sparkData}
|
|
260
|
+
width={sparklineWidth}
|
|
261
|
+
label="tok/s"
|
|
232
262
|
fixedMax={sparkMax}
|
|
233
263
|
thresholds={{ warning: 150, error: 500 }}
|
|
234
264
|
style={sparklineConfig.style}
|
|
@@ -237,9 +267,9 @@ export function KpiStrip({
|
|
|
237
267
|
/>
|
|
238
268
|
</box>
|
|
239
269
|
</box>
|
|
240
|
-
|
|
270
|
+
|
|
241
271
|
<box height={1} overflow="hidden">
|
|
242
|
-
<text fg={colors.border}>{
|
|
272
|
+
<text fg={colors.border}>{"─".repeat(300)}</text>
|
|
243
273
|
</box>
|
|
244
274
|
</>
|
|
245
275
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ReactNode } from
|
|
2
|
-
import { useColors } from
|
|
3
|
-
import { usePulse } from
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
3
|
+
import { usePulse } from "../hooks/usePulse.ts";
|
|
4
4
|
|
|
5
5
|
interface LimitGaugeProps {
|
|
6
6
|
label: string;
|
|
@@ -17,58 +17,75 @@ interface LimitGaugeProps {
|
|
|
17
17
|
criticalThreshold?: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const FRACTIONAL_BLOCKS = [
|
|
20
|
+
const FRACTIONAL_BLOCKS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
21
21
|
|
|
22
|
-
function renderFractionalBar(
|
|
22
|
+
function renderFractionalBar(
|
|
23
|
+
percent: number,
|
|
24
|
+
width: number,
|
|
25
|
+
filledColor: string,
|
|
26
|
+
emptyColor: string,
|
|
27
|
+
): ReactNode[] {
|
|
23
28
|
const totalSegments = width * 8;
|
|
24
29
|
const filledSegments = Math.round((percent / 100) * totalSegments);
|
|
25
30
|
const fullBlocks = Math.floor(filledSegments / 8);
|
|
26
31
|
const remainder = filledSegments % 8;
|
|
27
32
|
const emptyBlocks = width - fullBlocks - (remainder > 0 ? 1 : 0);
|
|
28
|
-
|
|
33
|
+
|
|
29
34
|
const elements: ReactNode[] = [];
|
|
30
|
-
|
|
35
|
+
|
|
31
36
|
if (fullBlocks > 0) {
|
|
32
|
-
elements.push(
|
|
37
|
+
elements.push(
|
|
38
|
+
<span key="full" fg={filledColor}>
|
|
39
|
+
{"█".repeat(fullBlocks)}
|
|
40
|
+
</span>,
|
|
41
|
+
);
|
|
33
42
|
}
|
|
34
|
-
|
|
43
|
+
|
|
35
44
|
if (remainder > 0) {
|
|
36
|
-
elements.push(
|
|
45
|
+
elements.push(
|
|
46
|
+
<span key="partial" fg={filledColor}>
|
|
47
|
+
{FRACTIONAL_BLOCKS[remainder]}
|
|
48
|
+
</span>,
|
|
49
|
+
);
|
|
37
50
|
}
|
|
38
|
-
|
|
51
|
+
|
|
39
52
|
if (emptyBlocks > 0) {
|
|
40
|
-
elements.push(
|
|
53
|
+
elements.push(
|
|
54
|
+
<span key="empty" fg={emptyColor}>
|
|
55
|
+
{"─".repeat(emptyBlocks)}
|
|
56
|
+
</span>,
|
|
57
|
+
);
|
|
41
58
|
}
|
|
42
|
-
|
|
59
|
+
|
|
43
60
|
return elements;
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
function interpolatePulseColor(intensity: number, baseColor: string, dimColor: string): string {
|
|
47
64
|
const dimAmount = 0.4;
|
|
48
65
|
const t = intensity * dimAmount;
|
|
49
|
-
|
|
66
|
+
|
|
50
67
|
const parseHex = (hex: string) => {
|
|
51
|
-
const h = hex.replace(
|
|
68
|
+
const h = hex.replace("#", "");
|
|
52
69
|
return {
|
|
53
70
|
r: parseInt(h.substring(0, 2), 16),
|
|
54
71
|
g: parseInt(h.substring(2, 4), 16),
|
|
55
72
|
b: parseInt(h.substring(4, 6), 16),
|
|
56
73
|
};
|
|
57
74
|
};
|
|
58
|
-
|
|
75
|
+
|
|
59
76
|
const base = parseHex(baseColor);
|
|
60
77
|
const dim = parseHex(dimColor);
|
|
61
|
-
|
|
78
|
+
|
|
62
79
|
const r = Math.round(base.r + (dim.r - base.r) * t);
|
|
63
80
|
const g = Math.round(base.g + (dim.g - base.g) * t);
|
|
64
81
|
const b = Math.round(base.b + (dim.b - base.b) * t);
|
|
65
|
-
|
|
66
|
-
return `#${r.toString(16).padStart(2,
|
|
82
|
+
|
|
83
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
|
-
export function LimitGauge({
|
|
70
|
-
label,
|
|
71
|
-
usedPercent,
|
|
86
|
+
export function LimitGauge({
|
|
87
|
+
label,
|
|
88
|
+
usedPercent,
|
|
72
89
|
color,
|
|
73
90
|
ghost = false,
|
|
74
91
|
error,
|
|
@@ -81,34 +98,34 @@ export function LimitGauge({
|
|
|
81
98
|
criticalThreshold = 95,
|
|
82
99
|
}: LimitGaugeProps) {
|
|
83
100
|
const colors = useColors();
|
|
84
|
-
|
|
101
|
+
|
|
85
102
|
const percent = usedPercent ?? 0;
|
|
86
103
|
const isCritical = percent >= criticalThreshold;
|
|
87
104
|
const isWarning = percent >= warningThreshold && percent < criticalThreshold;
|
|
88
|
-
|
|
105
|
+
|
|
89
106
|
// Critical: fast, intense pulse (80ms interval)
|
|
90
107
|
// Warning: moderate pulse (200ms interval) to draw attention
|
|
91
108
|
const criticalPulseStep = usePulse({ enabled: isCritical, intervalMs: 80 });
|
|
92
109
|
const warningPulseStep = usePulse({ enabled: isWarning, intervalMs: 200 });
|
|
93
|
-
|
|
94
|
-
const pulseIntensity = isCritical
|
|
95
|
-
? Math.abs(Math.sin(criticalPulseStep * 0.3))
|
|
96
|
-
: isWarning
|
|
110
|
+
|
|
111
|
+
const pulseIntensity = isCritical
|
|
112
|
+
? Math.abs(Math.sin(criticalPulseStep * 0.3))
|
|
113
|
+
: isWarning
|
|
97
114
|
? Math.abs(Math.sin(warningPulseStep * 0.25))
|
|
98
115
|
: 0;
|
|
99
|
-
|
|
116
|
+
|
|
100
117
|
const truncateLabel = (lbl: string, maxLen: number): string => {
|
|
101
118
|
if (lbl.length <= maxLen) return lbl.padEnd(maxLen);
|
|
102
|
-
return lbl.slice(0, maxLen - 1) +
|
|
119
|
+
return lbl.slice(0, maxLen - 1) + "…";
|
|
103
120
|
};
|
|
104
|
-
|
|
121
|
+
|
|
105
122
|
if (compact) {
|
|
106
123
|
const shortLabel = truncateLabel(label, 10);
|
|
107
|
-
const percentStr = usedPercent !== null ? `${Math.round(percent)}%` :
|
|
108
|
-
const statusIcon = isCritical ?
|
|
124
|
+
const percentStr = usedPercent !== null ? `${Math.round(percent)}%` : "--";
|
|
125
|
+
const statusIcon = isCritical ? "!!" : isWarning ? "!" : "";
|
|
109
126
|
const statusColor = isCritical ? colors.error : isWarning ? color : colors.textMuted;
|
|
110
127
|
const textColor = isCritical ? colors.error : isWarning ? color : colors.text;
|
|
111
|
-
|
|
128
|
+
|
|
112
129
|
return (
|
|
113
130
|
<text height={1}>
|
|
114
131
|
{selected && <span fg={colors.primary}>▌</span>}
|
|
@@ -118,7 +135,7 @@ export function LimitGauge({
|
|
|
118
135
|
</text>
|
|
119
136
|
);
|
|
120
137
|
}
|
|
121
|
-
|
|
138
|
+
|
|
122
139
|
if (ghost) {
|
|
123
140
|
const ghostLabel = truncateLabel(label, labelWidth);
|
|
124
141
|
return (
|
|
@@ -126,57 +143,77 @@ export function LimitGauge({
|
|
|
126
143
|
<text height={1}>
|
|
127
144
|
<span fg={colors.textSubtle}>○ </span>
|
|
128
145
|
<span fg={colors.textSubtle}>{ghostLabel} </span>
|
|
129
|
-
<span fg={colors.textSubtle}>{
|
|
130
|
-
<span fg={colors.textSubtle}>
|
|
146
|
+
<span fg={colors.textSubtle}>{"─".repeat(barWidth)}</span>
|
|
147
|
+
<span fg={colors.textSubtle}> --</span>
|
|
131
148
|
</text>
|
|
132
149
|
</box>
|
|
133
150
|
);
|
|
134
151
|
}
|
|
135
|
-
|
|
152
|
+
|
|
136
153
|
if (error) {
|
|
137
154
|
const displayLabel = truncateLabel(label, labelWidth);
|
|
138
|
-
const errorIcon = selected ?
|
|
155
|
+
const errorIcon = selected ? "▌" : "✗";
|
|
139
156
|
return (
|
|
140
|
-
<box
|
|
157
|
+
<box
|
|
158
|
+
height={1}
|
|
159
|
+
overflow="hidden"
|
|
160
|
+
{...(selected ? { backgroundColor: colors.borderMuted } : {})}
|
|
161
|
+
>
|
|
141
162
|
<text height={1}>
|
|
142
163
|
<span fg={selected ? colors.primary : colors.error}>{errorIcon} </span>
|
|
143
164
|
<span fg={selected ? colors.text : colors.text}>{displayLabel} </span>
|
|
144
|
-
<span fg={colors.error}>{
|
|
165
|
+
<span fg={colors.error}>{"─".repeat(barWidth)}</span>
|
|
145
166
|
<span fg={colors.error}> ERR</span>
|
|
146
167
|
</text>
|
|
147
168
|
</box>
|
|
148
169
|
);
|
|
149
170
|
}
|
|
150
|
-
|
|
151
|
-
const barColor = isCritical
|
|
171
|
+
|
|
172
|
+
const barColor = isCritical
|
|
152
173
|
? interpolatePulseColor(pulseIntensity, colors.error, colors.background)
|
|
153
|
-
: isWarning
|
|
174
|
+
: isWarning
|
|
154
175
|
? interpolatePulseColor(pulseIntensity, color, colors.background)
|
|
155
176
|
: color;
|
|
156
|
-
|
|
157
|
-
const statusIcon = isCritical ?
|
|
177
|
+
|
|
178
|
+
const statusIcon = isCritical ? "!!" : isWarning ? "!" : "●";
|
|
158
179
|
const statusColor = isCritical ? colors.error : isWarning ? color : colors.success;
|
|
159
|
-
|
|
180
|
+
|
|
160
181
|
const displayLabel = truncateLabel(label, labelWidth);
|
|
161
|
-
const percentStr = usedPercent !== null ? `${Math.round(percent)}%`.padStart(4) :
|
|
162
|
-
|
|
182
|
+
const percentStr = usedPercent !== null ? `${Math.round(percent)}%`.padStart(4) : " --";
|
|
183
|
+
|
|
163
184
|
const percentDisplay = isCritical ? (
|
|
164
|
-
<span fg={colors.background} bg={colors.error}>
|
|
185
|
+
<span fg={colors.background} bg={colors.error}>
|
|
186
|
+
{percentStr}
|
|
187
|
+
</span>
|
|
165
188
|
) : (
|
|
166
189
|
<span fg={isWarning ? color : colors.textMuted}>{percentStr}</span>
|
|
167
190
|
);
|
|
168
|
-
|
|
169
|
-
const resetDisplay = resetTime ?
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const selectionIndicator = selected ? '▌' : statusIcon;
|
|
174
|
-
|
|
191
|
+
|
|
192
|
+
const resetDisplay = resetTime ? <span fg={colors.textSubtle}> {resetTime}</span> : null;
|
|
193
|
+
|
|
194
|
+
const selectionIndicator = selected ? "▌" : statusIcon;
|
|
195
|
+
|
|
175
196
|
return (
|
|
176
|
-
<box
|
|
197
|
+
<box
|
|
198
|
+
height={1}
|
|
199
|
+
overflow="hidden"
|
|
200
|
+
{...(selected ? { backgroundColor: colors.borderMuted } : {})}
|
|
201
|
+
>
|
|
177
202
|
<text height={1}>
|
|
178
203
|
<span fg={selected ? colors.primary : statusColor}>{selectionIndicator} </span>
|
|
179
|
-
<span
|
|
204
|
+
<span
|
|
205
|
+
fg={
|
|
206
|
+
selected
|
|
207
|
+
? colors.text
|
|
208
|
+
: isCritical
|
|
209
|
+
? colors.error
|
|
210
|
+
: isWarning
|
|
211
|
+
? color
|
|
212
|
+
: colors.textMuted
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
{displayLabel}{" "}
|
|
216
|
+
</span>
|
|
180
217
|
{renderFractionalBar(percent, barWidth, barColor, colors.border)}
|
|
181
218
|
{percentDisplay}
|
|
182
219
|
{resetDisplay}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { RGBA } from "@opentui/core";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
/** Standardized z-index tiers for overlay layering */
|
|
5
5
|
export const Z_INDEX = {
|
|
@@ -18,8 +18,8 @@ interface ModalBackdropProps {
|
|
|
18
18
|
onBackdropClick?: () => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function ModalBackdrop({
|
|
22
|
-
children,
|
|
21
|
+
export function ModalBackdrop({
|
|
22
|
+
children,
|
|
23
23
|
zIndex = Z_INDEX.MODAL,
|
|
24
24
|
onBackdropClick,
|
|
25
25
|
}: ModalBackdropProps) {
|