@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.
Files changed (160) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +42 -38
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +36 -23
  19. package/src/plugins/notifications/visual-flash.ts +31 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +68 -57
  34. package/src/plugins/registry.ts +80 -71
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/persistence-service.ts +206 -0
  73. package/src/storage/repos/agentSessions.ts +33 -23
  74. package/src/storage/repos/providerSnapshots.ts +5 -5
  75. package/src/storage/repos/usageEvents.ts +40 -28
  76. package/src/storage/retention.ts +146 -0
  77. package/src/storage/types.ts +5 -5
  78. package/src/tui/App.tsx +180 -137
  79. package/src/tui/components/CommandPalette.tsx +46 -34
  80. package/src/tui/components/DebugConsole.tsx +39 -33
  81. package/src/tui/components/DebugPanel.tsx +314 -183
  82. package/src/tui/components/FooterHints.tsx +2 -2
  83. package/src/tui/components/GhostProviderCard.tsx +37 -35
  84. package/src/tui/components/Header.tsx +68 -35
  85. package/src/tui/components/HelpOverlay.tsx +99 -29
  86. package/src/tui/components/InlineGauge.tsx +10 -7
  87. package/src/tui/components/InlineSparkline.tsx +17 -11
  88. package/src/tui/components/KpiStrip.tsx +128 -98
  89. package/src/tui/components/LimitGauge.tsx +96 -59
  90. package/src/tui/components/ModalBackdrop.tsx +4 -4
  91. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  92. package/src/tui/components/ProviderCard.tsx +232 -226
  93. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  94. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  95. package/src/tui/components/ProvidersList.tsx +215 -126
  96. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  97. package/src/tui/components/SessionsTable.tsx +320 -169
  98. package/src/tui/components/SettingsModal.tsx +593 -269
  99. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  100. package/src/tui/components/Skeleton.tsx +24 -17
  101. package/src/tui/components/SmartSidebar.tsx +360 -258
  102. package/src/tui/components/Sparkline.tsx +64 -52
  103. package/src/tui/components/Spinner.tsx +3 -3
  104. package/src/tui/components/StatusBar.tsx +27 -27
  105. package/src/tui/components/ThemePicker.tsx +67 -66
  106. package/src/tui/components/Toast.tsx +9 -6
  107. package/src/tui/components/UsageGauge.tsx +10 -13
  108. package/src/tui/components/index.ts +5 -5
  109. package/src/tui/contexts/AgentSessionContext.tsx +164 -151
  110. package/src/tui/contexts/ConfigContext.tsx +15 -13
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  112. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  113. package/src/tui/contexts/DrawerContext.tsx +11 -9
  114. package/src/tui/contexts/InputContext.tsx +2 -2
  115. package/src/tui/contexts/LogContext.tsx +62 -47
  116. package/src/tui/contexts/PluginContext.tsx +238 -183
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  118. package/src/tui/contexts/StorageContext.tsx +95 -149
  119. package/src/tui/contexts/ThemeContext.tsx +21 -16
  120. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  121. package/src/tui/contexts/ToastContext.tsx +10 -7
  122. package/src/tui/contexts/index.ts +9 -5
  123. package/src/tui/createApp.tsx +8 -12
  124. package/src/tui/debug/captureFrame.ts +25 -28
  125. package/src/tui/debug/snapshot.tsx +179 -162
  126. package/src/tui/driver/assertions.ts +30 -22
  127. package/src/tui/driver/cli.ts +231 -221
  128. package/src/tui/driver/coverage.ts +27 -44
  129. package/src/tui/driver/demo-snapshot.ts +33 -33
  130. package/src/tui/driver/diff.ts +26 -32
  131. package/src/tui/driver/driver.ts +43 -48
  132. package/src/tui/driver/index.ts +36 -40
  133. package/src/tui/driver/recorder.ts +37 -48
  134. package/src/tui/driver/test-workflow.ts +51 -51
  135. package/src/tui/driver/test.ts +25 -25
  136. package/src/tui/hooks/index.ts +24 -20
  137. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  138. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  139. package/src/tui/hooks/useDashboardState.ts +26 -24
  140. package/src/tui/hooks/useEmaActivity.ts +82 -74
  141. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  142. package/src/tui/hooks/useExitAnimation.ts +7 -3
  143. package/src/tui/hooks/usePulse.ts +4 -4
  144. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  145. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  146. package/src/tui/hooks/useValueFlash.ts +10 -14
  147. package/src/tui/index.tsx +29 -6
  148. package/src/tui/shutdown.ts +19 -0
  149. package/src/tui/utils/providerColor.ts +1 -1
  150. package/src/tui/views/Dashboard.tsx +164 -102
  151. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  152. package/src/tui/views/ProjectsView.tsx +495 -243
  153. package/src/tui/views/RealTimeDashboard.tsx +176 -116
  154. package/src/tui/views/SettingsView.tsx +225 -189
  155. package/src/tui/views/index.ts +2 -2
  156. package/src/types/opentui-test-utils.d.ts +7 -7
  157. package/src/utils/clipboard.ts +29 -29
  158. package/src/utils/currency.test.ts +53 -53
  159. package/src/utils/currency.ts +35 -32
  160. 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
- 'Dashboard',
28
- 'Providers',
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).forEach((k) => delete viewCounts[k]);
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('\n').slice(0, 5);
120
- const headerArea = lines.join('\n');
111
+ const lines = frame.split("\n").slice(0, 5);
112
+ const headerArea = lines.join("\n");
121
113
 
122
- if (headerArea.includes('[1] Dashboard') && 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('[')) continue;
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
- '║ COVERAGE REPORT ║',
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('║ View Breakdown: ║');
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('\n');
186
+ return lines.join("\n");
204
187
  }
@@ -1,66 +1,66 @@
1
1
  #!/usr/bin/env bun
2
- import { createDriver } from './driver.ts';
3
- import * as fs from 'fs/promises';
4
- import * as path from 'path';
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 = './snapshots';
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, 'utf-8');
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('Creating driver...');
16
+ console.log("Creating driver...");
17
17
  const driver = await createDriver({ width: 100, height: 30 });
18
-
19
- console.log('Launching app...');
18
+
19
+ console.log("Launching app...");
20
20
  await driver.launch();
21
21
  await driver.waitForStable();
22
-
23
- console.log('Capturing dashboard...');
22
+
23
+ console.log("Capturing dashboard...");
24
24
  const dashboardFrame = await driver.capture();
25
- const dashboardPath = await saveSnapshot(dashboardFrame, '01-dashboard');
25
+ const dashboardPath = await saveSnapshot(dashboardFrame, "01-dashboard");
26
26
  console.log(`Saved: ${dashboardPath}`);
27
-
28
- console.log('Switching to Providers view...');
29
- await driver.pressKey('2');
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, '02-providers');
32
+ const providersPath = await saveSnapshot(providersFrame, "02-providers");
33
33
  console.log(`Saved: ${providersPath}`);
34
-
35
- console.log('Switching to Trends view...');
36
- await driver.pressKey('3');
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, '03-trends');
39
+ const trendsPath = await saveSnapshot(trendsFrame, "03-trends");
40
40
  console.log(`Saved: ${trendsPath}`);
41
-
42
- console.log('Switching to Projects view...');
43
- await driver.pressKey('4');
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, '04-projects');
46
+ const projectsPath = await saveSnapshot(projectsFrame, "04-projects");
47
47
  console.log(`Saved: ${projectsPath}`);
48
-
49
- console.log('Switching to Settings view...');
50
- await driver.pressKey('5');
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, '05-settings');
53
+ const settingsPath = await saveSnapshot(settingsFrame, "05-settings");
54
54
  console.log(`Saved: ${settingsPath}`);
55
-
56
- console.log('Closing driver...');
55
+
56
+ console.log("Closing driver...");
57
57
  await driver.close();
58
-
59
- console.log('\n✓ All snapshots saved to ./snapshots/');
60
- console.log(' You can view them with: cat snapshots/01-dashboard.txt');
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('Demo failed:', err);
64
+ console.error("Demo failed:", err);
65
65
  process.exit(1);
66
66
  });
@@ -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('\n');
34
- const actualLines = actual.split('\n');
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('\n'),
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 '✓ Frames are identical';
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)}%)${' '.repeat(Math.max(0, 24 - result.changePercentage.toFixed(1).length))}║`,
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('\n');
123
+ return lines.join("\n");
130
124
  }
131
125
 
132
126
  export function createCharDiff(expected: string, actual: string): string {
133
- const expectedLines = expected.split('\n');
134
- const actualLines = actual.split('\n');
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('\n');
159
+ return diffLines.join("\n");
166
160
  }
@@ -1,9 +1,8 @@
1
- import { createTestRenderer } from '@opentui/core/testing';
2
- import { AppContext, createRoot } from '@opentui/react';
3
- import React from 'react';
4
- import { act } from 'react';
5
- import { createAppElement, type CreateAppOptions } from '../createApp.tsx';
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: 'up' | 'down' | 'left' | 'right'): Promise<void>;
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('Driver not launched. Call launch() first.');
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('Driver already launched. Call close() first.');
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 === '\r' || char === '\n') {
143
+ if (char === "\r" || char === "\n") {
149
144
  setup.mockInput.pressEnter();
150
- } else if (char === '\t') {
145
+ } else if (char === "\t") {
151
146
  setup.mockInput.pressTab();
152
- } else if (char === '\x1b') {
147
+ } else if (char === "\x1b") {
153
148
  setup.mockInput.pressEscape();
154
- } else if (char === '\x7f') {
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: 'up' | 'down' | 'left' | 'right') {
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
 
@@ -1,53 +1,49 @@
1
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
-
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
- type DiffOptions,
18
- } from './diff.ts';
19
-
26
+ } from "./diff.ts";
20
27
  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
-
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
- type Recorder,
39
- type Recording,
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
- } 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';
47
+ replayRecording,
48
+ saveRecording,
49
+ } from "./recorder.ts";