@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
|
@@ -23,17 +23,9 @@ export interface CoverageTracker {
|
|
|
23
23
|
isTracking(): boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const KNOWN_VIEWS = [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'Trends',
|
|
30
|
-
'Projects',
|
|
31
|
-
'Settings',
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
export function createCoverageTracker(
|
|
35
|
-
knownViews: string[] = KNOWN_VIEWS
|
|
36
|
-
): CoverageTracker {
|
|
26
|
+
const KNOWN_VIEWS = ["Dashboard", "Providers", "Trends", "Projects", "Settings"];
|
|
27
|
+
|
|
28
|
+
export function createCoverageTracker(knownViews: string[] = KNOWN_VIEWS): CoverageTracker {
|
|
37
29
|
let tracking = false;
|
|
38
30
|
let sessionStart = 0;
|
|
39
31
|
let currentView: string | null = null;
|
|
@@ -45,9 +37,7 @@ export function createCoverageTracker(
|
|
|
45
37
|
const uniqueViews = [...new Set(viewsVisited.map((v) => v.view))];
|
|
46
38
|
const coveragePercentage =
|
|
47
39
|
knownViews.length > 0
|
|
48
|
-
? (uniqueViews.filter((v) => knownViews.includes(v)).length /
|
|
49
|
-
knownViews.length) *
|
|
50
|
-
100
|
|
40
|
+
? (uniqueViews.filter((v) => knownViews.includes(v)).length / knownViews.length) * 100
|
|
51
41
|
: undefined;
|
|
52
42
|
|
|
53
43
|
return {
|
|
@@ -68,7 +58,9 @@ export function createCoverageTracker(
|
|
|
68
58
|
currentView = null;
|
|
69
59
|
currentViewStart = 0;
|
|
70
60
|
viewsVisited.length = 0;
|
|
71
|
-
Object.keys(viewCounts)
|
|
61
|
+
for (const k of Object.keys(viewCounts)) {
|
|
62
|
+
delete viewCounts[k];
|
|
63
|
+
}
|
|
72
64
|
},
|
|
73
65
|
|
|
74
66
|
stop() {
|
|
@@ -116,26 +108,23 @@ export function createCoverageTracker(
|
|
|
116
108
|
}
|
|
117
109
|
|
|
118
110
|
export function detectViewFromFrame(frame: string): string | null {
|
|
119
|
-
const lines = frame.split(
|
|
120
|
-
const headerArea = lines.join(
|
|
111
|
+
const lines = frame.split("\n").slice(0, 5);
|
|
112
|
+
const headerArea = lines.join("\n");
|
|
121
113
|
|
|
122
|
-
if (headerArea.includes(
|
|
114
|
+
if (headerArea.includes("[1] Dashboard") && headerArea.includes("│")) {
|
|
123
115
|
const match = headerArea.match(/\[(\d)\] (\w+)/g);
|
|
124
116
|
if (match) {
|
|
125
117
|
for (const m of match) {
|
|
126
|
-
if (!m.includes(
|
|
118
|
+
if (!m.includes("[")) continue;
|
|
127
119
|
const viewMatch = m.match(/\[(\d)\] (\w+)/);
|
|
128
120
|
if (viewMatch) {
|
|
129
121
|
const num = viewMatch[1];
|
|
130
122
|
const name = viewMatch[2];
|
|
131
123
|
if (
|
|
132
124
|
headerArea.includes(`[${num}]`) &&
|
|
133
|
-
frame.toLowerCase().includes(name?.toLowerCase() ??
|
|
125
|
+
frame.toLowerCase().includes(name?.toLowerCase() ?? "")
|
|
134
126
|
) {
|
|
135
|
-
if (
|
|
136
|
-
headerArea.includes(`[${num}] ${name}`) &&
|
|
137
|
-
!headerArea.includes(`│ ${name}`)
|
|
138
|
-
) {
|
|
127
|
+
if (headerArea.includes(`[${num}] ${name}`) && !headerArea.includes(`│ ${name}`)) {
|
|
139
128
|
return name ?? null;
|
|
140
129
|
}
|
|
141
130
|
}
|
|
@@ -163,42 +152,36 @@ export function detectViewFromFrame(frame: string): string | null {
|
|
|
163
152
|
|
|
164
153
|
export function formatCoverageReport(report: CoverageReport): string {
|
|
165
154
|
const lines: string[] = [
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
155
|
+
"╔══════════════════════════════════════════════════════════════════╗",
|
|
156
|
+
"║ COVERAGE REPORT ║",
|
|
157
|
+
"╠══════════════════════════════════════════════════════════════════╣",
|
|
169
158
|
];
|
|
170
159
|
|
|
171
160
|
const duration = (report.sessionEnd ?? Date.now()) - report.sessionStart;
|
|
172
161
|
const durationSec = (duration / 1000).toFixed(1);
|
|
173
162
|
|
|
174
|
-
lines.push(
|
|
175
|
-
`║ Session Duration: ${durationSec}s`.padEnd(67) + '║'
|
|
176
|
-
);
|
|
163
|
+
lines.push(`║ Session Duration: ${durationSec}s`.padEnd(67) + "║");
|
|
177
164
|
lines.push(
|
|
178
165
|
`║ Views Visited: ${report.uniqueViews.length} unique, ${report.totalViewChanges} total`.padEnd(
|
|
179
|
-
67
|
|
180
|
-
) +
|
|
166
|
+
67,
|
|
167
|
+
) + "║",
|
|
181
168
|
);
|
|
182
169
|
|
|
183
170
|
if (report.coveragePercentage !== undefined) {
|
|
184
|
-
lines.push(
|
|
185
|
-
`║ Coverage: ${report.coveragePercentage.toFixed(0)}%`.padEnd(67) + '║'
|
|
186
|
-
);
|
|
171
|
+
lines.push(`║ Coverage: ${report.coveragePercentage.toFixed(0)}%`.padEnd(67) + "║");
|
|
187
172
|
}
|
|
188
173
|
|
|
189
|
-
lines.push(
|
|
190
|
-
lines.push(
|
|
174
|
+
lines.push("╠══════════════════════════════════════════════════════════════════╣");
|
|
175
|
+
lines.push("║ View Breakdown: ║");
|
|
191
176
|
|
|
192
|
-
for (const [view, count] of Object.entries(report.viewCounts).sort(
|
|
193
|
-
(a, b) => b[1] - a[1]
|
|
194
|
-
)) {
|
|
177
|
+
for (const [view, count] of Object.entries(report.viewCounts).sort((a, b) => b[1] - a[1])) {
|
|
195
178
|
const visits = report.viewsVisited.filter((v) => v.view === view);
|
|
196
179
|
const totalTime = visits.reduce((sum, v) => sum + (v.duration ?? 0), 0);
|
|
197
|
-
const timeStr = totalTime > 0 ? ` (${(totalTime / 1000).toFixed(1)}s)` :
|
|
198
|
-
lines.push(`║ ${view}: ${count}x${timeStr}`.padEnd(67) +
|
|
180
|
+
const timeStr = totalTime > 0 ? ` (${(totalTime / 1000).toFixed(1)}s)` : "";
|
|
181
|
+
lines.push(`║ ${view}: ${count}x${timeStr}`.padEnd(67) + "║");
|
|
199
182
|
}
|
|
200
183
|
|
|
201
|
-
lines.push(
|
|
184
|
+
lines.push("╚══════════════════════════════════════════════════════════════════╝");
|
|
202
185
|
|
|
203
|
-
return lines.join(
|
|
186
|
+
return lines.join("\n");
|
|
204
187
|
}
|
|
@@ -1,66 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { createDriver } from "./driver.ts";
|
|
5
5
|
|
|
6
|
-
const SNAPSHOTS_DIR =
|
|
6
|
+
const SNAPSHOTS_DIR = "./snapshots";
|
|
7
7
|
|
|
8
8
|
async function saveSnapshot(frame: string, name: string): Promise<string> {
|
|
9
9
|
await fs.mkdir(SNAPSHOTS_DIR, { recursive: true });
|
|
10
10
|
const filePath = path.join(SNAPSHOTS_DIR, `${name}.txt`);
|
|
11
|
-
await fs.writeFile(filePath, frame,
|
|
11
|
+
await fs.writeFile(filePath, frame, "utf-8");
|
|
12
12
|
return path.resolve(filePath);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async function main() {
|
|
16
|
-
console.log(
|
|
16
|
+
console.log("Creating driver...");
|
|
17
17
|
const driver = await createDriver({ width: 100, height: 30 });
|
|
18
|
-
|
|
19
|
-
console.log(
|
|
18
|
+
|
|
19
|
+
console.log("Launching app...");
|
|
20
20
|
await driver.launch();
|
|
21
21
|
await driver.waitForStable();
|
|
22
|
-
|
|
23
|
-
console.log(
|
|
22
|
+
|
|
23
|
+
console.log("Capturing dashboard...");
|
|
24
24
|
const dashboardFrame = await driver.capture();
|
|
25
|
-
const dashboardPath = await saveSnapshot(dashboardFrame,
|
|
25
|
+
const dashboardPath = await saveSnapshot(dashboardFrame, "01-dashboard");
|
|
26
26
|
console.log(`Saved: ${dashboardPath}`);
|
|
27
|
-
|
|
28
|
-
console.log(
|
|
29
|
-
await driver.pressKey(
|
|
27
|
+
|
|
28
|
+
console.log("Switching to Providers view...");
|
|
29
|
+
await driver.pressKey("2");
|
|
30
30
|
await driver.waitForStable();
|
|
31
31
|
const providersFrame = await driver.capture();
|
|
32
|
-
const providersPath = await saveSnapshot(providersFrame,
|
|
32
|
+
const providersPath = await saveSnapshot(providersFrame, "02-providers");
|
|
33
33
|
console.log(`Saved: ${providersPath}`);
|
|
34
|
-
|
|
35
|
-
console.log(
|
|
36
|
-
await driver.pressKey(
|
|
34
|
+
|
|
35
|
+
console.log("Switching to Trends view...");
|
|
36
|
+
await driver.pressKey("3");
|
|
37
37
|
await driver.waitForStable();
|
|
38
38
|
const trendsFrame = await driver.capture();
|
|
39
|
-
const trendsPath = await saveSnapshot(trendsFrame,
|
|
39
|
+
const trendsPath = await saveSnapshot(trendsFrame, "03-trends");
|
|
40
40
|
console.log(`Saved: ${trendsPath}`);
|
|
41
|
-
|
|
42
|
-
console.log(
|
|
43
|
-
await driver.pressKey(
|
|
41
|
+
|
|
42
|
+
console.log("Switching to Projects view...");
|
|
43
|
+
await driver.pressKey("4");
|
|
44
44
|
await driver.waitForStable();
|
|
45
45
|
const projectsFrame = await driver.capture();
|
|
46
|
-
const projectsPath = await saveSnapshot(projectsFrame,
|
|
46
|
+
const projectsPath = await saveSnapshot(projectsFrame, "04-projects");
|
|
47
47
|
console.log(`Saved: ${projectsPath}`);
|
|
48
|
-
|
|
49
|
-
console.log(
|
|
50
|
-
await driver.pressKey(
|
|
48
|
+
|
|
49
|
+
console.log("Switching to Settings view...");
|
|
50
|
+
await driver.pressKey("5");
|
|
51
51
|
await driver.waitForStable();
|
|
52
52
|
const settingsFrame = await driver.capture();
|
|
53
|
-
const settingsPath = await saveSnapshot(settingsFrame,
|
|
53
|
+
const settingsPath = await saveSnapshot(settingsFrame, "05-settings");
|
|
54
54
|
console.log(`Saved: ${settingsPath}`);
|
|
55
|
-
|
|
56
|
-
console.log(
|
|
55
|
+
|
|
56
|
+
console.log("Closing driver...");
|
|
57
57
|
await driver.close();
|
|
58
|
-
|
|
59
|
-
console.log(
|
|
60
|
-
console.log(
|
|
58
|
+
|
|
59
|
+
console.log("\n✓ All snapshots saved to ./snapshots/");
|
|
60
|
+
console.log(" You can view them with: cat snapshots/01-dashboard.txt");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
main().catch((err) => {
|
|
64
|
-
console.error(
|
|
64
|
+
console.error("Demo failed:", err);
|
|
65
65
|
process.exit(1);
|
|
66
66
|
});
|
package/src/tui/driver/diff.ts
CHANGED
|
@@ -23,15 +23,14 @@ export interface DiffOptions {
|
|
|
23
23
|
export function diffFrames(
|
|
24
24
|
expected: string,
|
|
25
25
|
actual: string,
|
|
26
|
-
options: DiffOptions = {}
|
|
26
|
+
options: DiffOptions = {},
|
|
27
27
|
): DiffResult {
|
|
28
28
|
const { ignoreWhitespace = false, contextLines = 2 } = options;
|
|
29
29
|
|
|
30
|
-
const normalize = (s: string) =>
|
|
31
|
-
ignoreWhitespace ? s.replace(/\s+/g, ' ').trim() : s;
|
|
30
|
+
const normalize = (s: string) => (ignoreWhitespace ? s.replace(/\s+/g, " ").trim() : s);
|
|
32
31
|
|
|
33
|
-
const expectedLines = expected.split(
|
|
34
|
-
const actualLines = actual.split(
|
|
32
|
+
const expectedLines = expected.split("\n");
|
|
33
|
+
const actualLines = actual.split("\n");
|
|
35
34
|
|
|
36
35
|
const additions: LineChange[] = [];
|
|
37
36
|
const deletions: LineChange[] = [];
|
|
@@ -42,8 +41,8 @@ export function diffFrames(
|
|
|
42
41
|
const changedLineNumbers = new Set<number>();
|
|
43
42
|
|
|
44
43
|
for (let i = 0; i < maxLines; i++) {
|
|
45
|
-
const expLine = expectedLines[i] ??
|
|
46
|
-
const actLine = actualLines[i] ??
|
|
44
|
+
const expLine = expectedLines[i] ?? "";
|
|
45
|
+
const actLine = actualLines[i] ?? "";
|
|
47
46
|
const expNorm = normalize(expLine);
|
|
48
47
|
const actNorm = normalize(actLine);
|
|
49
48
|
|
|
@@ -65,11 +64,11 @@ export function diffFrames(
|
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
for (let i = 0; i < maxLines; i++) {
|
|
68
|
-
const expLine = expectedLines[i] ??
|
|
69
|
-
const actLine = actualLines[i] ??
|
|
67
|
+
const expLine = expectedLines[i] ?? "";
|
|
68
|
+
const actLine = actualLines[i] ?? "";
|
|
70
69
|
|
|
71
70
|
const isInContext = Array.from(changedLineNumbers).some(
|
|
72
|
-
(changed) => Math.abs(changed - i) <= contextLines
|
|
71
|
+
(changed) => Math.abs(changed - i) <= contextLines,
|
|
73
72
|
);
|
|
74
73
|
|
|
75
74
|
if (changedLineNumbers.has(i)) {
|
|
@@ -89,8 +88,7 @@ export function diffFrames(
|
|
|
89
88
|
const identical = changedLineNumbers.size === 0;
|
|
90
89
|
const changedLines = changedLineNumbers.size;
|
|
91
90
|
const totalLines = maxLines;
|
|
92
|
-
const changePercentage =
|
|
93
|
-
totalLines > 0 ? (changedLines / totalLines) * 100 : 0;
|
|
91
|
+
const changePercentage = totalLines > 0 ? (changedLines / totalLines) * 100 : 0;
|
|
94
92
|
|
|
95
93
|
return {
|
|
96
94
|
identical,
|
|
@@ -100,57 +98,53 @@ export function diffFrames(
|
|
|
100
98
|
additions,
|
|
101
99
|
deletions,
|
|
102
100
|
modifications,
|
|
103
|
-
visualDiff: diffLines.join(
|
|
101
|
+
visualDiff: diffLines.join("\n"),
|
|
104
102
|
};
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
export function highlightDiff(
|
|
108
|
-
expected: string,
|
|
109
|
-
actual: string,
|
|
110
|
-
options: DiffOptions = {}
|
|
111
|
-
): string {
|
|
105
|
+
export function highlightDiff(expected: string, actual: string, options: DiffOptions = {}): string {
|
|
112
106
|
const result = diffFrames(expected, actual, options);
|
|
113
107
|
|
|
114
108
|
if (result.identical) {
|
|
115
|
-
return
|
|
109
|
+
return "✓ Frames are identical";
|
|
116
110
|
}
|
|
117
111
|
|
|
118
112
|
const lines: string[] = [
|
|
119
113
|
`╔══════════════════════════════════════════════════════════════════╗`,
|
|
120
114
|
`║ FRAME DIFF ║`,
|
|
121
|
-
`║ Changed: ${result.changedLines}/${result.totalLines} lines (${result.changePercentage.toFixed(1)}%)${
|
|
115
|
+
`║ Changed: ${result.changedLines}/${result.totalLines} lines (${result.changePercentage.toFixed(1)}%)${" ".repeat(Math.max(0, 24 - result.changePercentage.toFixed(1).length))}║`,
|
|
122
116
|
`╠══════════════════════════════════════════════════════════════════╣`,
|
|
123
117
|
`║ - = removed + = added ║`,
|
|
124
118
|
`╚══════════════════════════════════════════════════════════════════╝`,
|
|
125
|
-
|
|
119
|
+
"",
|
|
126
120
|
result.visualDiff,
|
|
127
121
|
];
|
|
128
122
|
|
|
129
|
-
return lines.join(
|
|
123
|
+
return lines.join("\n");
|
|
130
124
|
}
|
|
131
125
|
|
|
132
126
|
export function createCharDiff(expected: string, actual: string): string {
|
|
133
|
-
const expectedLines = expected.split(
|
|
134
|
-
const actualLines = actual.split(
|
|
127
|
+
const expectedLines = expected.split("\n");
|
|
128
|
+
const actualLines = actual.split("\n");
|
|
135
129
|
const maxLines = Math.max(expectedLines.length, actualLines.length);
|
|
136
130
|
|
|
137
131
|
const diffLines: string[] = [];
|
|
138
132
|
|
|
139
133
|
for (let i = 0; i < maxLines; i++) {
|
|
140
|
-
const expLine = expectedLines[i] ??
|
|
141
|
-
const actLine = actualLines[i] ??
|
|
134
|
+
const expLine = expectedLines[i] ?? "";
|
|
135
|
+
const actLine = actualLines[i] ?? "";
|
|
142
136
|
|
|
143
137
|
if (expLine === actLine) {
|
|
144
138
|
continue;
|
|
145
139
|
}
|
|
146
140
|
|
|
147
141
|
const maxLen = Math.max(expLine.length, actLine.length);
|
|
148
|
-
let markers =
|
|
142
|
+
let markers = "";
|
|
149
143
|
|
|
150
144
|
for (let j = 0; j < maxLen; j++) {
|
|
151
|
-
const expChar = expLine[j] ??
|
|
152
|
-
const actChar = actLine[j] ??
|
|
153
|
-
markers += expChar === actChar ?
|
|
145
|
+
const expChar = expLine[j] ?? " ";
|
|
146
|
+
const actChar = actLine[j] ?? " ";
|
|
147
|
+
markers += expChar === actChar ? " " : "^";
|
|
154
148
|
}
|
|
155
149
|
|
|
156
150
|
if (markers.trim()) {
|
|
@@ -158,9 +152,9 @@ export function createCharDiff(expected: string, actual: string): string {
|
|
|
158
152
|
diffLines.push(` exp: ${expLine}`);
|
|
159
153
|
diffLines.push(` act: ${actLine}`);
|
|
160
154
|
diffLines.push(` ${markers}`);
|
|
161
|
-
diffLines.push(
|
|
155
|
+
diffLines.push("");
|
|
162
156
|
}
|
|
163
157
|
}
|
|
164
158
|
|
|
165
|
-
return diffLines.join(
|
|
159
|
+
return diffLines.join("\n");
|
|
166
160
|
}
|
package/src/tui/driver/driver.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { createTestRenderer } from
|
|
2
|
-
import { AppContext, createRoot } from
|
|
3
|
-
import React from
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { TestModeContext } from '../hooks/useSafeRenderer.ts';
|
|
1
|
+
import { createTestRenderer } from "@opentui/core/testing";
|
|
2
|
+
import { AppContext, createRoot } from "@opentui/react";
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { type CreateAppOptions, createAppElement } from "../createApp.tsx";
|
|
5
|
+
import { TestModeContext } from "../hooks/useSafeRenderer.ts";
|
|
7
6
|
|
|
8
7
|
export interface DriverOptions {
|
|
9
8
|
width?: number;
|
|
@@ -44,22 +43,22 @@ export interface Driver {
|
|
|
44
43
|
launch(): Promise<void>;
|
|
45
44
|
close(): Promise<void>;
|
|
46
45
|
isRunning(): boolean;
|
|
47
|
-
|
|
46
|
+
|
|
48
47
|
sendKeys(keys: string): Promise<void>;
|
|
49
48
|
pressKey(key: string, modifiers?: KeyModifiers): Promise<void>;
|
|
50
49
|
typeText(text: string, delay?: number): Promise<void>;
|
|
51
50
|
pressEnter(): Promise<void>;
|
|
52
51
|
pressEscape(): Promise<void>;
|
|
53
52
|
pressTab(): Promise<void>;
|
|
54
|
-
pressArrow(direction:
|
|
55
|
-
|
|
53
|
+
pressArrow(direction: "up" | "down" | "left" | "right"): Promise<void>;
|
|
54
|
+
|
|
56
55
|
capture(): Promise<string>;
|
|
57
56
|
captureWithMeta(): Promise<CaptureResult>;
|
|
58
|
-
|
|
57
|
+
|
|
59
58
|
waitForText(text: string, options?: WaitOptions): Promise<boolean>;
|
|
60
59
|
waitForStable(options?: StableOptions): Promise<void>;
|
|
61
60
|
settle(): Promise<void>;
|
|
62
|
-
|
|
61
|
+
|
|
63
62
|
resize(cols: number, rows: number): Promise<void>;
|
|
64
63
|
getSize(): { width: number; height: number };
|
|
65
64
|
}
|
|
@@ -67,19 +66,19 @@ export interface Driver {
|
|
|
67
66
|
export async function createDriver(options: DriverOptions = {}): Promise<Driver> {
|
|
68
67
|
const width = options.width ?? 100;
|
|
69
68
|
const height = options.height ?? 30;
|
|
70
|
-
const appOptions = {
|
|
71
|
-
...options.appOptions,
|
|
69
|
+
const appOptions = {
|
|
70
|
+
...options.appOptions,
|
|
72
71
|
debug: options.appOptions?.debug ?? false,
|
|
73
72
|
testMode: true,
|
|
74
73
|
};
|
|
75
|
-
|
|
74
|
+
|
|
76
75
|
let testSetup: TestSetup | null = null;
|
|
77
76
|
let currentWidth = width;
|
|
78
77
|
let currentHeight = height;
|
|
79
78
|
|
|
80
79
|
const assertRunning = (): TestSetup => {
|
|
81
80
|
if (!testSetup) {
|
|
82
|
-
throw new Error(
|
|
81
|
+
throw new Error("Driver not launched. Call launch() first.");
|
|
83
82
|
}
|
|
84
83
|
return testSetup;
|
|
85
84
|
};
|
|
@@ -87,39 +86,35 @@ export async function createDriver(options: DriverOptions = {}): Promise<Driver>
|
|
|
87
86
|
const driver: Driver = {
|
|
88
87
|
async launch() {
|
|
89
88
|
if (testSetup) {
|
|
90
|
-
throw new Error(
|
|
89
|
+
throw new Error("Driver already launched. Call close() first.");
|
|
91
90
|
}
|
|
92
|
-
|
|
91
|
+
|
|
93
92
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
94
|
-
|
|
95
|
-
const coreTestSetup = await createTestRenderer({
|
|
96
|
-
width: currentWidth,
|
|
93
|
+
|
|
94
|
+
const coreTestSetup = await createTestRenderer({
|
|
95
|
+
width: currentWidth,
|
|
97
96
|
height: currentHeight,
|
|
98
97
|
});
|
|
99
|
-
|
|
98
|
+
|
|
100
99
|
const root = createRoot(coreTestSetup.renderer);
|
|
101
|
-
|
|
100
|
+
|
|
102
101
|
const appElement = createAppElement(appOptions);
|
|
103
|
-
|
|
102
|
+
|
|
104
103
|
const wrappedApp = React.createElement(
|
|
105
104
|
AppContext.Provider,
|
|
106
|
-
{
|
|
107
|
-
value: {
|
|
108
|
-
keyHandler: coreTestSetup.renderer.keyInput,
|
|
109
|
-
renderer: coreTestSetup.renderer
|
|
110
|
-
}
|
|
105
|
+
{
|
|
106
|
+
value: {
|
|
107
|
+
keyHandler: coreTestSetup.renderer.keyInput,
|
|
108
|
+
renderer: coreTestSetup.renderer,
|
|
109
|
+
},
|
|
111
110
|
},
|
|
112
|
-
React.createElement(
|
|
113
|
-
TestModeContext.Provider,
|
|
114
|
-
{ value: true },
|
|
115
|
-
appElement
|
|
116
|
-
)
|
|
111
|
+
React.createElement(TestModeContext.Provider, { value: true }, appElement),
|
|
117
112
|
);
|
|
118
|
-
|
|
113
|
+
|
|
119
114
|
act(() => {
|
|
120
115
|
root.render(wrappedApp);
|
|
121
116
|
});
|
|
122
|
-
|
|
117
|
+
|
|
123
118
|
testSetup = { ...coreTestSetup, root };
|
|
124
119
|
await testSetup.renderOnce();
|
|
125
120
|
},
|
|
@@ -143,21 +138,21 @@ export async function createDriver(options: DriverOptions = {}): Promise<Driver>
|
|
|
143
138
|
|
|
144
139
|
async sendKeys(keys: string) {
|
|
145
140
|
const setup = assertRunning();
|
|
146
|
-
|
|
141
|
+
|
|
147
142
|
for (const char of keys) {
|
|
148
|
-
if (char ===
|
|
143
|
+
if (char === "\r" || char === "\n") {
|
|
149
144
|
setup.mockInput.pressEnter();
|
|
150
|
-
} else if (char ===
|
|
145
|
+
} else if (char === "\t") {
|
|
151
146
|
setup.mockInput.pressTab();
|
|
152
|
-
} else if (char ===
|
|
147
|
+
} else if (char === "\x1b") {
|
|
153
148
|
setup.mockInput.pressEscape();
|
|
154
|
-
} else if (char ===
|
|
149
|
+
} else if (char === "\x7f") {
|
|
155
150
|
setup.mockInput.pressBackspace();
|
|
156
151
|
} else {
|
|
157
152
|
setup.mockInput.pressKey(char);
|
|
158
153
|
}
|
|
159
154
|
}
|
|
160
|
-
|
|
155
|
+
|
|
161
156
|
await this.settle();
|
|
162
157
|
},
|
|
163
158
|
|
|
@@ -191,7 +186,7 @@ export async function createDriver(options: DriverOptions = {}): Promise<Driver>
|
|
|
191
186
|
await this.settle();
|
|
192
187
|
},
|
|
193
188
|
|
|
194
|
-
async pressArrow(direction:
|
|
189
|
+
async pressArrow(direction: "up" | "down" | "left" | "right") {
|
|
195
190
|
const setup = assertRunning();
|
|
196
191
|
setup.mockInput.pressArrow(direction);
|
|
197
192
|
await this.settle();
|
|
@@ -216,23 +211,23 @@ export async function createDriver(options: DriverOptions = {}): Promise<Driver>
|
|
|
216
211
|
async waitForText(text: string, options: WaitOptions = {}): Promise<boolean> {
|
|
217
212
|
const { timeout = 5000, interval = 50 } = options;
|
|
218
213
|
const start = Date.now();
|
|
219
|
-
|
|
214
|
+
|
|
220
215
|
while (Date.now() - start < timeout) {
|
|
221
216
|
const frame = await this.capture();
|
|
222
217
|
if (frame.includes(text)) {
|
|
223
218
|
return true;
|
|
224
219
|
}
|
|
225
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
226
221
|
}
|
|
227
|
-
|
|
222
|
+
|
|
228
223
|
return false;
|
|
229
224
|
},
|
|
230
225
|
|
|
231
226
|
async waitForStable(options: StableOptions = {}): Promise<void> {
|
|
232
227
|
const { maxIterations = 10, intervalMs = 50, stableFrames = 2 } = options;
|
|
233
|
-
let lastFrame =
|
|
228
|
+
let lastFrame = "";
|
|
234
229
|
let stableCount = 0;
|
|
235
|
-
|
|
230
|
+
|
|
236
231
|
for (let i = 0; i < maxIterations; i++) {
|
|
237
232
|
const frame = await this.capture();
|
|
238
233
|
if (frame === lastFrame) {
|
|
@@ -244,7 +239,7 @@ export async function createDriver(options: DriverOptions = {}): Promise<Driver>
|
|
|
244
239
|
stableCount = 0;
|
|
245
240
|
}
|
|
246
241
|
lastFrame = frame;
|
|
247
|
-
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
248
243
|
}
|
|
249
244
|
},
|
|
250
245
|
|
package/src/tui/driver/index.ts
CHANGED
|
@@ -1,53 +1,49 @@
|
|
|
1
1
|
export {
|
|
2
|
-
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
type AssertOptions,
|
|
3
|
+
type AssertResult,
|
|
4
|
+
assertSnapshot,
|
|
5
|
+
deleteGoldenFile,
|
|
6
|
+
type GoldenFile,
|
|
7
|
+
type GoldenFileInfo,
|
|
8
|
+
getGoldenFile,
|
|
9
|
+
listGoldenFiles,
|
|
10
|
+
} from "./assertions.ts";
|
|
11
|
+
export {
|
|
12
|
+
type CoverageReport,
|
|
13
|
+
type CoverageTracker,
|
|
14
|
+
createCoverageTracker,
|
|
15
|
+
detectViewFromFrame,
|
|
16
|
+
formatCoverageReport,
|
|
17
|
+
type ViewVisit,
|
|
18
|
+
} from "./coverage.ts";
|
|
11
19
|
export {
|
|
12
|
-
diffFrames,
|
|
13
|
-
highlightDiff,
|
|
14
20
|
createCharDiff,
|
|
21
|
+
type DiffOptions,
|
|
15
22
|
type DiffResult,
|
|
23
|
+
diffFrames,
|
|
24
|
+
highlightDiff,
|
|
16
25
|
type LineChange,
|
|
17
|
-
|
|
18
|
-
} from './diff.ts';
|
|
19
|
-
|
|
26
|
+
} from "./diff.ts";
|
|
20
27
|
export {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
|
|
29
|
-
} from './assertions.ts';
|
|
30
|
-
|
|
28
|
+
type CaptureResult,
|
|
29
|
+
createDriver,
|
|
30
|
+
type Driver,
|
|
31
|
+
type DriverOptions,
|
|
32
|
+
type KeyModifiers,
|
|
33
|
+
type StableOptions,
|
|
34
|
+
type WaitOptions,
|
|
35
|
+
} from "./driver.ts";
|
|
31
36
|
export {
|
|
32
37
|
createRecorder,
|
|
33
|
-
replayRecording,
|
|
34
|
-
saveRecording,
|
|
35
|
-
loadRecording,
|
|
36
|
-
listRecordings,
|
|
37
38
|
deleteRecording,
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
listRecordings,
|
|
40
|
+
loadRecording,
|
|
40
41
|
type RecordedCommand,
|
|
42
|
+
type Recorder,
|
|
41
43
|
type RecorderOptions,
|
|
44
|
+
type Recording,
|
|
42
45
|
type ReplayOptions,
|
|
43
46
|
type ReplayResult,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
createCoverageTracker,
|
|
48
|
-
detectViewFromFrame,
|
|
49
|
-
formatCoverageReport,
|
|
50
|
-
type CoverageTracker,
|
|
51
|
-
type CoverageReport,
|
|
52
|
-
type ViewVisit,
|
|
53
|
-
} from './coverage.ts';
|
|
47
|
+
replayRecording,
|
|
48
|
+
saveRecording,
|
|
49
|
+
} from "./recorder.ts";
|