@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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
const BUCKET_COUNT = 60;
|
|
4
4
|
const AVG_WINDOW = 10;
|
|
@@ -27,22 +27,24 @@ export interface UseActivityRateResult {
|
|
|
27
27
|
|
|
28
28
|
export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
29
29
|
const [buckets, setBuckets] = useState<number[]>(() => Array(BUCKET_COUNT).fill(0));
|
|
30
|
-
const [activity, setActivity] = useState<ActivityState>({
|
|
31
|
-
instantRate: 0,
|
|
30
|
+
const [activity, setActivity] = useState<ActivityState>({
|
|
31
|
+
instantRate: 0,
|
|
32
|
+
avgRate: 0,
|
|
33
|
+
isSpike: false,
|
|
32
34
|
});
|
|
33
|
-
|
|
34
|
-
const stateRef = useRef<{
|
|
35
|
-
lastTokens: number;
|
|
35
|
+
|
|
36
|
+
const stateRef = useRef<{
|
|
37
|
+
lastTokens: number;
|
|
36
38
|
lastTokenTime: number;
|
|
37
39
|
lastShiftTime: number;
|
|
38
40
|
initialized: boolean;
|
|
39
|
-
}>({
|
|
40
|
-
lastTokens: -1,
|
|
41
|
+
}>({
|
|
42
|
+
lastTokens: -1,
|
|
41
43
|
lastTokenTime: Date.now(),
|
|
42
44
|
lastShiftTime: Date.now(),
|
|
43
45
|
initialized: false,
|
|
44
46
|
});
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
const debugDataRef = useRef<ActivityDebugData>({
|
|
47
49
|
lastDeltaTokens: 0,
|
|
48
50
|
lastDt: 0,
|
|
@@ -54,12 +56,12 @@ export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
|
54
56
|
|
|
55
57
|
const calculateRates = useCallback((bucketData: number[]) => {
|
|
56
58
|
const recentBuckets = bucketData.slice(-AVG_WINDOW);
|
|
57
|
-
|
|
59
|
+
|
|
58
60
|
const instantRate = Math.max(...recentBuckets);
|
|
59
61
|
const avgRate = recentBuckets.reduce((a, b) => a + b, 0) / AVG_WINDOW;
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
const isSpike = instantRate >= 500 && instantRate >= avgRate * 3;
|
|
62
|
-
|
|
64
|
+
|
|
63
65
|
return { instantRate, avgRate, isSpike };
|
|
64
66
|
}, []);
|
|
65
67
|
|
|
@@ -69,7 +71,7 @@ export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
|
69
71
|
const shift = Math.min(count, BUCKET_COUNT);
|
|
70
72
|
const decayFactor = 0.7;
|
|
71
73
|
const zeroThreshold = 0.5; // Values below this become true zero (shows as dot)
|
|
72
|
-
|
|
74
|
+
|
|
73
75
|
for (let i = 0; i < BUCKET_COUNT - shift; i++) {
|
|
74
76
|
const val = bucketData[i + shift] ?? 0;
|
|
75
77
|
newBuckets[i] = val < zeroThreshold ? 0 : val;
|
|
@@ -77,69 +79,75 @@ export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
|
77
79
|
for (let i = BUCKET_COUNT - shift; i < BUCKET_COUNT; i++) {
|
|
78
80
|
const prevValue = bucketData[BUCKET_COUNT - shift - 1] ?? 0;
|
|
79
81
|
const decaySteps = i - (BUCKET_COUNT - shift) + 1;
|
|
80
|
-
const decayed = prevValue *
|
|
82
|
+
const decayed = prevValue * decayFactor ** decaySteps;
|
|
81
83
|
newBuckets[i] = decayed < zeroThreshold ? 0 : decayed;
|
|
82
84
|
}
|
|
83
85
|
return newBuckets;
|
|
84
86
|
}, []);
|
|
85
87
|
|
|
86
|
-
const applyActivityRamp = useCallback(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < rampWidth; i++) {
|
|
91
|
-
const bucketIdx = BUCKET_COUNT - 1 - i;
|
|
92
|
-
if (bucketIdx < 0) break;
|
|
93
|
-
|
|
94
|
-
const distanceFromPeak = i;
|
|
95
|
-
const rampMultiplier = Math.max(0.15, 1 - (distanceFromPeak * 0.25));
|
|
96
|
-
const rampedValue = peakRate * rampMultiplier;
|
|
97
|
-
|
|
98
|
-
newBuckets[bucketIdx] = Math.max(newBuckets[bucketIdx] ?? 0, rampedValue);
|
|
99
|
-
}
|
|
100
|
-
return newBuckets;
|
|
101
|
-
}, []);
|
|
88
|
+
const applyActivityRamp = useCallback(
|
|
89
|
+
(bucketData: number[], peakRate: number, fillCount: number): number[] => {
|
|
90
|
+
const newBuckets = [...bucketData];
|
|
91
|
+
const rampWidth = Math.min(fillCount + 2, 6);
|
|
102
92
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
setBuckets(prev => {
|
|
114
|
-
const secSinceShift = (now - stateRef.current.lastShiftTime) / 1000;
|
|
115
|
-
const bucketsToShift = Math.floor(secSinceShift);
|
|
116
|
-
|
|
117
|
-
let newBuckets = shiftBucketsWithDecay(prev, bucketsToShift);
|
|
118
|
-
if (bucketsToShift > 0) {
|
|
119
|
-
stateRef.current.lastShiftTime = now;
|
|
120
|
-
debugDataRef.current.bucketsShifted = bucketsToShift;
|
|
93
|
+
for (let i = 0; i < rampWidth; i++) {
|
|
94
|
+
const bucketIdx = BUCKET_COUNT - 1 - i;
|
|
95
|
+
if (bucketIdx < 0) break;
|
|
96
|
+
|
|
97
|
+
const distanceFromPeak = i;
|
|
98
|
+
const rampMultiplier = Math.max(0.15, 1 - distanceFromPeak * 0.25);
|
|
99
|
+
const rampedValue = peakRate * rampMultiplier;
|
|
100
|
+
|
|
101
|
+
newBuckets[bucketIdx] = Math.max(newBuckets[bucketIdx] ?? 0, rampedValue);
|
|
121
102
|
}
|
|
122
|
-
|
|
123
|
-
newBuckets = applyActivityRamp(newBuckets, rate, bucketsToShift);
|
|
124
|
-
|
|
125
|
-
const rates = calculateRates(newBuckets);
|
|
126
|
-
setActivity(rates);
|
|
127
103
|
return newBuckets;
|
|
128
|
-
}
|
|
129
|
-
|
|
104
|
+
},
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const injectDelta = useCallback(
|
|
109
|
+
(delta: number) => {
|
|
110
|
+
if (delta <= 0 || delta >= 1000000) return;
|
|
111
|
+
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const rate = delta;
|
|
114
|
+
|
|
115
|
+
debugDataRef.current.lastDeltaTokens = delta;
|
|
116
|
+
debugDataRef.current.lastDt = 1;
|
|
117
|
+
debugDataRef.current.currentBucketValue = rate;
|
|
118
|
+
|
|
119
|
+
setBuckets((prev) => {
|
|
120
|
+
const secSinceShift = (now - stateRef.current.lastShiftTime) / 1000;
|
|
121
|
+
const bucketsToShift = Math.floor(secSinceShift);
|
|
122
|
+
|
|
123
|
+
let newBuckets = shiftBucketsWithDecay(prev, bucketsToShift);
|
|
124
|
+
if (bucketsToShift > 0) {
|
|
125
|
+
stateRef.current.lastShiftTime = now;
|
|
126
|
+
debugDataRef.current.bucketsShifted = bucketsToShift;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
newBuckets = applyActivityRamp(newBuckets, rate, bucketsToShift);
|
|
130
|
+
|
|
131
|
+
const rates = calculateRates(newBuckets);
|
|
132
|
+
setActivity(rates);
|
|
133
|
+
return newBuckets;
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
[calculateRates, shiftBucketsWithDecay, applyActivityRamp],
|
|
137
|
+
);
|
|
130
138
|
|
|
131
139
|
useEffect(() => {
|
|
132
140
|
const now = Date.now();
|
|
133
|
-
|
|
141
|
+
|
|
134
142
|
debugDataRef.current.refreshCount++;
|
|
135
143
|
debugDataRef.current.lastRefreshTime = now;
|
|
136
144
|
|
|
137
145
|
const prevTokens = stateRef.current.lastTokens;
|
|
138
146
|
const wasInitialized = stateRef.current.initialized;
|
|
139
|
-
|
|
147
|
+
|
|
140
148
|
if (prevTokens === -1 || prevTokens === 0 || !wasInitialized) {
|
|
141
|
-
stateRef.current = {
|
|
142
|
-
lastTokens: totalTokens,
|
|
149
|
+
stateRef.current = {
|
|
150
|
+
lastTokens: totalTokens,
|
|
143
151
|
lastTokenTime: now,
|
|
144
152
|
lastShiftTime: now,
|
|
145
153
|
initialized: totalTokens > 0,
|
|
@@ -149,7 +157,7 @@ export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
|
149
157
|
|
|
150
158
|
const deltaTokens = Math.max(0, totalTokens - prevTokens);
|
|
151
159
|
const rawDt = (now - stateRef.current.lastTokenTime) / 1000;
|
|
152
|
-
|
|
160
|
+
|
|
153
161
|
// Require minimum 0.5s between rate calculations
|
|
154
162
|
// DON'T update lastTokens here — that would silently consume the delta
|
|
155
163
|
// without ever writing it to buckets. Let the delta accumulate until
|
|
@@ -157,35 +165,35 @@ export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
|
157
165
|
if (rawDt < 0.5) {
|
|
158
166
|
return;
|
|
159
167
|
}
|
|
160
|
-
|
|
168
|
+
|
|
161
169
|
// Cap dt at 10s so long idle gaps don't dilute the rate when tokens finally arrive
|
|
162
170
|
// This shows activity as a "burst" rather than averaged over idle time
|
|
163
171
|
const dt = Math.min(Math.max(rawDt, 0.5), 10);
|
|
164
|
-
|
|
172
|
+
|
|
165
173
|
debugDataRef.current.lastDeltaTokens = deltaTokens;
|
|
166
174
|
debugDataRef.current.lastDt = dt;
|
|
167
175
|
|
|
168
176
|
if (deltaTokens > 0 && deltaTokens < 1000000) {
|
|
169
177
|
const rate = deltaTokens / dt;
|
|
170
178
|
debugDataRef.current.currentBucketValue = rate;
|
|
171
|
-
|
|
172
|
-
setBuckets(prev => {
|
|
179
|
+
|
|
180
|
+
setBuckets((prev) => {
|
|
173
181
|
const secSinceShift = (now - stateRef.current.lastShiftTime) / 1000;
|
|
174
182
|
const bucketsToShift = Math.floor(secSinceShift);
|
|
175
|
-
|
|
183
|
+
|
|
176
184
|
let newBuckets = shiftBucketsWithDecay(prev, bucketsToShift);
|
|
177
185
|
if (bucketsToShift > 0) {
|
|
178
186
|
stateRef.current.lastShiftTime = now;
|
|
179
187
|
debugDataRef.current.bucketsShifted = bucketsToShift;
|
|
180
188
|
}
|
|
181
|
-
|
|
189
|
+
|
|
182
190
|
newBuckets = applyActivityRamp(newBuckets, rate, bucketsToShift);
|
|
183
|
-
|
|
191
|
+
|
|
184
192
|
const rates = calculateRates(newBuckets);
|
|
185
193
|
setActivity(rates);
|
|
186
194
|
return newBuckets;
|
|
187
195
|
});
|
|
188
|
-
|
|
196
|
+
|
|
189
197
|
stateRef.current.lastTokens = totalTokens;
|
|
190
198
|
stateRef.current.lastTokenTime = now;
|
|
191
199
|
}
|
|
@@ -195,15 +203,15 @@ export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
|
195
203
|
const interval = setInterval(() => {
|
|
196
204
|
const now = Date.now();
|
|
197
205
|
const secSinceShift = (now - stateRef.current.lastShiftTime) / 1000;
|
|
198
|
-
|
|
206
|
+
|
|
199
207
|
if (secSinceShift >= 1) {
|
|
200
208
|
const bucketsToShift = Math.floor(secSinceShift);
|
|
201
209
|
debugDataRef.current.bucketsShifted = bucketsToShift;
|
|
202
|
-
|
|
203
|
-
setBuckets(prev => {
|
|
210
|
+
|
|
211
|
+
setBuckets((prev) => {
|
|
204
212
|
const newBuckets = shiftBucketsWithDecay(prev, bucketsToShift);
|
|
205
213
|
stateRef.current.lastShiftTime = now;
|
|
206
|
-
|
|
214
|
+
|
|
207
215
|
const rates = calculateRates(newBuckets);
|
|
208
216
|
setActivity(rates);
|
|
209
217
|
return newBuckets;
|
|
@@ -214,8 +222,8 @@ export function useEmaActivity(totalTokens: number): UseActivityRateResult {
|
|
|
214
222
|
return () => clearInterval(interval);
|
|
215
223
|
}, [calculateRates, shiftBucketsWithDecay]);
|
|
216
224
|
|
|
217
|
-
return {
|
|
218
|
-
activity,
|
|
225
|
+
return {
|
|
226
|
+
activity,
|
|
219
227
|
sparkData: buckets,
|
|
220
228
|
debugDataRef,
|
|
221
229
|
injectDelta,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
export interface UseEntranceAnimationOptions {
|
|
4
4
|
durationMs?: number;
|
|
@@ -17,18 +17,18 @@ export function useEntranceAnimation(options: UseEntranceAnimationOptions = {}):
|
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
const intervalMs = 1000 / fps;
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
const tick = () => {
|
|
22
22
|
const elapsed = Date.now() - mountTime.current;
|
|
23
23
|
const progress = Math.min(1, elapsed / durationMs);
|
|
24
|
-
const eased = 1 -
|
|
24
|
+
const eased = 1 - (1 - progress) ** 2;
|
|
25
25
|
setIntensity(eased);
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
if (progress < 1) {
|
|
28
28
|
setTimeout(tick, intervalMs);
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
tick();
|
|
33
33
|
}, [durationMs, fps]);
|
|
34
34
|
|
|
@@ -41,22 +41,22 @@ export function useEntranceAnimation(options: UseEntranceAnimationOptions = {}):
|
|
|
41
41
|
export function applyEntranceFade(intensity: number, color: string, dimColor: string): string {
|
|
42
42
|
if (intensity >= 1) return color;
|
|
43
43
|
if (intensity <= 0) return dimColor;
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
const parseHex = (hex: string): [number, number, number] => {
|
|
46
|
-
const clean = hex.replace(
|
|
46
|
+
const clean = hex.replace("#", "");
|
|
47
47
|
return [
|
|
48
48
|
parseInt(clean.slice(0, 2), 16),
|
|
49
49
|
parseInt(clean.slice(2, 4), 16),
|
|
50
50
|
parseInt(clean.slice(4, 6), 16),
|
|
51
51
|
];
|
|
52
52
|
};
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
const [r1, g1, b1] = parseHex(dimColor);
|
|
55
55
|
const [r2, g2, b2] = parseHex(color);
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
const r = Math.round(r1 + (r2 - r1) * intensity);
|
|
58
58
|
const g = Math.round(g1 + (g2 - g1) * intensity);
|
|
59
59
|
const b = Math.round(b1 + (b2 - b1) * intensity);
|
|
60
|
-
|
|
61
|
-
return `#${r.toString(16).padStart(2,
|
|
60
|
+
|
|
61
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
62
62
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
export interface ExitAnimationItem<T> {
|
|
4
4
|
item: T;
|
|
@@ -27,7 +27,7 @@ interface ExitingEntry<T> {
|
|
|
27
27
|
|
|
28
28
|
export function useExitAnimation<T>(
|
|
29
29
|
items: T[],
|
|
30
|
-
options: UseExitAnimationOptions<T
|
|
30
|
+
options: UseExitAnimationOptions<T>,
|
|
31
31
|
): UseExitAnimationResult<T> {
|
|
32
32
|
const { durationMs = 500, fps = 30, getKey, bulkThreshold = 10 } = options;
|
|
33
33
|
const prevKeysRef = useRef<Set<string>>(new Set());
|
|
@@ -43,6 +43,10 @@ export function useExitAnimation<T>(
|
|
|
43
43
|
for (const [key, entry] of exitingRef.current) {
|
|
44
44
|
if (now - entry.startTime >= durationMs) {
|
|
45
45
|
exitingRef.current.delete(key);
|
|
46
|
+
// Also clean from prev refs so the render-time diff
|
|
47
|
+
// doesn't re-add them on the next forceUpdate render.
|
|
48
|
+
prevKeysRef.current.delete(key);
|
|
49
|
+
prevItemsRef.current.delete(key);
|
|
46
50
|
} else {
|
|
47
51
|
anyActive = true;
|
|
48
52
|
}
|
|
@@ -123,7 +127,7 @@ export function useExitAnimation<T>(
|
|
|
123
127
|
for (const [, entry] of exitingRef.current) {
|
|
124
128
|
const elapsed = now - entry.startTime;
|
|
125
129
|
const progress = Math.min(1, elapsed / durationMs);
|
|
126
|
-
const eased =
|
|
130
|
+
const eased = progress ** 2;
|
|
127
131
|
|
|
128
132
|
result.push({
|
|
129
133
|
item: entry.item,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for the usePulse hook.
|
|
@@ -20,7 +20,7 @@ export interface UsePulseOptions {
|
|
|
20
20
|
* @returns RGB tuple [r, g, b] or null if invalid
|
|
21
21
|
*/
|
|
22
22
|
function parseHexColor(hex: string): [number, number, number] | null {
|
|
23
|
-
const cleanHex = hex.startsWith(
|
|
23
|
+
const cleanHex = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
24
24
|
|
|
25
25
|
let expandedHex: string;
|
|
26
26
|
|
|
@@ -57,7 +57,7 @@ function parseHexColor(hex: string): [number, number, number] | null {
|
|
|
57
57
|
function rgbToHex(r: number, g: number, b: number): string {
|
|
58
58
|
const toHex = (n: number) => {
|
|
59
59
|
const clamped = Math.max(0, Math.min(255, Math.round(n)));
|
|
60
|
-
return clamped.toString(16).padStart(2,
|
|
60
|
+
return clamped.toString(16).padStart(2, "0");
|
|
61
61
|
};
|
|
62
62
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
63
63
|
}
|
|
@@ -81,7 +81,7 @@ export function getPulseColor(
|
|
|
81
81
|
step: number,
|
|
82
82
|
baseColor: string,
|
|
83
83
|
peakColor: string,
|
|
84
|
-
steps: number = 12
|
|
84
|
+
steps: number = 12,
|
|
85
85
|
): string {
|
|
86
86
|
const baseRgb = parseHexColor(baseColor);
|
|
87
87
|
const peakRgb = parseHexColor(peakColor);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { CliRenderer } from
|
|
2
|
-
import { useRenderer } from
|
|
3
|
-
import { createContext, useContext } from
|
|
1
|
+
import type { CliRenderer } from "@opentui/core";
|
|
2
|
+
import { useRenderer } from "@opentui/react";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Context to signal we're in test/headless mode where renderer may not be available.
|
|
@@ -15,10 +15,10 @@ export const TestModeContext = createContext<boolean>(false);
|
|
|
15
15
|
*/
|
|
16
16
|
export function useSafeRenderer(): CliRenderer | null {
|
|
17
17
|
const isTestMode = useContext(TestModeContext);
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
if (isTestMode) {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
return useRenderer();
|
|
24
24
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { AgentSessionAggregate } from "@/agents/types.ts";
|
|
3
|
+
import type { ProviderUsageData } from "@/plugins/types/provider.ts";
|
|
4
|
+
import type { ProviderState } from "../contexts/PluginContext.tsx";
|
|
5
|
+
|
|
6
|
+
interface ProviderSessionTotals {
|
|
7
|
+
input: number;
|
|
8
|
+
output: number;
|
|
9
|
+
cacheRead: number;
|
|
10
|
+
cacheWrite: number;
|
|
11
|
+
costToday: number;
|
|
12
|
+
costMonth: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function aggregateSessionsByProvider(
|
|
16
|
+
sessions: AgentSessionAggregate[],
|
|
17
|
+
): Map<string, ProviderSessionTotals> {
|
|
18
|
+
const totals = new Map<string, ProviderSessionTotals>();
|
|
19
|
+
|
|
20
|
+
for (const session of sessions) {
|
|
21
|
+
for (const stream of session.streams) {
|
|
22
|
+
const pid = stream.providerId;
|
|
23
|
+
let entry = totals.get(pid);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
entry = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, costToday: 0, costMonth: 0 };
|
|
26
|
+
totals.set(pid, entry);
|
|
27
|
+
}
|
|
28
|
+
entry.input += stream.tokens.input;
|
|
29
|
+
entry.output += stream.tokens.output;
|
|
30
|
+
entry.cacheRead += stream.tokens.cacheRead ?? 0;
|
|
31
|
+
entry.cacheWrite += stream.tokens.cacheWrite ?? 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Cost is session-level — attribute to providers proportionally by token share
|
|
35
|
+
if (session.streams.length > 0) {
|
|
36
|
+
const totalSessionTokens = session.streams.reduce(
|
|
37
|
+
(s, st) =>
|
|
38
|
+
s +
|
|
39
|
+
st.tokens.input +
|
|
40
|
+
st.tokens.output +
|
|
41
|
+
(st.tokens.cacheRead ?? 0) +
|
|
42
|
+
(st.tokens.cacheWrite ?? 0),
|
|
43
|
+
0,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
for (const stream of session.streams) {
|
|
47
|
+
const pid = stream.providerId;
|
|
48
|
+
const entry = totals.get(pid)!;
|
|
49
|
+
const streamTokens =
|
|
50
|
+
stream.tokens.input +
|
|
51
|
+
stream.tokens.output +
|
|
52
|
+
(stream.tokens.cacheRead ?? 0) +
|
|
53
|
+
(stream.tokens.cacheWrite ?? 0);
|
|
54
|
+
const ratio = totalSessionTokens > 0 ? streamTokens / totalSessionTokens : 0;
|
|
55
|
+
entry.costToday += session.costInDay * ratio;
|
|
56
|
+
entry.costMonth += session.costInMonth * ratio;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return totals;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function enrichUsageData(
|
|
65
|
+
usage: ProviderUsageData,
|
|
66
|
+
sessionTotals: ProviderSessionTotals,
|
|
67
|
+
): ProviderUsageData {
|
|
68
|
+
const enriched = { ...usage };
|
|
69
|
+
|
|
70
|
+
if (!enriched.tokens && (sessionTotals.input > 0 || sessionTotals.output > 0)) {
|
|
71
|
+
enriched.tokens = {
|
|
72
|
+
input: sessionTotals.input,
|
|
73
|
+
output: sessionTotals.output,
|
|
74
|
+
...(sessionTotals.cacheRead > 0 ? { cacheRead: sessionTotals.cacheRead } : {}),
|
|
75
|
+
...(sessionTotals.cacheWrite > 0 ? { cacheWrite: sessionTotals.cacheWrite } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hasSessionCost = sessionTotals.costToday > 0 || sessionTotals.costMonth > 0;
|
|
80
|
+
if (hasSessionCost) {
|
|
81
|
+
const costObj: NonNullable<ProviderUsageData["cost"]> = {
|
|
82
|
+
source: enriched.cost?.source ?? "estimated",
|
|
83
|
+
};
|
|
84
|
+
if (enriched.cost?.actual) costObj.actual = enriched.cost.actual;
|
|
85
|
+
if (enriched.cost?.estimated) {
|
|
86
|
+
costObj.estimated = enriched.cost.estimated;
|
|
87
|
+
} else if (sessionTotals.costToday > 0) {
|
|
88
|
+
costObj.estimated = { total: sessionTotals.costToday, currency: "USD" };
|
|
89
|
+
}
|
|
90
|
+
if (sessionTotals.costToday > 0) {
|
|
91
|
+
costObj.estimatedDaily = { total: sessionTotals.costToday, currency: "USD" };
|
|
92
|
+
}
|
|
93
|
+
if (sessionTotals.costMonth > 0) {
|
|
94
|
+
costObj.estimatedMonthly = { total: sessionTotals.costMonth, currency: "USD" };
|
|
95
|
+
}
|
|
96
|
+
enriched.cost = costObj;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return enriched;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findProviderMatch(
|
|
103
|
+
providerId: string,
|
|
104
|
+
providers: Map<string, ProviderState>,
|
|
105
|
+
): string | null {
|
|
106
|
+
if (providers.has(providerId)) return providerId;
|
|
107
|
+
|
|
108
|
+
for (const [pluginId, state] of providers) {
|
|
109
|
+
const aliases = state.plugin.meta?.providerAliases ?? [];
|
|
110
|
+
if (aliases.includes(providerId)) return pluginId;
|
|
111
|
+
if (pluginId.includes(providerId) || providerId.includes(pluginId)) return pluginId;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function useSessionEnrichedProviders(
|
|
117
|
+
providers: Map<string, ProviderState>,
|
|
118
|
+
sessions: AgentSessionAggregate[],
|
|
119
|
+
): Map<string, ProviderState> {
|
|
120
|
+
return useMemo(() => {
|
|
121
|
+
if (sessions.length === 0) return providers;
|
|
122
|
+
|
|
123
|
+
const sessionTotals = aggregateSessionsByProvider(sessions);
|
|
124
|
+
if (sessionTotals.size === 0) return providers;
|
|
125
|
+
|
|
126
|
+
const enriched = new Map(providers);
|
|
127
|
+
let changed = false;
|
|
128
|
+
|
|
129
|
+
for (const [sessionProviderId, totals] of sessionTotals) {
|
|
130
|
+
const pluginId = findProviderMatch(sessionProviderId, providers);
|
|
131
|
+
if (!pluginId) continue;
|
|
132
|
+
|
|
133
|
+
const state = enriched.get(pluginId);
|
|
134
|
+
if (!state?.usage) continue;
|
|
135
|
+
const alreadyHasAll =
|
|
136
|
+
state.usage.tokens &&
|
|
137
|
+
state.usage.cost?.estimatedDaily &&
|
|
138
|
+
state.usage.cost?.estimatedMonthly;
|
|
139
|
+
if (alreadyHasAll) continue;
|
|
140
|
+
|
|
141
|
+
const enrichedUsage = enrichUsageData(state.usage, totals);
|
|
142
|
+
if (enrichedUsage !== state.usage) {
|
|
143
|
+
enriched.set(pluginId, { ...state, usage: enrichedUsage });
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return changed ? enriched : providers;
|
|
149
|
+
}, [providers, sessions]);
|
|
150
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
export interface UseValueFlashOptions {
|
|
4
4
|
durationMs?: number;
|
|
@@ -14,10 +14,10 @@ export interface UseValueFlashResult {
|
|
|
14
14
|
|
|
15
15
|
export function useValueFlash(
|
|
16
16
|
value: number,
|
|
17
|
-
options: UseValueFlashOptions = {}
|
|
17
|
+
options: UseValueFlashOptions = {},
|
|
18
18
|
): UseValueFlashResult {
|
|
19
19
|
const { durationMs = 600, increaseOnly = true, threshold = 0 } = options;
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
const prevValueRef = useRef<number>(value);
|
|
22
22
|
const [isFlashing, setIsFlashing] = useState(false);
|
|
23
23
|
const [step, setStep] = useState(0);
|
|
@@ -30,10 +30,8 @@ export function useValueFlash(
|
|
|
30
30
|
useEffect(() => {
|
|
31
31
|
const prevValue = prevValueRef.current;
|
|
32
32
|
const delta = value - prevValue;
|
|
33
|
-
|
|
34
|
-
const shouldFlash = increaseOnly
|
|
35
|
-
? delta > threshold
|
|
36
|
-
: Math.abs(delta) > threshold;
|
|
33
|
+
|
|
34
|
+
const shouldFlash = increaseOnly ? delta > threshold : Math.abs(delta) > threshold;
|
|
37
35
|
|
|
38
36
|
if (shouldFlash && prevValue !== value) {
|
|
39
37
|
if (flashTimeoutRef.current) {
|
|
@@ -74,18 +72,16 @@ export function useValueFlash(
|
|
|
74
72
|
};
|
|
75
73
|
}, [value, durationMs, increaseOnly, threshold, steps, stepDuration]);
|
|
76
74
|
|
|
77
|
-
const intensity = isFlashing
|
|
78
|
-
? Math.sin((step / steps) * Math.PI)
|
|
79
|
-
: 0;
|
|
75
|
+
const intensity = isFlashing ? Math.sin((step / steps) * Math.PI) : 0;
|
|
80
76
|
|
|
81
77
|
return { isFlashing, intensity, step };
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
export function interpolateColor(factor: number, colorA: string, colorB: string): string {
|
|
85
81
|
const parseHex = (hex: string): [number, number, number] | null => {
|
|
86
|
-
const clean = hex.startsWith(
|
|
82
|
+
const clean = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
87
83
|
if (clean.length === 3) {
|
|
88
|
-
const [r, g, b] = clean.split(
|
|
84
|
+
const [r, g, b] = clean.split("").map((c) => parseInt(c + c, 16));
|
|
89
85
|
return [r!, g!, b!];
|
|
90
86
|
}
|
|
91
87
|
if (clean.length === 6) {
|
|
@@ -100,12 +96,12 @@ export function interpolateColor(factor: number, colorA: string, colorB: string)
|
|
|
100
96
|
|
|
101
97
|
const a = parseHex(colorA);
|
|
102
98
|
const b = parseHex(colorB);
|
|
103
|
-
|
|
99
|
+
|
|
104
100
|
if (!a || !b) return colorA;
|
|
105
101
|
|
|
106
102
|
const r = Math.round(a[0] + (b[0] - a[0]) * factor);
|
|
107
103
|
const g = Math.round(a[1] + (b[1] - a[1]) * factor);
|
|
108
104
|
const bl = Math.round(a[2] + (b[2] - a[2]) * factor);
|
|
109
105
|
|
|
110
|
-
return `#${r.toString(16).padStart(2,
|
|
106
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${bl.toString(16).padStart(2, "0")}`;
|
|
111
107
|
}
|
package/src/tui/index.tsx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { createCliRenderer } from
|
|
2
|
-
import { createRoot } from
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import { createCliRenderer } from "@opentui/core";
|
|
2
|
+
import { createRoot } from "@opentui/react";
|
|
3
|
+
import type { DemoPreset } from "@/demo/simulator.ts";
|
|
4
|
+
import { pluginLifecycle } from "@/plugins/lifecycle.ts";
|
|
5
|
+
import { notificationBus } from "@/plugins/notification-bus.ts";
|
|
6
|
+
import { type CreateAppOptions, createAppElement } from "./createApp.tsx";
|
|
7
|
+
import { registerShutdown } from "./shutdown.ts";
|
|
5
8
|
|
|
6
9
|
export interface TuiOptions {
|
|
7
10
|
refreshInterval?: number;
|
|
@@ -12,6 +15,8 @@ export interface TuiOptions {
|
|
|
12
15
|
cliPlugins?: string[];
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
const FORCE_EXIT_GRACE_MS = 1_000;
|
|
19
|
+
|
|
15
20
|
export async function startTui(options: TuiOptions = {}) {
|
|
16
21
|
const renderer = await createCliRenderer({
|
|
17
22
|
exitOnCtrlC: false,
|
|
@@ -43,8 +48,26 @@ export async function startTui(options: TuiOptions = {}) {
|
|
|
43
48
|
|
|
44
49
|
root.render(createAppElement(appOptions));
|
|
45
50
|
|
|
51
|
+
const shutdown = async () => {
|
|
52
|
+
try {
|
|
53
|
+
await pluginLifecycle.stopAll();
|
|
54
|
+
await pluginLifecycle.destroyAll();
|
|
55
|
+
} catch {
|
|
56
|
+
/* best-effort */
|
|
57
|
+
}
|
|
58
|
+
notificationBus.destroy();
|
|
59
|
+
renderer.destroy();
|
|
60
|
+
|
|
61
|
+
const forceExit = setTimeout(() => process.exit(0), FORCE_EXIT_GRACE_MS);
|
|
62
|
+
if (typeof forceExit === "object" && "unref" in forceExit) {
|
|
63
|
+
forceExit.unref();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
registerShutdown(shutdown);
|
|
68
|
+
|
|
46
69
|
return renderer;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
|
-
export { App } from
|
|
50
|
-
export {
|
|
72
|
+
export { App } from "./App.tsx";
|
|
73
|
+
export { type CreateAppOptions, createAppElement } from "./createApp.tsx";
|