@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,279 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { diffFrames, highlightDiff, type DiffResult } from './diff.ts';
4
+
5
+ const DEFAULT_GOLDEN_DIR = './golden';
6
+
7
+ export interface GoldenFile {
8
+ version: 1;
9
+ width: number;
10
+ height: number;
11
+ frame: string;
12
+ createdAt: string;
13
+ updatedAt: string;
14
+ }
15
+
16
+ export interface AssertResult {
17
+ passed: boolean;
18
+ goldenExists: boolean;
19
+ diff?: DiffResult;
20
+ visual?: string;
21
+ goldenPath: string;
22
+ message: string;
23
+ dimensionMismatch?: {
24
+ expected: { width: number; height: number };
25
+ actual: { width: number; height: number };
26
+ };
27
+ }
28
+
29
+ export interface AssertOptions {
30
+ goldenDir?: string;
31
+ updateGolden?: boolean;
32
+ ignoreWhitespace?: boolean;
33
+ width?: number;
34
+ height?: number;
35
+ }
36
+
37
+ function parseGoldenFile(content: string): GoldenFile | null {
38
+ try {
39
+ const parsed = JSON.parse(content);
40
+ if (parsed.version === 1 && typeof parsed.frame === 'string') {
41
+ return parsed as GoldenFile;
42
+ }
43
+ return null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function createGoldenContent(frame: string, width: number, height: number, existingCreatedAt?: string): string {
50
+ const now = new Date().toISOString();
51
+ const golden: GoldenFile = {
52
+ version: 1,
53
+ width,
54
+ height,
55
+ frame,
56
+ createdAt: existingCreatedAt ?? now,
57
+ updatedAt: now,
58
+ };
59
+ return JSON.stringify(golden, null, 2);
60
+ }
61
+
62
+ async function tryReadFile(filePath: string): Promise<string | null> {
63
+ try {
64
+ return await fs.readFile(filePath, 'utf-8');
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ async function tryUnlink(filePath: string): Promise<void> {
71
+ try {
72
+ await fs.unlink(filePath);
73
+ } catch {
74
+ }
75
+ }
76
+
77
+ export async function assertSnapshot(
78
+ name: string,
79
+ actual: string,
80
+ options: AssertOptions = {}
81
+ ): Promise<AssertResult> {
82
+ const {
83
+ goldenDir = DEFAULT_GOLDEN_DIR,
84
+ updateGolden = false,
85
+ ignoreWhitespace = false,
86
+ width,
87
+ height,
88
+ } = options;
89
+
90
+ const goldenPath = path.join(goldenDir, `${name}.golden.json`);
91
+ const legacyPath = path.join(goldenDir, `${name}.golden.txt`);
92
+
93
+ await fs.mkdir(goldenDir, { recursive: true });
94
+
95
+ let goldenExists = false;
96
+ let golden: GoldenFile | null = null;
97
+ let actualGoldenPath = goldenPath;
98
+ let legacyContent: string | null = null;
99
+
100
+ const jsonContent = await tryReadFile(goldenPath);
101
+ if (jsonContent !== null) {
102
+ golden = parseGoldenFile(jsonContent);
103
+ goldenExists = true;
104
+ } else {
105
+ legacyContent = await tryReadFile(legacyPath);
106
+ if (legacyContent !== null) {
107
+ goldenExists = true;
108
+ actualGoldenPath = legacyPath;
109
+ }
110
+ }
111
+
112
+ if (!goldenExists || updateGolden) {
113
+ if (width === undefined || height === undefined) {
114
+ return {
115
+ passed: false,
116
+ goldenExists,
117
+ goldenPath: path.resolve(goldenPath),
118
+ message: 'Cannot create golden file: width and height are required',
119
+ };
120
+ }
121
+
122
+ const content = createGoldenContent(actual, width, height, golden?.createdAt);
123
+ await fs.writeFile(goldenPath, content, 'utf-8');
124
+
125
+ if (actualGoldenPath === legacyPath) {
126
+ await tryUnlink(legacyPath);
127
+ }
128
+
129
+ const action = goldenExists ? 'Updated' : 'Created new';
130
+ return {
131
+ passed: true,
132
+ goldenExists: !goldenExists ? false : true,
133
+ goldenPath: path.resolve(goldenPath),
134
+ message: `${action} golden file: ${goldenPath} (${width}x${height})`,
135
+ };
136
+ }
137
+
138
+ if (golden === null && legacyContent !== null) {
139
+ const diff = diffFrames(legacyContent, actual, { ignoreWhitespace });
140
+
141
+ if (diff.identical) {
142
+ return {
143
+ passed: true,
144
+ goldenExists: true,
145
+ diff,
146
+ goldenPath: path.resolve(actualGoldenPath),
147
+ message: `Snapshot matches golden file (legacy format - consider updating with dimensions)`,
148
+ };
149
+ }
150
+
151
+ return {
152
+ passed: false,
153
+ goldenExists: true,
154
+ diff,
155
+ visual: highlightDiff(legacyContent, actual, { ignoreWhitespace }),
156
+ goldenPath: path.resolve(actualGoldenPath),
157
+ message: `Snapshot differs from golden file (legacy format): ${diff.changedLines}/${diff.totalLines} lines changed (${diff.changePercentage.toFixed(1)}%)`,
158
+ };
159
+ }
160
+
161
+ if (golden === null) {
162
+ return {
163
+ passed: false,
164
+ goldenExists: false,
165
+ goldenPath: path.resolve(goldenPath),
166
+ message: 'Golden file is corrupted or in unknown format',
167
+ };
168
+ }
169
+
170
+ if (width !== undefined && height !== undefined) {
171
+ if (golden.width !== width || golden.height !== height) {
172
+ return {
173
+ passed: false,
174
+ goldenExists: true,
175
+ goldenPath: path.resolve(goldenPath),
176
+ message: `Terminal size mismatch: golden was captured at ${golden.width}x${golden.height}, current is ${width}x${height}`,
177
+ dimensionMismatch: {
178
+ expected: { width: golden.width, height: golden.height },
179
+ actual: { width, height },
180
+ },
181
+ };
182
+ }
183
+ }
184
+
185
+ const diff = diffFrames(golden.frame, actual, { ignoreWhitespace });
186
+
187
+ if (diff.identical) {
188
+ return {
189
+ passed: true,
190
+ goldenExists: true,
191
+ diff,
192
+ goldenPath: path.resolve(goldenPath),
193
+ message: `Snapshot matches golden file (${golden.width}x${golden.height})`,
194
+ };
195
+ }
196
+
197
+ return {
198
+ passed: false,
199
+ goldenExists: true,
200
+ diff,
201
+ visual: highlightDiff(golden.frame, actual, { ignoreWhitespace }),
202
+ goldenPath: path.resolve(goldenPath),
203
+ message: `Snapshot differs from golden file: ${diff.changedLines}/${diff.totalLines} lines changed (${diff.changePercentage.toFixed(1)}%)`,
204
+ };
205
+ }
206
+
207
+ export async function listGoldenFiles(goldenDir: string = DEFAULT_GOLDEN_DIR): Promise<string[]> {
208
+ try {
209
+ const files = await fs.readdir(goldenDir);
210
+ return files
211
+ .filter((f) => f.endsWith('.golden.json') || f.endsWith('.golden.txt'))
212
+ .map((f) => f.replace('.golden.json', '').replace('.golden.txt', ''));
213
+ } catch {
214
+ return [];
215
+ }
216
+ }
217
+
218
+ export async function deleteGoldenFile(name: string, goldenDir: string = DEFAULT_GOLDEN_DIR): Promise<boolean> {
219
+ const jsonPath = path.join(goldenDir, `${name}.golden.json`);
220
+ const txtPath = path.join(goldenDir, `${name}.golden.txt`);
221
+
222
+ let deleted = false;
223
+
224
+ try {
225
+ await fs.unlink(jsonPath);
226
+ deleted = true;
227
+ } catch {
228
+ }
229
+
230
+ try {
231
+ await fs.unlink(txtPath);
232
+ deleted = true;
233
+ } catch {
234
+ }
235
+
236
+ return deleted;
237
+ }
238
+
239
+ export interface GoldenFileInfo {
240
+ name: string;
241
+ content: string;
242
+ width?: number;
243
+ height?: number;
244
+ createdAt?: string;
245
+ updatedAt?: string;
246
+ isLegacy: boolean;
247
+ }
248
+
249
+ export async function getGoldenFile(name: string, goldenDir: string = DEFAULT_GOLDEN_DIR): Promise<GoldenFileInfo | null> {
250
+ const jsonPath = path.join(goldenDir, `${name}.golden.json`);
251
+ const txtPath = path.join(goldenDir, `${name}.golden.txt`);
252
+
253
+ const jsonContent = await tryReadFile(jsonPath);
254
+ if (jsonContent !== null) {
255
+ const golden = parseGoldenFile(jsonContent);
256
+ if (golden) {
257
+ return {
258
+ name,
259
+ content: golden.frame,
260
+ width: golden.width,
261
+ height: golden.height,
262
+ createdAt: golden.createdAt,
263
+ updatedAt: golden.updatedAt,
264
+ isLegacy: false,
265
+ };
266
+ }
267
+ }
268
+
269
+ const txtContent = await tryReadFile(txtPath);
270
+ if (txtContent !== null) {
271
+ return {
272
+ name,
273
+ content: txtContent,
274
+ isLegacy: true,
275
+ };
276
+ }
277
+
278
+ return null;
279
+ }