@tokentop/ttop 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/ttop.js +49 -0
- package/package.json +68 -0
- package/src/agents/aggregator.ts +176 -0
- package/src/agents/costing.ts +74 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/types.ts +71 -0
- package/src/cli.ts +167 -0
- package/src/config/schema.test.ts +166 -0
- package/src/config/schema.ts +165 -0
- package/src/demo/simulator.ts +745 -0
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +186 -0
- package/src/plugins/lifecycle.ts +177 -0
- package/src/plugins/loader.test.ts +195 -0
- package/src/plugins/loader.ts +282 -0
- package/src/plugins/notification-bus.ts +153 -0
- package/src/plugins/notifications/index.ts +2 -0
- package/src/plugins/notifications/terminal-bell.ts +66 -0
- package/src/plugins/notifications/visual-flash.ts +57 -0
- package/src/plugins/npm-installer.ts +164 -0
- package/src/plugins/plugin-context-factory.ts +67 -0
- package/src/plugins/plugin-host.ts +205 -0
- package/src/plugins/providers/anthropic.ts +325 -0
- package/src/plugins/providers/antigravity.ts +309 -0
- package/src/plugins/providers/codex.ts +250 -0
- package/src/plugins/providers/gemini.ts +391 -0
- package/src/plugins/providers/github-copilot.ts +194 -0
- package/src/plugins/providers/index.ts +12 -0
- package/src/plugins/providers/minimax.ts +223 -0
- package/src/plugins/providers/openai-api.ts +250 -0
- package/src/plugins/providers/opencode-zen.ts +125 -0
- package/src/plugins/providers/perplexity.ts +177 -0
- package/src/plugins/providers/zai.ts +225 -0
- package/src/plugins/registry.ts +251 -0
- package/src/plugins/sandbox-guard.test.ts +191 -0
- package/src/plugins/sandbox-guard.ts +155 -0
- package/src/plugins/sandbox.ts +138 -0
- package/src/plugins/themes/catppuccin-latte.ts +45 -0
- package/src/plugins/themes/catppuccin-mocha.ts +45 -0
- package/src/plugins/themes/claude-code.ts +45 -0
- package/src/plugins/themes/dracula.ts +45 -0
- package/src/plugins/themes/github-light.ts +45 -0
- package/src/plugins/themes/gruvbox-dark.ts +45 -0
- package/src/plugins/themes/gruvbox-light.ts +45 -0
- package/src/plugins/themes/index.ts +15 -0
- package/src/plugins/themes/kanagawa.ts +45 -0
- package/src/plugins/themes/nord.ts +45 -0
- package/src/plugins/themes/one-dark.ts +45 -0
- package/src/plugins/themes/opencode.ts +47 -0
- package/src/plugins/themes/rose-pine-dawn.ts +45 -0
- package/src/plugins/themes/rose-pine.ts +45 -0
- package/src/plugins/themes/solarized-light.ts +45 -0
- package/src/plugins/themes/tokyo-night.ts +49 -0
- package/src/plugins/types/agent.ts +108 -0
- package/src/plugins/types/base.ts +267 -0
- package/src/plugins/types/index.ts +19 -0
- package/src/plugins/types/notification.ts +51 -0
- package/src/plugins/types/provider.ts +233 -0
- package/src/plugins/types/theme.ts +70 -0
- package/src/plugins/update-checker.ts +177 -0
- package/src/pricing/estimator.test.ts +181 -0
- package/src/pricing/estimator.ts +80 -0
- package/src/pricing/fallback.test.ts +71 -0
- package/src/pricing/fallback.ts +62 -0
- package/src/pricing/index.ts +66 -0
- package/src/pricing/models-dev.ts +130 -0
- package/src/storage/database.ts +613 -0
- package/src/storage/db.ts +64 -0
- package/src/storage/index.ts +66 -0
- package/src/storage/migrations/index.ts +189 -0
- package/src/storage/paths.ts +24 -0
- package/src/storage/repos/agentSessions.ts +210 -0
- package/src/storage/repos/providerSnapshots.ts +116 -0
- package/src/storage/repos/usageEvents.ts +334 -0
- package/src/storage/types.ts +288 -0
- package/src/tui/App.tsx +381 -0
- package/src/tui/components/CommandPalette.tsx +166 -0
- package/src/tui/components/DebugConsole.tsx +161 -0
- package/src/tui/components/DebugPanel.tsx +660 -0
- package/src/tui/components/FooterHints.tsx +22 -0
- package/src/tui/components/GhostProviderCard.tsx +45 -0
- package/src/tui/components/Header.tsx +125 -0
- package/src/tui/components/HelpOverlay.tsx +45 -0
- package/src/tui/components/InlineGauge.tsx +34 -0
- package/src/tui/components/InlineSparkline.tsx +78 -0
- package/src/tui/components/KpiStrip.tsx +246 -0
- package/src/tui/components/LimitGauge.tsx +186 -0
- package/src/tui/components/ModalBackdrop.tsx +42 -0
- package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
- package/src/tui/components/ProviderCard.tsx +309 -0
- package/src/tui/components/ProviderDetailPanel.tsx +237 -0
- package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
- package/src/tui/components/ProvidersList.tsx +380 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
- package/src/tui/components/SessionsTable.tsx +401 -0
- package/src/tui/components/SettingsModal.tsx +587 -0
- package/src/tui/components/SidebarBreakdown.tsx +87 -0
- package/src/tui/components/Skeleton.tsx +102 -0
- package/src/tui/components/SmartSidebar.tsx +644 -0
- package/src/tui/components/Sparkline.tsx +180 -0
- package/src/tui/components/Spinner.tsx +36 -0
- package/src/tui/components/StatusBar.tsx +96 -0
- package/src/tui/components/ThemePicker.tsx +234 -0
- package/src/tui/components/Toast.tsx +61 -0
- package/src/tui/components/UsageGauge.tsx +80 -0
- package/src/tui/components/index.ts +5 -0
- package/src/tui/contexts/AgentSessionContext.tsx +307 -0
- package/src/tui/contexts/ConfigContext.tsx +141 -0
- package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
- package/src/tui/contexts/DemoModeContext.tsx +60 -0
- package/src/tui/contexts/DrawerContext.tsx +42 -0
- package/src/tui/contexts/InputContext.tsx +30 -0
- package/src/tui/contexts/LogContext.tsx +150 -0
- package/src/tui/contexts/PluginContext.tsx +424 -0
- package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
- package/src/tui/contexts/StorageContext.tsx +233 -0
- package/src/tui/contexts/ThemeContext.tsx +95 -0
- package/src/tui/contexts/TimeWindowContext.tsx +158 -0
- package/src/tui/contexts/ToastContext.tsx +40 -0
- package/src/tui/contexts/index.ts +5 -0
- package/src/tui/createApp.tsx +47 -0
- package/src/tui/debug/captureFrame.ts +262 -0
- package/src/tui/debug/snapshot.tsx +644 -0
- package/src/tui/driver/assertions.ts +279 -0
- package/src/tui/driver/cli.ts +717 -0
- package/src/tui/driver/coverage.ts +204 -0
- package/src/tui/driver/demo-snapshot.ts +66 -0
- package/src/tui/driver/diff.ts +166 -0
- package/src/tui/driver/driver.ts +270 -0
- package/src/tui/driver/index.ts +53 -0
- package/src/tui/driver/recorder.ts +303 -0
- package/src/tui/driver/test-workflow.ts +115 -0
- package/src/tui/driver/test.ts +47 -0
- package/src/tui/hooks/index.ts +20 -0
- package/src/tui/hooks/useAnimatedValue.ts +113 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
- package/src/tui/hooks/useDashboardState.ts +116 -0
- package/src/tui/hooks/useEmaActivity.ts +223 -0
- package/src/tui/hooks/useEntranceAnimation.ts +62 -0
- package/src/tui/hooks/useExitAnimation.ts +137 -0
- package/src/tui/hooks/usePulse.ts +138 -0
- package/src/tui/hooks/useSafeRenderer.ts +24 -0
- package/src/tui/hooks/useValueFlash.ts +111 -0
- package/src/tui/index.tsx +50 -0
- package/src/tui/utils/providerColor.ts +32 -0
- package/src/tui/views/Dashboard.tsx +340 -0
- package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
- package/src/tui/views/ProjectsView.tsx +1277 -0
- package/src/tui/views/RealTimeDashboard.tsx +393 -0
- package/src/tui/views/SettingsView.tsx +465 -0
- package/src/tui/views/index.ts +2 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/opentui-test-utils.d.ts +18 -0
- package/src/utils/clipboard.ts +118 -0
- package/src/utils/currency.test.ts +106 -0
- package/src/utils/currency.ts +79 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,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
|
+
}
|