@tokentop/ttop 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -0
  3. package/bin/ttop.js +49 -0
  4. package/package.json +68 -0
  5. package/src/agents/aggregator.ts +176 -0
  6. package/src/agents/costing.ts +74 -0
  7. package/src/agents/index.ts +12 -0
  8. package/src/agents/types.ts +71 -0
  9. package/src/cli.ts +167 -0
  10. package/src/config/schema.test.ts +166 -0
  11. package/src/config/schema.ts +165 -0
  12. package/src/demo/simulator.ts +745 -0
  13. package/src/plugins/agents/index.ts +2 -0
  14. package/src/plugins/auth-sources.ts +186 -0
  15. package/src/plugins/lifecycle.ts +177 -0
  16. package/src/plugins/loader.test.ts +195 -0
  17. package/src/plugins/loader.ts +282 -0
  18. package/src/plugins/notification-bus.ts +153 -0
  19. package/src/plugins/notifications/index.ts +2 -0
  20. package/src/plugins/notifications/terminal-bell.ts +66 -0
  21. package/src/plugins/notifications/visual-flash.ts +57 -0
  22. package/src/plugins/npm-installer.ts +164 -0
  23. package/src/plugins/plugin-context-factory.ts +67 -0
  24. package/src/plugins/plugin-host.ts +205 -0
  25. package/src/plugins/providers/anthropic.ts +325 -0
  26. package/src/plugins/providers/antigravity.ts +309 -0
  27. package/src/plugins/providers/codex.ts +250 -0
  28. package/src/plugins/providers/gemini.ts +391 -0
  29. package/src/plugins/providers/github-copilot.ts +194 -0
  30. package/src/plugins/providers/index.ts +12 -0
  31. package/src/plugins/providers/minimax.ts +223 -0
  32. package/src/plugins/providers/openai-api.ts +250 -0
  33. package/src/plugins/providers/opencode-zen.ts +125 -0
  34. package/src/plugins/providers/perplexity.ts +177 -0
  35. package/src/plugins/providers/zai.ts +225 -0
  36. package/src/plugins/registry.ts +251 -0
  37. package/src/plugins/sandbox-guard.test.ts +191 -0
  38. package/src/plugins/sandbox-guard.ts +155 -0
  39. package/src/plugins/sandbox.ts +138 -0
  40. package/src/plugins/themes/catppuccin-latte.ts +45 -0
  41. package/src/plugins/themes/catppuccin-mocha.ts +45 -0
  42. package/src/plugins/themes/claude-code.ts +45 -0
  43. package/src/plugins/themes/dracula.ts +45 -0
  44. package/src/plugins/themes/github-light.ts +45 -0
  45. package/src/plugins/themes/gruvbox-dark.ts +45 -0
  46. package/src/plugins/themes/gruvbox-light.ts +45 -0
  47. package/src/plugins/themes/index.ts +15 -0
  48. package/src/plugins/themes/kanagawa.ts +45 -0
  49. package/src/plugins/themes/nord.ts +45 -0
  50. package/src/plugins/themes/one-dark.ts +45 -0
  51. package/src/plugins/themes/opencode.ts +47 -0
  52. package/src/plugins/themes/rose-pine-dawn.ts +45 -0
  53. package/src/plugins/themes/rose-pine.ts +45 -0
  54. package/src/plugins/themes/solarized-light.ts +45 -0
  55. package/src/plugins/themes/tokyo-night.ts +49 -0
  56. package/src/plugins/types/agent.ts +108 -0
  57. package/src/plugins/types/base.ts +267 -0
  58. package/src/plugins/types/index.ts +19 -0
  59. package/src/plugins/types/notification.ts +51 -0
  60. package/src/plugins/types/provider.ts +233 -0
  61. package/src/plugins/types/theme.ts +70 -0
  62. package/src/plugins/update-checker.ts +177 -0
  63. package/src/pricing/estimator.test.ts +181 -0
  64. package/src/pricing/estimator.ts +80 -0
  65. package/src/pricing/fallback.test.ts +71 -0
  66. package/src/pricing/fallback.ts +62 -0
  67. package/src/pricing/index.ts +66 -0
  68. package/src/pricing/models-dev.ts +130 -0
  69. package/src/storage/database.ts +613 -0
  70. package/src/storage/db.ts +64 -0
  71. package/src/storage/index.ts +66 -0
  72. package/src/storage/migrations/index.ts +189 -0
  73. package/src/storage/paths.ts +24 -0
  74. package/src/storage/repos/agentSessions.ts +210 -0
  75. package/src/storage/repos/providerSnapshots.ts +116 -0
  76. package/src/storage/repos/usageEvents.ts +334 -0
  77. package/src/storage/types.ts +288 -0
  78. package/src/tui/App.tsx +381 -0
  79. package/src/tui/components/CommandPalette.tsx +166 -0
  80. package/src/tui/components/DebugConsole.tsx +161 -0
  81. package/src/tui/components/DebugPanel.tsx +660 -0
  82. package/src/tui/components/FooterHints.tsx +22 -0
  83. package/src/tui/components/GhostProviderCard.tsx +45 -0
  84. package/src/tui/components/Header.tsx +125 -0
  85. package/src/tui/components/HelpOverlay.tsx +45 -0
  86. package/src/tui/components/InlineGauge.tsx +34 -0
  87. package/src/tui/components/InlineSparkline.tsx +78 -0
  88. package/src/tui/components/KpiStrip.tsx +246 -0
  89. package/src/tui/components/LimitGauge.tsx +186 -0
  90. package/src/tui/components/ModalBackdrop.tsx +42 -0
  91. package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
  92. package/src/tui/components/ProviderCard.tsx +309 -0
  93. package/src/tui/components/ProviderDetailPanel.tsx +237 -0
  94. package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
  95. package/src/tui/components/ProvidersList.tsx +380 -0
  96. package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
  97. package/src/tui/components/SessionsTable.tsx +401 -0
  98. package/src/tui/components/SettingsModal.tsx +587 -0
  99. package/src/tui/components/SidebarBreakdown.tsx +87 -0
  100. package/src/tui/components/Skeleton.tsx +102 -0
  101. package/src/tui/components/SmartSidebar.tsx +644 -0
  102. package/src/tui/components/Sparkline.tsx +180 -0
  103. package/src/tui/components/Spinner.tsx +36 -0
  104. package/src/tui/components/StatusBar.tsx +96 -0
  105. package/src/tui/components/ThemePicker.tsx +234 -0
  106. package/src/tui/components/Toast.tsx +61 -0
  107. package/src/tui/components/UsageGauge.tsx +80 -0
  108. package/src/tui/components/index.ts +5 -0
  109. package/src/tui/contexts/AgentSessionContext.tsx +307 -0
  110. package/src/tui/contexts/ConfigContext.tsx +141 -0
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
  112. package/src/tui/contexts/DemoModeContext.tsx +60 -0
  113. package/src/tui/contexts/DrawerContext.tsx +42 -0
  114. package/src/tui/contexts/InputContext.tsx +30 -0
  115. package/src/tui/contexts/LogContext.tsx +150 -0
  116. package/src/tui/contexts/PluginContext.tsx +424 -0
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
  118. package/src/tui/contexts/StorageContext.tsx +233 -0
  119. package/src/tui/contexts/ThemeContext.tsx +95 -0
  120. package/src/tui/contexts/TimeWindowContext.tsx +158 -0
  121. package/src/tui/contexts/ToastContext.tsx +40 -0
  122. package/src/tui/contexts/index.ts +5 -0
  123. package/src/tui/createApp.tsx +47 -0
  124. package/src/tui/debug/captureFrame.ts +262 -0
  125. package/src/tui/debug/snapshot.tsx +644 -0
  126. package/src/tui/driver/assertions.ts +279 -0
  127. package/src/tui/driver/cli.ts +717 -0
  128. package/src/tui/driver/coverage.ts +204 -0
  129. package/src/tui/driver/demo-snapshot.ts +66 -0
  130. package/src/tui/driver/diff.ts +166 -0
  131. package/src/tui/driver/driver.ts +270 -0
  132. package/src/tui/driver/index.ts +53 -0
  133. package/src/tui/driver/recorder.ts +303 -0
  134. package/src/tui/driver/test-workflow.ts +115 -0
  135. package/src/tui/driver/test.ts +47 -0
  136. package/src/tui/hooks/index.ts +20 -0
  137. package/src/tui/hooks/useAnimatedValue.ts +113 -0
  138. package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
  139. package/src/tui/hooks/useDashboardState.ts +116 -0
  140. package/src/tui/hooks/useEmaActivity.ts +223 -0
  141. package/src/tui/hooks/useEntranceAnimation.ts +62 -0
  142. package/src/tui/hooks/useExitAnimation.ts +137 -0
  143. package/src/tui/hooks/usePulse.ts +138 -0
  144. package/src/tui/hooks/useSafeRenderer.ts +24 -0
  145. package/src/tui/hooks/useValueFlash.ts +111 -0
  146. package/src/tui/index.tsx +50 -0
  147. package/src/tui/utils/providerColor.ts +32 -0
  148. package/src/tui/views/Dashboard.tsx +340 -0
  149. package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
  150. package/src/tui/views/ProjectsView.tsx +1277 -0
  151. package/src/tui/views/RealTimeDashboard.tsx +393 -0
  152. package/src/tui/views/SettingsView.tsx +465 -0
  153. package/src/tui/views/index.ts +2 -0
  154. package/src/types/global.d.ts +5 -0
  155. package/src/types/opentui-test-utils.d.ts +18 -0
  156. package/src/utils/clipboard.ts +118 -0
  157. package/src/utils/currency.test.ts +106 -0
  158. package/src/utils/currency.ts +79 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,644 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Headless Snapshot Script for Component Isolation Testing
4
+ *
5
+ * Renders components in a headless environment and captures the output.
6
+ * Useful for debugging layout issues without running the full app.
7
+ *
8
+ * Usage:
9
+ * bun src/tui/debug/snapshot.tsx [component] [options]
10
+ *
11
+ * Examples:
12
+ * bun src/tui/debug/snapshot.tsx --list # List all available components
13
+ * bun src/tui/debug/snapshot.tsx header # Snapshot Header
14
+ * bun src/tui/debug/snapshot.tsx provider-card # Snapshot ProviderCard
15
+ * bun src/tui/debug/snapshot.tsx kpi-strip # Snapshot KpiStrip
16
+ * bun src/tui/debug/snapshot.tsx --width 120 --height 40 # Custom dimensions
17
+ * bun src/tui/debug/snapshot.tsx toast --output frame.txt # Custom output file
18
+ *
19
+ * Adding New Components:
20
+ * 1. Create a mock data factory function: createMock<ComponentName>Props()
21
+ * 2. Register it in COMPONENT_REGISTRY with render function
22
+ * 3. That's it! The component will be available via CLI
23
+ */
24
+
25
+ import * as fs from 'fs/promises';
26
+ import * as path from 'path';
27
+ import type { ReactNode } from 'react';
28
+ import { testRender } from '@opentui/react/test-utils';
29
+ import { ThemeProvider } from '../contexts/ThemeContext.tsx';
30
+ import { LogProvider } from '../contexts/LogContext.tsx';
31
+ import { getFramesDir } from './captureFrame.ts';
32
+
33
+ import { Header } from '../components/Header.tsx';
34
+ import { StatusBar } from '../components/StatusBar.tsx';
35
+ import { ProviderCard } from '../components/ProviderCard.tsx';
36
+ import { UsageGauge } from '../components/UsageGauge.tsx';
37
+ import { Toast } from '../components/Toast.tsx';
38
+ import { Spinner } from '../components/Spinner.tsx';
39
+ import { SkeletonText, SkeletonGauge, SkeletonProviderContent } from '../components/Skeleton.tsx';
40
+ import { DebugConsole } from '../components/DebugConsole.tsx';
41
+ import { KpiStrip } from '../components/KpiStrip.tsx';
42
+ import { HistoricalTrendsView } from '../views/HistoricalTrendsView.tsx';
43
+ import { ProjectsView } from '../views/ProjectsView.tsx';
44
+ import { SettingsView } from '../views/SettingsView.tsx';
45
+ import { SessionDetailsDrawer } from '../components/SessionDetailsDrawer.tsx';
46
+ import { AgentSessionProvider } from '../contexts/AgentSessionContext.tsx';
47
+ import { TimeWindowProvider } from '../contexts/TimeWindowContext.tsx';
48
+ import { StorageProvider } from '../contexts/StorageContext.tsx';
49
+ import { InputProvider } from '../contexts/InputContext.tsx';
50
+ import { PluginProvider } from '../contexts/PluginContext.tsx';
51
+ import { ToastProvider } from '../contexts/ToastContext.tsx';
52
+ import { ConfigProvider } from '../contexts/ConfigContext.tsx';
53
+ import { DEFAULT_CONFIG } from '@/config/schema.ts';
54
+ import { createDriver } from '../driver/driver.ts';
55
+
56
+ function createMockHeaderProps() {
57
+ return {
58
+ title: 'tokentop',
59
+ subtitle: 'v0.1.0',
60
+ activeView: 'dashboard' as const,
61
+ };
62
+ }
63
+
64
+ function createMockStatusBarProps() {
65
+ const now = Date.now();
66
+ return {
67
+ lastRefresh: now - 30000,
68
+ nextRefresh: now + 30000,
69
+ message: 'tokentop - htop for AI usage',
70
+ };
71
+ }
72
+
73
+ function createMockProviderCardProps() {
74
+ const now = Date.now();
75
+ return {
76
+ name: 'Anthropic',
77
+ configured: true,
78
+ loading: false,
79
+ color: '#D97706',
80
+ focused: false,
81
+ usage: {
82
+ planType: 'Claude Pro',
83
+ limitReached: false,
84
+ limits: {
85
+ primary: {
86
+ usedPercent: 65,
87
+ label: 'Daily Messages',
88
+ windowMinutes: 1440,
89
+ resetsAt: now + 3600000,
90
+ },
91
+ secondary: {
92
+ usedPercent: 23,
93
+ label: 'Weekly Tokens',
94
+ windowMinutes: 10080,
95
+ },
96
+ },
97
+ credits: {
98
+ hasCredits: true,
99
+ unlimited: false,
100
+ balance: '$42.50',
101
+ },
102
+ fetchedAt: now,
103
+ },
104
+ };
105
+ }
106
+
107
+ function createMockProviderCardLoadingProps() {
108
+ return {
109
+ name: 'OpenAI',
110
+ configured: true,
111
+ loading: true,
112
+ color: '#10A37F',
113
+ focused: false,
114
+ usage: null,
115
+ };
116
+ }
117
+
118
+ function createMockProviderCardUnconfiguredProps() {
119
+ return {
120
+ name: 'Google',
121
+ configured: false,
122
+ loading: false,
123
+ color: '#4285F4',
124
+ focused: false,
125
+ usage: null,
126
+ };
127
+ }
128
+
129
+ function createMockProviderCardErrorProps() {
130
+ const now = Date.now();
131
+ return {
132
+ name: 'Mistral',
133
+ configured: true,
134
+ loading: false,
135
+ color: '#FF7000',
136
+ focused: false,
137
+ usage: {
138
+ error: 'API rate limit exceeded',
139
+ fetchedAt: now,
140
+ },
141
+ };
142
+ }
143
+
144
+ function createMockUsageGaugeProps() {
145
+ const now = Date.now();
146
+ return {
147
+ label: 'Daily Tokens',
148
+ usedPercent: 73,
149
+ windowLabel: '24-hour window',
150
+ resetsAt: now + 7200000,
151
+ width: 40,
152
+ color: '#7C3AED',
153
+ };
154
+ }
155
+
156
+ function createMockToastProps() {
157
+ return {
158
+ message: 'Settings saved successfully!',
159
+ type: 'success' as const,
160
+ duration: 999999,
161
+ onDismiss: () => {},
162
+ };
163
+ }
164
+
165
+ function createMockToastErrorProps() {
166
+ return {
167
+ message: 'Failed to connect to API',
168
+ type: 'error' as const,
169
+ duration: 999999,
170
+ onDismiss: () => {},
171
+ };
172
+ }
173
+
174
+ function createMockToastWarningProps() {
175
+ return {
176
+ message: 'Rate limit approaching',
177
+ type: 'warning' as const,
178
+ duration: 999999,
179
+ onDismiss: () => {},
180
+ };
181
+ }
182
+
183
+ function createMockKpiStripProps() {
184
+ const now = Date.now();
185
+ return {
186
+ totalCost: 12.43,
187
+ totalTokens: 48250,
188
+ totalRequests: 128,
189
+ activeCount: 3,
190
+ deltaCost: 3.12,
191
+ deltaTokens: 12450,
192
+ windowSec: 300,
193
+ activity: {
194
+ instantRate: 48,
195
+ avgRate: 32,
196
+ isSpike: false,
197
+ },
198
+ sparkData: [
199
+ 4, 6, 8, 12, 18, 22, 28, 36, 32, 40, 44, 48, 52, 48, 44, 40, 36, 32,
200
+ 28, 24, 22, 20, 18, 16, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54,
201
+ ],
202
+ budget: {
203
+ limit: 25,
204
+ budgetCost: 12.43,
205
+ budgetType: 'daily' as const,
206
+ budgetTypeLabel: 'Daily',
207
+ warningPercent: 80,
208
+ criticalPercent: 90,
209
+ },
210
+ lastRefreshAt: now - 30000,
211
+ };
212
+ }
213
+
214
+ function createMockSpinnerProps() {
215
+ return {
216
+ color: '#7C3AED',
217
+ };
218
+ }
219
+
220
+ function createMockSessionDetailsDrawerProps() {
221
+ const now = Date.now();
222
+ return {
223
+ session: {
224
+ sessionId: 'ses_39c52c5b5ffe4D0eUfUnnCdNw4',
225
+ sessionName: 'test-smoke done',
226
+ agentId: 'opencode',
227
+ agentName: 'OpenCode',
228
+ projectPath: '/Users/nigel/development/opencode-mission-control',
229
+ startedAt: now - 10000000,
230
+ lastActivityAt: now,
231
+ status: 'active' as const,
232
+ totals: {
233
+ input: 1500000,
234
+ output: 200000,
235
+ cacheRead: 10000000,
236
+ cacheWrite: 5000000,
237
+ },
238
+ totalCostUsd: 16.05,
239
+ requestCount: 150,
240
+ streams: [
241
+ {
242
+ providerId: 'anthropic',
243
+ modelId: 'claude-3-opus-20240229',
244
+ tokens: {
245
+ input: 1500000,
246
+ output: 200000,
247
+ cacheRead: 10000000,
248
+ cacheWrite: 5000000,
249
+ },
250
+ requestCount: 150,
251
+ costUsd: 16.05,
252
+ costBreakdown: {
253
+ total: 16.05,
254
+ input: 0.01,
255
+ output: 1.01,
256
+ cacheRead: 10.03,
257
+ cacheWrite: 5.00,
258
+ },
259
+ },
260
+ ],
261
+ costInDay: 16.05,
262
+ costInWeek: 16.05,
263
+ costInMonth: 16.05,
264
+ },
265
+ onClose: () => {},
266
+ };
267
+ }
268
+
269
+ interface ComponentEntry {
270
+ name: string;
271
+ description: string;
272
+ defaultWidth: number;
273
+ defaultHeight: number;
274
+ render: () => ReactNode;
275
+ }
276
+
277
+ const COMPONENT_REGISTRY: Record<string, ComponentEntry> = {
278
+ 'header': {
279
+ name: 'Header',
280
+ description: 'Top navigation header with title and view tabs',
281
+ defaultWidth: 100,
282
+ defaultHeight: 3,
283
+ render: () => <Header {...createMockHeaderProps()} />,
284
+ },
285
+ 'status-bar': {
286
+ name: 'StatusBar',
287
+ description: 'Bottom status bar with refresh timing',
288
+ defaultWidth: 100,
289
+ defaultHeight: 3,
290
+ render: () => <StatusBar {...createMockStatusBarProps()} />,
291
+ },
292
+ 'provider-card': {
293
+ name: 'ProviderCard',
294
+ description: 'Provider usage card with gauges (configured state)',
295
+ defaultWidth: 50,
296
+ defaultHeight: 20,
297
+ render: () => <ProviderCard {...createMockProviderCardProps()} />,
298
+ },
299
+ 'provider-card-loading': {
300
+ name: 'ProviderCard (Loading)',
301
+ description: 'Provider card in loading state with skeleton',
302
+ defaultWidth: 50,
303
+ defaultHeight: 20,
304
+ render: () => <ProviderCard {...createMockProviderCardLoadingProps()} />,
305
+ },
306
+ 'provider-card-unconfigured': {
307
+ name: 'ProviderCard (Unconfigured)',
308
+ description: 'Provider card not configured state',
309
+ defaultWidth: 50,
310
+ defaultHeight: 20,
311
+ render: () => <ProviderCard {...createMockProviderCardUnconfiguredProps()} />,
312
+ },
313
+ 'provider-card-error': {
314
+ name: 'ProviderCard (Error)',
315
+ description: 'Provider card with error state',
316
+ defaultWidth: 50,
317
+ defaultHeight: 20,
318
+ render: () => <ProviderCard {...createMockProviderCardErrorProps()} />,
319
+ },
320
+ 'usage-gauge': {
321
+ name: 'UsageGauge',
322
+ description: 'Single usage gauge with progress bar',
323
+ defaultWidth: 45,
324
+ defaultHeight: 5,
325
+ render: () => <UsageGauge {...createMockUsageGaugeProps()} />,
326
+ },
327
+ 'toast': {
328
+ name: 'Toast (Success)',
329
+ description: 'Toast notification - success variant',
330
+ defaultWidth: 50,
331
+ defaultHeight: 8,
332
+ render: () => <Toast {...createMockToastProps()} />,
333
+ },
334
+ 'toast-error': {
335
+ name: 'Toast (Error)',
336
+ description: 'Toast notification - error variant',
337
+ defaultWidth: 50,
338
+ defaultHeight: 8,
339
+ render: () => <Toast {...createMockToastErrorProps()} />,
340
+ },
341
+ 'toast-warning': {
342
+ name: 'Toast (Warning)',
343
+ description: 'Toast notification - warning variant',
344
+ defaultWidth: 50,
345
+ defaultHeight: 8,
346
+ render: () => <Toast {...createMockToastWarningProps()} />,
347
+ },
348
+ 'spinner': {
349
+ name: 'Spinner',
350
+ description: 'Animated loading spinner',
351
+ defaultWidth: 10,
352
+ defaultHeight: 3,
353
+ render: () => <Spinner {...createMockSpinnerProps()} />,
354
+ },
355
+ 'skeleton-text': {
356
+ name: 'SkeletonText',
357
+ description: 'Shimmer loading placeholder for text',
358
+ defaultWidth: 20,
359
+ defaultHeight: 3,
360
+ render: () => <SkeletonText width={15} />,
361
+ },
362
+ 'skeleton-gauge': {
363
+ name: 'SkeletonGauge',
364
+ description: 'Shimmer loading placeholder for gauge',
365
+ defaultWidth: 45,
366
+ defaultHeight: 5,
367
+ render: () => <SkeletonGauge barWidth={30} />,
368
+ },
369
+ 'skeleton-provider': {
370
+ name: 'SkeletonProviderContent',
371
+ description: 'Full skeleton for provider card content',
372
+ defaultWidth: 45,
373
+ defaultHeight: 12,
374
+ render: () => <SkeletonProviderContent />,
375
+ },
376
+ 'debug-console': {
377
+ name: 'DebugConsole',
378
+ description: 'Debug console with log entries',
379
+ defaultWidth: 100,
380
+ defaultHeight: 20,
381
+ render: () => <DebugConsole height={15} follow={true} />,
382
+ },
383
+ 'kpi-strip': {
384
+ name: 'KpiStrip',
385
+ description: 'KPI strip with activity sparkline ramp',
386
+ defaultWidth: 120,
387
+ defaultHeight: 6,
388
+ render: () => <KpiStrip {...createMockKpiStripProps()} />,
389
+ },
390
+ 'historical-trends': {
391
+ name: 'HistoricalTrendsView',
392
+ description: 'ASCII chart showing cost trends over time',
393
+ defaultWidth: 80,
394
+ defaultHeight: 20,
395
+ render: () => <HistoricalTrendsView />,
396
+ },
397
+ 'projects-view': {
398
+ name: 'ProjectsView',
399
+ description: 'Sessions grouped by project with cost breakdown',
400
+ defaultWidth: 100,
401
+ defaultHeight: 25,
402
+ render: () => (
403
+ <InputProvider>
404
+ <StorageProvider>
405
+ <TimeWindowProvider defaultWindow="24h">
406
+ <PluginProvider>
407
+ <AgentSessionProvider autoRefresh={false}>
408
+ <ProjectsView />
409
+ </AgentSessionProvider>
410
+ </PluginProvider>
411
+ </TimeWindowProvider>
412
+ </StorageProvider>
413
+ </InputProvider>
414
+ ),
415
+ },
416
+ 'settings-view': {
417
+ name: 'SettingsView',
418
+ description: 'Application settings with categories',
419
+ defaultWidth: 80,
420
+ defaultHeight: 25,
421
+ render: () => (
422
+ <ConfigProvider initialConfig={DEFAULT_CONFIG}>
423
+ <ToastProvider>
424
+ <SettingsView />
425
+ </ToastProvider>
426
+ </ConfigProvider>
427
+ ),
428
+ },
429
+ 'session-details': {
430
+ name: 'SessionDetailsDrawer',
431
+ description: 'Detailed view of a session',
432
+ defaultWidth: 100,
433
+ defaultHeight: 30,
434
+ render: () => <SessionDetailsDrawer {...createMockSessionDetailsDrawerProps()} />,
435
+ },
436
+ };
437
+
438
+ interface SnapshotOptions {
439
+ width?: number;
440
+ height?: number;
441
+ output?: string;
442
+ component: string;
443
+ list: boolean;
444
+ all: boolean;
445
+ }
446
+
447
+ function parseArgs(): SnapshotOptions {
448
+ const args = process.argv.slice(2);
449
+ const options: SnapshotOptions = {
450
+ component: 'debug-inspector',
451
+ list: false,
452
+ all: false,
453
+ };
454
+
455
+ for (let i = 0; i < args.length; i++) {
456
+ const arg = args[i]!;
457
+ if (arg === '--list' || arg === '-l') {
458
+ options.list = true;
459
+ } else if (arg === '--all' || arg === '-a') {
460
+ options.all = true;
461
+ } else if (arg === '--width' || arg === '-w') {
462
+ const val = args[i + 1];
463
+ if (val) {
464
+ options.width = parseInt(val, 10);
465
+ i++;
466
+ }
467
+ } else if (arg === '--height' || arg === '-h') {
468
+ const val = args[i + 1];
469
+ if (val) {
470
+ options.height = parseInt(val, 10);
471
+ i++;
472
+ }
473
+ } else if (arg === '--output' || arg === '-o') {
474
+ const val = args[i + 1];
475
+ if (val) {
476
+ options.output = val;
477
+ i++;
478
+ }
479
+ } else if (arg === '--help') {
480
+ printHelp();
481
+ process.exit(0);
482
+ } else if (!arg.startsWith('-')) {
483
+ options.component = arg;
484
+ }
485
+ }
486
+
487
+ return options;
488
+ }
489
+
490
+ function printHelp(): void {
491
+ console.log(`
492
+ Headless Component Snapshot Tool
493
+
494
+ Usage:
495
+ bun src/tui/debug/snapshot.tsx [component] [options]
496
+
497
+ Options:
498
+ --list, -l List all available components
499
+ --all, -a Snapshot all components
500
+ --width, -w <n> Set terminal width (default: component-specific)
501
+ --height, -h <n> Set terminal height (default: component-specific)
502
+ --output, -o <path> Output file path (default: auto-generated)
503
+ --help Show this help message
504
+
505
+ Examples:
506
+ bun src/tui/debug/snapshot.tsx --list
507
+ bun src/tui/debug/snapshot.tsx debug-inspector
508
+ bun src/tui/debug/snapshot.tsx provider-card --width 60 --height 25
509
+ bun src/tui/debug/snapshot.tsx toast --output my-toast.txt
510
+ bun src/tui/debug/snapshot.tsx --all
511
+ `);
512
+ }
513
+
514
+ function printComponentList(): void {
515
+ console.log('\nAvailable Components:\n');
516
+ console.log(' ID Description');
517
+ console.log(' ' + '-'.repeat(70));
518
+
519
+ for (const [id, entry] of Object.entries(COMPONENT_REGISTRY)) {
520
+ const paddedId = id.padEnd(28);
521
+ console.log(` ${paddedId}${entry.description}`);
522
+ }
523
+
524
+ console.log('\nUsage: bun src/tui/debug/snapshot.tsx <component-id>\n');
525
+ }
526
+
527
+ async function snapshotComponent(
528
+ componentId: string,
529
+ width?: number,
530
+ height?: number
531
+ ): Promise<string> {
532
+ const entry = COMPONENT_REGISTRY[componentId];
533
+ if (!entry) {
534
+ throw new Error(`Unknown component: ${componentId}`);
535
+ }
536
+
537
+ const finalWidth = width ?? entry.defaultWidth;
538
+ const finalHeight = height ?? entry.defaultHeight;
539
+
540
+ if (componentId === 'kpi-strip') {
541
+ const driver = await createDriver({ width: finalWidth, height: finalHeight });
542
+ await driver.launch();
543
+ await driver.waitForStable();
544
+ const frame = await driver.capture();
545
+ await driver.close();
546
+ return frame;
547
+ }
548
+
549
+ const element = entry.render();
550
+
551
+ const wrappedElement = (
552
+ <ThemeProvider>
553
+ <LogProvider>
554
+ <box width={finalWidth} height={finalHeight}>
555
+ {element}
556
+ </box>
557
+ </LogProvider>
558
+ </ThemeProvider>
559
+ );
560
+
561
+ const testSetup = await testRender(wrappedElement, {
562
+ width: finalWidth,
563
+ height: finalHeight,
564
+ });
565
+
566
+ await testSetup.renderOnce();
567
+ const frame = testSetup.captureCharFrame();
568
+ testSetup.renderer.destroy();
569
+
570
+ return frame;
571
+ }
572
+
573
+ async function saveSnapshot(
574
+ componentId: string,
575
+ frame: string,
576
+ outputPath?: string
577
+ ): Promise<string> {
578
+ const finalPath = outputPath ?? path.join(
579
+ getFramesDir(),
580
+ `snapshot-${componentId}-${new Date().toISOString().replace(/[:.]/g, '-')}.txt`
581
+ );
582
+
583
+ await fs.mkdir(path.dirname(finalPath), { recursive: true });
584
+ await fs.writeFile(finalPath, frame, 'utf-8');
585
+
586
+ return finalPath;
587
+ }
588
+
589
+ async function main() {
590
+ const options = parseArgs();
591
+
592
+ if (options.list) {
593
+ printComponentList();
594
+ return;
595
+ }
596
+
597
+ if (options.all) {
598
+ console.log('Capturing all components...\n');
599
+ const framesDir = getFramesDir();
600
+ const allDir = path.join(framesDir, `snapshot-all-${new Date().toISOString().replace(/[:.]/g, '-')}`);
601
+ await fs.mkdir(allDir, { recursive: true });
602
+
603
+ for (const [id, entry] of Object.entries(COMPONENT_REGISTRY)) {
604
+ try {
605
+ const frame = await snapshotComponent(id, options.width, options.height);
606
+ const outputPath = path.join(allDir, `${id}.txt`);
607
+ await fs.writeFile(outputPath, frame, 'utf-8');
608
+ console.log(` ✓ ${entry.name} -> ${id}.txt`);
609
+ } catch (err) {
610
+ console.error(` ✗ ${entry.name}: ${err}`);
611
+ }
612
+ }
613
+
614
+ console.log(`\nAll snapshots saved to: ${allDir}`);
615
+ return;
616
+ }
617
+
618
+ const entry = COMPONENT_REGISTRY[options.component];
619
+ if (!entry) {
620
+ console.error(`Unknown component: ${options.component}`);
621
+ console.error('\nAvailable components:');
622
+ for (const id of Object.keys(COMPONENT_REGISTRY)) {
623
+ console.error(` - ${id}`);
624
+ }
625
+ process.exit(1);
626
+ }
627
+
628
+ const width = options.width ?? entry.defaultWidth;
629
+ const height = options.height ?? entry.defaultHeight;
630
+
631
+ console.log(`Capturing ${entry.name} at ${width}x${height}...`);
632
+
633
+ const frame = await snapshotComponent(options.component, width, height);
634
+ const outputPath = await saveSnapshot(options.component, frame, options.output);
635
+
636
+ console.log(`Snapshot saved to: ${outputPath}`);
637
+ console.log('\n--- Preview ---');
638
+ console.log(frame);
639
+ }
640
+
641
+ main().catch((err) => {
642
+ console.error('Snapshot failed:', err);
643
+ process.exit(1);
644
+ });