@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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -0
  3. package/bin/ttop.js +49 -0
  4. package/package.json +68 -0
  5. package/src/agents/aggregator.ts +176 -0
  6. package/src/agents/costing.ts +74 -0
  7. package/src/agents/index.ts +12 -0
  8. package/src/agents/types.ts +71 -0
  9. package/src/cli.ts +167 -0
  10. package/src/config/schema.test.ts +166 -0
  11. package/src/config/schema.ts +165 -0
  12. package/src/demo/simulator.ts +745 -0
  13. package/src/plugins/agents/index.ts +2 -0
  14. package/src/plugins/auth-sources.ts +186 -0
  15. package/src/plugins/lifecycle.ts +177 -0
  16. package/src/plugins/loader.test.ts +195 -0
  17. package/src/plugins/loader.ts +282 -0
  18. package/src/plugins/notification-bus.ts +153 -0
  19. package/src/plugins/notifications/index.ts +2 -0
  20. package/src/plugins/notifications/terminal-bell.ts +66 -0
  21. package/src/plugins/notifications/visual-flash.ts +57 -0
  22. package/src/plugins/npm-installer.ts +164 -0
  23. package/src/plugins/plugin-context-factory.ts +67 -0
  24. package/src/plugins/plugin-host.ts +205 -0
  25. package/src/plugins/providers/anthropic.ts +325 -0
  26. package/src/plugins/providers/antigravity.ts +309 -0
  27. package/src/plugins/providers/codex.ts +250 -0
  28. package/src/plugins/providers/gemini.ts +391 -0
  29. package/src/plugins/providers/github-copilot.ts +194 -0
  30. package/src/plugins/providers/index.ts +12 -0
  31. package/src/plugins/providers/minimax.ts +223 -0
  32. package/src/plugins/providers/openai-api.ts +250 -0
  33. package/src/plugins/providers/opencode-zen.ts +125 -0
  34. package/src/plugins/providers/perplexity.ts +177 -0
  35. package/src/plugins/providers/zai.ts +225 -0
  36. package/src/plugins/registry.ts +251 -0
  37. package/src/plugins/sandbox-guard.test.ts +191 -0
  38. package/src/plugins/sandbox-guard.ts +155 -0
  39. package/src/plugins/sandbox.ts +138 -0
  40. package/src/plugins/themes/catppuccin-latte.ts +45 -0
  41. package/src/plugins/themes/catppuccin-mocha.ts +45 -0
  42. package/src/plugins/themes/claude-code.ts +45 -0
  43. package/src/plugins/themes/dracula.ts +45 -0
  44. package/src/plugins/themes/github-light.ts +45 -0
  45. package/src/plugins/themes/gruvbox-dark.ts +45 -0
  46. package/src/plugins/themes/gruvbox-light.ts +45 -0
  47. package/src/plugins/themes/index.ts +15 -0
  48. package/src/plugins/themes/kanagawa.ts +45 -0
  49. package/src/plugins/themes/nord.ts +45 -0
  50. package/src/plugins/themes/one-dark.ts +45 -0
  51. package/src/plugins/themes/opencode.ts +47 -0
  52. package/src/plugins/themes/rose-pine-dawn.ts +45 -0
  53. package/src/plugins/themes/rose-pine.ts +45 -0
  54. package/src/plugins/themes/solarized-light.ts +45 -0
  55. package/src/plugins/themes/tokyo-night.ts +49 -0
  56. package/src/plugins/types/agent.ts +108 -0
  57. package/src/plugins/types/base.ts +267 -0
  58. package/src/plugins/types/index.ts +19 -0
  59. package/src/plugins/types/notification.ts +51 -0
  60. package/src/plugins/types/provider.ts +233 -0
  61. package/src/plugins/types/theme.ts +70 -0
  62. package/src/plugins/update-checker.ts +177 -0
  63. package/src/pricing/estimator.test.ts +181 -0
  64. package/src/pricing/estimator.ts +80 -0
  65. package/src/pricing/fallback.test.ts +71 -0
  66. package/src/pricing/fallback.ts +62 -0
  67. package/src/pricing/index.ts +66 -0
  68. package/src/pricing/models-dev.ts +130 -0
  69. package/src/storage/database.ts +613 -0
  70. package/src/storage/db.ts +64 -0
  71. package/src/storage/index.ts +66 -0
  72. package/src/storage/migrations/index.ts +189 -0
  73. package/src/storage/paths.ts +24 -0
  74. package/src/storage/repos/agentSessions.ts +210 -0
  75. package/src/storage/repos/providerSnapshots.ts +116 -0
  76. package/src/storage/repos/usageEvents.ts +334 -0
  77. package/src/storage/types.ts +288 -0
  78. package/src/tui/App.tsx +381 -0
  79. package/src/tui/components/CommandPalette.tsx +166 -0
  80. package/src/tui/components/DebugConsole.tsx +161 -0
  81. package/src/tui/components/DebugPanel.tsx +660 -0
  82. package/src/tui/components/FooterHints.tsx +22 -0
  83. package/src/tui/components/GhostProviderCard.tsx +45 -0
  84. package/src/tui/components/Header.tsx +125 -0
  85. package/src/tui/components/HelpOverlay.tsx +45 -0
  86. package/src/tui/components/InlineGauge.tsx +34 -0
  87. package/src/tui/components/InlineSparkline.tsx +78 -0
  88. package/src/tui/components/KpiStrip.tsx +246 -0
  89. package/src/tui/components/LimitGauge.tsx +186 -0
  90. package/src/tui/components/ModalBackdrop.tsx +42 -0
  91. package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
  92. package/src/tui/components/ProviderCard.tsx +309 -0
  93. package/src/tui/components/ProviderDetailPanel.tsx +237 -0
  94. package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
  95. package/src/tui/components/ProvidersList.tsx +380 -0
  96. package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
  97. package/src/tui/components/SessionsTable.tsx +401 -0
  98. package/src/tui/components/SettingsModal.tsx +587 -0
  99. package/src/tui/components/SidebarBreakdown.tsx +87 -0
  100. package/src/tui/components/Skeleton.tsx +102 -0
  101. package/src/tui/components/SmartSidebar.tsx +644 -0
  102. package/src/tui/components/Sparkline.tsx +180 -0
  103. package/src/tui/components/Spinner.tsx +36 -0
  104. package/src/tui/components/StatusBar.tsx +96 -0
  105. package/src/tui/components/ThemePicker.tsx +234 -0
  106. package/src/tui/components/Toast.tsx +61 -0
  107. package/src/tui/components/UsageGauge.tsx +80 -0
  108. package/src/tui/components/index.ts +5 -0
  109. package/src/tui/contexts/AgentSessionContext.tsx +307 -0
  110. package/src/tui/contexts/ConfigContext.tsx +141 -0
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
  112. package/src/tui/contexts/DemoModeContext.tsx +60 -0
  113. package/src/tui/contexts/DrawerContext.tsx +42 -0
  114. package/src/tui/contexts/InputContext.tsx +30 -0
  115. package/src/tui/contexts/LogContext.tsx +150 -0
  116. package/src/tui/contexts/PluginContext.tsx +424 -0
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
  118. package/src/tui/contexts/StorageContext.tsx +233 -0
  119. package/src/tui/contexts/ThemeContext.tsx +95 -0
  120. package/src/tui/contexts/TimeWindowContext.tsx +158 -0
  121. package/src/tui/contexts/ToastContext.tsx +40 -0
  122. package/src/tui/contexts/index.ts +5 -0
  123. package/src/tui/createApp.tsx +47 -0
  124. package/src/tui/debug/captureFrame.ts +262 -0
  125. package/src/tui/debug/snapshot.tsx +644 -0
  126. package/src/tui/driver/assertions.ts +279 -0
  127. package/src/tui/driver/cli.ts +717 -0
  128. package/src/tui/driver/coverage.ts +204 -0
  129. package/src/tui/driver/demo-snapshot.ts +66 -0
  130. package/src/tui/driver/diff.ts +166 -0
  131. package/src/tui/driver/driver.ts +270 -0
  132. package/src/tui/driver/index.ts +53 -0
  133. package/src/tui/driver/recorder.ts +303 -0
  134. package/src/tui/driver/test-workflow.ts +115 -0
  135. package/src/tui/driver/test.ts +47 -0
  136. package/src/tui/hooks/index.ts +20 -0
  137. package/src/tui/hooks/useAnimatedValue.ts +113 -0
  138. package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
  139. package/src/tui/hooks/useDashboardState.ts +116 -0
  140. package/src/tui/hooks/useEmaActivity.ts +223 -0
  141. package/src/tui/hooks/useEntranceAnimation.ts +62 -0
  142. package/src/tui/hooks/useExitAnimation.ts +137 -0
  143. package/src/tui/hooks/usePulse.ts +138 -0
  144. package/src/tui/hooks/useSafeRenderer.ts +24 -0
  145. package/src/tui/hooks/useValueFlash.ts +111 -0
  146. package/src/tui/index.tsx +50 -0
  147. package/src/tui/utils/providerColor.ts +32 -0
  148. package/src/tui/views/Dashboard.tsx +340 -0
  149. package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
  150. package/src/tui/views/ProjectsView.tsx +1277 -0
  151. package/src/tui/views/RealTimeDashboard.tsx +393 -0
  152. package/src/tui/views/SettingsView.tsx +465 -0
  153. package/src/tui/views/index.ts +2 -0
  154. package/src/types/global.d.ts +5 -0
  155. package/src/types/opentui-test-utils.d.ts +18 -0
  156. package/src/utils/clipboard.ts +118 -0
  157. package/src/utils/currency.test.ts +106 -0
  158. package/src/utils/currency.ts +79 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,53 @@
1
+ export {
2
+ createDriver,
3
+ type Driver,
4
+ type DriverOptions,
5
+ type CaptureResult,
6
+ type WaitOptions,
7
+ type StableOptions,
8
+ type KeyModifiers,
9
+ } from './driver.ts';
10
+
11
+ export {
12
+ diffFrames,
13
+ highlightDiff,
14
+ createCharDiff,
15
+ type DiffResult,
16
+ type LineChange,
17
+ type DiffOptions,
18
+ } from './diff.ts';
19
+
20
+ export {
21
+ assertSnapshot,
22
+ listGoldenFiles,
23
+ deleteGoldenFile,
24
+ getGoldenFile,
25
+ type AssertResult,
26
+ type AssertOptions,
27
+ type GoldenFile,
28
+ type GoldenFileInfo,
29
+ } from './assertions.ts';
30
+
31
+ export {
32
+ createRecorder,
33
+ replayRecording,
34
+ saveRecording,
35
+ loadRecording,
36
+ listRecordings,
37
+ deleteRecording,
38
+ type Recorder,
39
+ type Recording,
40
+ type RecordedCommand,
41
+ type RecorderOptions,
42
+ type ReplayOptions,
43
+ type ReplayResult,
44
+ } from './recorder.ts';
45
+
46
+ export {
47
+ createCoverageTracker,
48
+ detectViewFromFrame,
49
+ formatCoverageReport,
50
+ type CoverageTracker,
51
+ type CoverageReport,
52
+ type ViewVisit,
53
+ } from './coverage.ts';
@@ -0,0 +1,303 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import type { Driver } from './driver.ts';
4
+
5
+ export interface RecordedCommand {
6
+ action: string;
7
+ params?: Record<string, unknown>;
8
+ timestamp: number;
9
+ frameAfter?: string;
10
+ }
11
+
12
+ export interface Recording {
13
+ name: string;
14
+ createdAt: string;
15
+ width: number;
16
+ height: number;
17
+ commands: RecordedCommand[];
18
+ finalFrame?: string | undefined;
19
+ }
20
+
21
+ export interface RecorderOptions {
22
+ captureFrames?: boolean;
23
+ name?: string;
24
+ }
25
+
26
+ export interface Recorder {
27
+ start(): void;
28
+ stop(): Recording;
29
+ addCommand(action: string, params?: Record<string, unknown>): void;
30
+ captureFrame(frame: string): void;
31
+ isRecording(): boolean;
32
+ getRecording(): Recording | null;
33
+ }
34
+
35
+ export function createRecorder(
36
+ width: number,
37
+ height: number,
38
+ options: RecorderOptions = {}
39
+ ): Recorder {
40
+ const { captureFrames = false, name = `recording-${Date.now()}` } = options;
41
+
42
+ let recording = false;
43
+ let commands: RecordedCommand[] = [];
44
+ let lastFrame: string | undefined;
45
+ const startTime = Date.now();
46
+
47
+ return {
48
+ start() {
49
+ recording = true;
50
+ commands = [];
51
+ lastFrame = undefined;
52
+ },
53
+
54
+ stop() {
55
+ recording = false;
56
+ const result: Recording = {
57
+ name,
58
+ createdAt: new Date().toISOString(),
59
+ width,
60
+ height,
61
+ commands,
62
+ finalFrame: lastFrame,
63
+ };
64
+ return result;
65
+ },
66
+
67
+ addCommand(action: string, params?: Record<string, unknown>) {
68
+ if (!recording) return;
69
+
70
+ const cmd: RecordedCommand = {
71
+ action,
72
+ timestamp: Date.now() - startTime,
73
+ };
74
+
75
+ if (params && Object.keys(params).length > 0) {
76
+ cmd.params = params;
77
+ }
78
+
79
+ if (captureFrames && lastFrame) {
80
+ cmd.frameAfter = lastFrame;
81
+ }
82
+
83
+ commands.push(cmd);
84
+ },
85
+
86
+ captureFrame(frame: string) {
87
+ if (!recording) return;
88
+ lastFrame = frame;
89
+ },
90
+
91
+ isRecording() {
92
+ return recording;
93
+ },
94
+
95
+ getRecording() {
96
+ if (commands.length === 0) return null;
97
+ return {
98
+ name,
99
+ createdAt: new Date().toISOString(),
100
+ width,
101
+ height,
102
+ commands,
103
+ finalFrame: lastFrame,
104
+ };
105
+ },
106
+ };
107
+ }
108
+
109
+ export interface ReplayOptions {
110
+ speed?: number;
111
+ stepMode?: boolean;
112
+ onStep?: (
113
+ cmd: RecordedCommand,
114
+ index: number,
115
+ total: number
116
+ ) => Promise<void>;
117
+ onFrame?: (frame: string, cmd: RecordedCommand) => void;
118
+ }
119
+
120
+ export interface ReplayResult {
121
+ success: boolean;
122
+ commandsExecuted: number;
123
+ totalCommands: number;
124
+ errors: Array<{ index: number; command: RecordedCommand; error: string }>;
125
+ finalFrame?: string;
126
+ }
127
+
128
+ export async function replayRecording(
129
+ driver: Driver,
130
+ recording: Recording,
131
+ options: ReplayOptions = {}
132
+ ): Promise<ReplayResult> {
133
+ const { speed = 1, stepMode = false, onStep, onFrame } = options;
134
+
135
+ const result: ReplayResult = {
136
+ success: true,
137
+ commandsExecuted: 0,
138
+ totalCommands: recording.commands.length,
139
+ errors: [],
140
+ };
141
+
142
+ let lastTimestamp = 0;
143
+
144
+ for (let i = 0; i < recording.commands.length; i++) {
145
+ const cmd = recording.commands[i];
146
+ if (!cmd) continue;
147
+
148
+ if (stepMode && onStep) {
149
+ await onStep(cmd, i, recording.commands.length);
150
+ }
151
+
152
+ if (!stepMode && speed < Infinity && cmd.timestamp > lastTimestamp) {
153
+ const delay = (cmd.timestamp - lastTimestamp) / speed;
154
+ if (delay > 0) {
155
+ await new Promise((r) => setTimeout(r, delay));
156
+ }
157
+ }
158
+ lastTimestamp = cmd.timestamp;
159
+
160
+ try {
161
+ await executeCommand(driver, cmd);
162
+ result.commandsExecuted++;
163
+
164
+ if (onFrame) {
165
+ const frame = await driver.capture();
166
+ onFrame(frame, cmd);
167
+ }
168
+ } catch (err) {
169
+ result.success = false;
170
+ result.errors.push({
171
+ index: i,
172
+ command: cmd,
173
+ error: err instanceof Error ? err.message : String(err),
174
+ });
175
+ }
176
+ }
177
+
178
+ try {
179
+ result.finalFrame = await driver.capture();
180
+ } catch {
181
+ // Ignore capture errors
182
+ }
183
+
184
+ return result;
185
+ }
186
+
187
+ async function executeCommand(
188
+ driver: Driver,
189
+ cmd: RecordedCommand
190
+ ): Promise<void> {
191
+ const params = cmd.params ?? {};
192
+
193
+ switch (cmd.action) {
194
+ case 'sendKeys':
195
+ await driver.sendKeys(String(params.keys ?? ''));
196
+ break;
197
+
198
+ case 'pressKey':
199
+ await driver.pressKey(
200
+ String(params.key ?? ''),
201
+ params.modifiers as Record<string, boolean> | undefined
202
+ );
203
+ break;
204
+
205
+ case 'pressTab':
206
+ await driver.pressTab();
207
+ break;
208
+
209
+ case 'pressEnter':
210
+ await driver.pressEnter();
211
+ break;
212
+
213
+ case 'pressEscape':
214
+ await driver.pressEscape();
215
+ break;
216
+
217
+ case 'pressArrow':
218
+ await driver.pressArrow(params.direction as 'up' | 'down' | 'left' | 'right');
219
+ break;
220
+
221
+ case 'typeText':
222
+ await driver.typeText(
223
+ String(params.text ?? ''),
224
+ typeof params.delay === 'number' ? params.delay : 0
225
+ );
226
+ break;
227
+
228
+ case 'resize':
229
+ await driver.resize(
230
+ typeof params.cols === 'number' ? params.cols : 100,
231
+ typeof params.rows === 'number' ? params.rows : 30
232
+ );
233
+ break;
234
+
235
+ case 'waitForStable':
236
+ await driver.waitForStable({
237
+ maxIterations:
238
+ typeof params.maxIterations === 'number' ? params.maxIterations : 10,
239
+ intervalMs:
240
+ typeof params.intervalMs === 'number' ? params.intervalMs : 50,
241
+ });
242
+ break;
243
+
244
+ case 'waitForText':
245
+ await driver.waitForText(String(params.text ?? ''), {
246
+ timeout: typeof params.timeout === 'number' ? params.timeout : 5000,
247
+ });
248
+ break;
249
+
250
+ default:
251
+ // Skip unknown commands (launch, close, capture are not replayable)
252
+ break;
253
+ }
254
+ }
255
+
256
+ const DEFAULT_RECORDINGS_DIR = './recordings';
257
+
258
+ export async function saveRecording(
259
+ recording: Recording,
260
+ dir: string = DEFAULT_RECORDINGS_DIR
261
+ ): Promise<string> {
262
+ await fs.mkdir(dir, { recursive: true });
263
+ const filePath = path.join(dir, `${recording.name}.json`);
264
+ await fs.writeFile(filePath, JSON.stringify(recording, null, 2), 'utf-8');
265
+ return path.resolve(filePath);
266
+ }
267
+
268
+ export async function loadRecording(
269
+ name: string,
270
+ dir: string = DEFAULT_RECORDINGS_DIR
271
+ ): Promise<Recording | null> {
272
+ const filePath = path.join(dir, `${name}.json`);
273
+ try {
274
+ const content = await fs.readFile(filePath, 'utf-8');
275
+ return JSON.parse(content) as Recording;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ export async function listRecordings(
282
+ dir: string = DEFAULT_RECORDINGS_DIR
283
+ ): Promise<string[]> {
284
+ try {
285
+ const files = await fs.readdir(dir);
286
+ return files.filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', ''));
287
+ } catch {
288
+ return [];
289
+ }
290
+ }
291
+
292
+ export async function deleteRecording(
293
+ name: string,
294
+ dir: string = DEFAULT_RECORDINGS_DIR
295
+ ): Promise<boolean> {
296
+ const filePath = path.join(dir, `${name}.json`);
297
+ try {
298
+ await fs.unlink(filePath);
299
+ return true;
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bun
2
+ import { createDriver } from './driver.ts';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+
6
+ const SNAPSHOTS_DIR = './snapshots';
7
+
8
+ async function saveSnapshot(frame: string, name: string): Promise<string> {
9
+ await fs.mkdir(SNAPSHOTS_DIR, { recursive: true });
10
+ const filePath = path.join(SNAPSHOTS_DIR, `${name}.txt`);
11
+ await fs.writeFile(filePath, frame, 'utf-8');
12
+ return path.resolve(filePath);
13
+ }
14
+
15
+ async function main() {
16
+ // Use 80x24 to match your terminal
17
+ const driver = await createDriver({ width: 80, height: 24 });
18
+
19
+ console.log('Launching app...');
20
+ await driver.launch();
21
+ await driver.waitForStable();
22
+
23
+ // Step 1: Navigate to Settings (key 5)
24
+ console.log('Navigating to Settings...');
25
+ await driver.pressKey('5');
26
+ await driver.waitForStable();
27
+
28
+ // Step 2: Find and capture Alerts settings
29
+ // Settings page likely has tabs/sections - need to find Alerts
30
+ console.log('Looking for Alerts section...');
31
+ let frame = await driver.capture();
32
+ await saveSnapshot(frame, 'workflow-01-settings');
33
+ console.log('Saved settings view');
34
+
35
+ // Try navigating within settings to find Alerts (try arrow keys or tab)
36
+ // First let's see what's on the settings page
37
+ if (!frame.includes('Alerts') && !frame.includes('alerts')) {
38
+ // Try pressing tab or arrows to navigate
39
+ await driver.pressTab();
40
+ await driver.waitForStable();
41
+ frame = await driver.capture();
42
+ }
43
+
44
+ await saveSnapshot(frame, 'workflow-02-alerts-settings');
45
+ console.log('Saved alerts settings');
46
+
47
+ // Step 3: Navigate back to Dashboard (key 1)
48
+ console.log('Navigating to Dashboard...');
49
+ await driver.pressKey('1');
50
+ await driver.waitForStable();
51
+ frame = await driver.capture();
52
+ await saveSnapshot(frame, 'workflow-03-dashboard');
53
+ console.log('Saved dashboard view');
54
+
55
+ // Step 4: Change time scale to 7d
56
+ // Need to find how to change timescale - likely a dropdown or key
57
+ console.log('Changing time scale to 7d...');
58
+ // Try common keys for time scale selection
59
+ // Check if there's a visible control for this
60
+
61
+ // Try 't' for time or look for other controls
62
+ await driver.pressKey('t');
63
+ await driver.waitForStable();
64
+ frame = await driver.capture();
65
+ await saveSnapshot(frame, 'workflow-04-timescale-menu');
66
+
67
+ // If a menu opened, try to select 7d
68
+ if (frame.includes('7d') || frame.includes('7 d')) {
69
+ // Navigate to 7d option
70
+ await driver.pressArrow('down');
71
+ await driver.waitForStable();
72
+ await driver.pressArrow('down');
73
+ await driver.waitForStable();
74
+ frame = await driver.capture();
75
+ await saveSnapshot(frame, 'workflow-05-timescale-selecting');
76
+ await driver.pressEnter();
77
+ await driver.waitForStable();
78
+ }
79
+
80
+ frame = await driver.capture();
81
+ await saveSnapshot(frame, 'workflow-06-after-timescale');
82
+ console.log('Saved after timescale change');
83
+
84
+ // Step 5: Filter with "opus"
85
+ console.log('Filtering with "opus"...');
86
+ // Try '/' for search/filter (common pattern)
87
+ await driver.pressKey('/');
88
+ await driver.waitForStable();
89
+ frame = await driver.capture();
90
+ await saveSnapshot(frame, 'workflow-07-filter-input');
91
+
92
+ // Type "opus"
93
+ await driver.typeText('opus');
94
+ await driver.waitForStable();
95
+ frame = await driver.capture();
96
+ await saveSnapshot(frame, 'workflow-08-filter-opus');
97
+
98
+ // Press enter to apply filter
99
+ await driver.pressEnter();
100
+ await driver.waitForStable();
101
+
102
+ // Final result
103
+ frame = await driver.capture();
104
+ await saveSnapshot(frame, 'workflow-09-final-result');
105
+ console.log('Saved final filtered result');
106
+
107
+ await driver.close();
108
+
109
+ console.log('\n✓ Workflow complete! Check ./snapshots/workflow-*.txt');
110
+ }
111
+
112
+ main().catch((err) => {
113
+ console.error('Workflow failed:', err);
114
+ process.exit(1);
115
+ });
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env bun
2
+ import { createDriver } from './driver.ts';
3
+
4
+ async function main() {
5
+ console.log('Creating driver...');
6
+ const driver = await createDriver({ width: 100, height: 30 });
7
+
8
+ console.log('Launching app...');
9
+ await driver.launch();
10
+
11
+ console.log('Waiting for initial render...');
12
+ await driver.waitForStable();
13
+
14
+ console.log('Capturing initial frame...');
15
+ const frame1 = await driver.capture();
16
+ console.log('=== INITIAL FRAME ===');
17
+ console.log(frame1);
18
+ console.log('=== END FRAME ===\n');
19
+
20
+ console.log('Pressing "2" to switch to Providers view...');
21
+ await driver.pressKey('2');
22
+ await driver.waitForStable();
23
+
24
+ const frame2 = await driver.capture();
25
+ console.log('=== PROVIDERS VIEW ===');
26
+ console.log(frame2);
27
+ console.log('=== END FRAME ===\n');
28
+
29
+ console.log('Pressing "3" to switch to Trends view...');
30
+ await driver.pressKey('3');
31
+ await driver.waitForStable();
32
+
33
+ const frame3 = await driver.capture();
34
+ console.log('=== TRENDS VIEW ===');
35
+ console.log(frame3);
36
+ console.log('=== END FRAME ===\n');
37
+
38
+ console.log('Closing driver...');
39
+ await driver.close();
40
+
41
+ console.log('Done!');
42
+ }
43
+
44
+ main().catch((err) => {
45
+ console.error('Test failed:', err);
46
+ process.exit(1);
47
+ });
@@ -0,0 +1,20 @@
1
+ export { useEmaActivity } from './useEmaActivity.ts';
2
+ export type { ActivityState, ActivityDebugData, UseActivityRateResult } from './useEmaActivity.ts';
3
+
4
+ export { useDashboardState } from './useDashboardState.ts';
5
+ export type { DashboardDeltas, UseDashboardStateResult } from './useDashboardState.ts';
6
+
7
+ export { usePulse, getPulseColor } from './usePulse.ts';
8
+ export type { UsePulseOptions } from './usePulse.ts';
9
+
10
+ export { useValueFlash, interpolateColor } from './useValueFlash.ts';
11
+ export type { UseValueFlashOptions, UseValueFlashResult } from './useValueFlash.ts';
12
+
13
+ export { useAnimatedValue, useAnimatedCurrency, useAnimatedTokens, useAnimatedCount, easings } from './useAnimatedValue.ts';
14
+ export type { UseAnimatedValueOptions, EasingFunction } from './useAnimatedValue.ts';
15
+
16
+ export { useEntranceAnimation, applyEntranceFade } from './useEntranceAnimation.ts';
17
+ export type { UseEntranceAnimationOptions } from './useEntranceAnimation.ts';
18
+
19
+ export { useExitAnimation } from './useExitAnimation.ts';
20
+ export type { ExitAnimationItem, UseExitAnimationOptions, UseExitAnimationResult } from './useExitAnimation.ts';
@@ -0,0 +1,113 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+
3
+ export type EasingFunction = (t: number) => number;
4
+
5
+ export const easings = {
6
+ linear: (t: number) => t,
7
+ easeOutQuad: (t: number) => 1 - (1 - t) * (1 - t),
8
+ easeOutCubic: (t: number) => 1 - Math.pow(1 - t, 3),
9
+ easeOutExpo: (t: number) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
10
+ easeInOutQuad: (t: number) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
11
+ } as const;
12
+
13
+ export interface UseAnimatedValueOptions {
14
+ durationMs?: number;
15
+ easing?: keyof typeof easings | EasingFunction;
16
+ precision?: number;
17
+ frameRate?: number;
18
+ }
19
+
20
+ export function useAnimatedValue(
21
+ targetValue: number,
22
+ options: UseAnimatedValueOptions = {}
23
+ ): number {
24
+ const {
25
+ durationMs = 300,
26
+ easing = 'easeOutQuad',
27
+ precision = 2,
28
+ frameRate = 60,
29
+ } = options;
30
+
31
+ const [displayValue, setDisplayValue] = useState(targetValue);
32
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
33
+ const startValueRef = useRef(targetValue);
34
+ const startTimeRef = useRef<number>(Date.now());
35
+ const targetRef = useRef(targetValue);
36
+
37
+ const easingFn = typeof easing === 'function' ? easing : easings[easing];
38
+ const frameInterval = Math.round(1000 / frameRate);
39
+
40
+ useEffect(() => {
41
+ if (targetRef.current === targetValue && displayValue === targetValue) {
42
+ return;
43
+ }
44
+
45
+ if (intervalRef.current !== null) {
46
+ clearInterval(intervalRef.current);
47
+ }
48
+
49
+ startValueRef.current = displayValue;
50
+ startTimeRef.current = Date.now();
51
+ targetRef.current = targetValue;
52
+
53
+ intervalRef.current = setInterval(() => {
54
+ const elapsed = Date.now() - startTimeRef.current;
55
+ const progress = Math.min(elapsed / durationMs, 1);
56
+ const easedProgress = easingFn(progress);
57
+
58
+ const startVal = startValueRef.current;
59
+ const currentValue = startVal + (targetRef.current - startVal) * easedProgress;
60
+ const roundedValue = Number(currentValue.toFixed(precision));
61
+
62
+ setDisplayValue(roundedValue);
63
+
64
+ if (progress >= 1) {
65
+ if (intervalRef.current !== null) {
66
+ clearInterval(intervalRef.current);
67
+ intervalRef.current = null;
68
+ }
69
+ setDisplayValue(targetRef.current);
70
+ }
71
+ }, frameInterval);
72
+
73
+ return () => {
74
+ if (intervalRef.current !== null) {
75
+ clearInterval(intervalRef.current);
76
+ intervalRef.current = null;
77
+ }
78
+ };
79
+ }, [targetValue, durationMs, easingFn, precision, frameInterval, displayValue]);
80
+
81
+ return displayValue;
82
+ }
83
+
84
+ export function useAnimatedCurrency(
85
+ targetValue: number,
86
+ options: Omit<UseAnimatedValueOptions, 'precision'> = {}
87
+ ): string {
88
+ const animatedValue = useAnimatedValue(targetValue, { ...options, precision: 2 });
89
+ return `$${animatedValue.toFixed(2)}`;
90
+ }
91
+
92
+ export function useAnimatedTokens(
93
+ targetValue: number,
94
+ options: Omit<UseAnimatedValueOptions, 'precision'> = {}
95
+ ): string {
96
+ const animatedValue = useAnimatedValue(targetValue, { ...options, precision: 0 });
97
+
98
+ if (animatedValue >= 1_000_000) {
99
+ return `${(animatedValue / 1_000_000).toFixed(1)}M`;
100
+ }
101
+ if (animatedValue >= 1_000) {
102
+ return `${(animatedValue / 1_000).toFixed(1)}K`;
103
+ }
104
+ return Math.round(animatedValue).toLocaleString();
105
+ }
106
+
107
+ export function useAnimatedCount(
108
+ targetValue: number,
109
+ options: Omit<UseAnimatedValueOptions, 'precision'> = {}
110
+ ): string {
111
+ const animatedValue = useAnimatedValue(targetValue, { ...options, precision: 0 });
112
+ return Math.round(animatedValue).toLocaleString();
113
+ }