@appkit/llamacpp-cli 1.12.0 → 1.12.1

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 (114) hide show
  1. package/README.md +217 -168
  2. package/package.json +10 -2
  3. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  4. package/web/dist/assets/index-CVmonw3T.js +17 -0
  5. package/web/{index.html → dist/index.html} +2 -1
  6. package/.versionrc.json +0 -16
  7. package/CHANGELOG.md +0 -213
  8. package/docs/images/.gitkeep +0 -1
  9. package/docs/images/web-ui-servers.png +0 -0
  10. package/src/cli.ts +0 -523
  11. package/src/commands/admin/config.ts +0 -121
  12. package/src/commands/admin/logs.ts +0 -91
  13. package/src/commands/admin/restart.ts +0 -26
  14. package/src/commands/admin/start.ts +0 -27
  15. package/src/commands/admin/status.ts +0 -84
  16. package/src/commands/admin/stop.ts +0 -16
  17. package/src/commands/config-global.ts +0 -38
  18. package/src/commands/config.ts +0 -323
  19. package/src/commands/create.ts +0 -183
  20. package/src/commands/delete.ts +0 -74
  21. package/src/commands/list.ts +0 -37
  22. package/src/commands/logs-all.ts +0 -251
  23. package/src/commands/logs.ts +0 -345
  24. package/src/commands/monitor.ts +0 -110
  25. package/src/commands/ps.ts +0 -84
  26. package/src/commands/pull.ts +0 -44
  27. package/src/commands/rm.ts +0 -107
  28. package/src/commands/router/config.ts +0 -116
  29. package/src/commands/router/logs.ts +0 -256
  30. package/src/commands/router/restart.ts +0 -36
  31. package/src/commands/router/start.ts +0 -60
  32. package/src/commands/router/status.ts +0 -119
  33. package/src/commands/router/stop.ts +0 -33
  34. package/src/commands/run.ts +0 -233
  35. package/src/commands/search.ts +0 -107
  36. package/src/commands/server-show.ts +0 -161
  37. package/src/commands/show.ts +0 -207
  38. package/src/commands/start.ts +0 -101
  39. package/src/commands/stop.ts +0 -39
  40. package/src/commands/tui.ts +0 -25
  41. package/src/lib/admin-manager.ts +0 -435
  42. package/src/lib/admin-server.ts +0 -1243
  43. package/src/lib/config-generator.ts +0 -130
  44. package/src/lib/download-job-manager.ts +0 -213
  45. package/src/lib/history-manager.ts +0 -172
  46. package/src/lib/launchctl-manager.ts +0 -225
  47. package/src/lib/metrics-aggregator.ts +0 -257
  48. package/src/lib/model-downloader.ts +0 -328
  49. package/src/lib/model-scanner.ts +0 -157
  50. package/src/lib/model-search.ts +0 -114
  51. package/src/lib/models-dir-setup.ts +0 -46
  52. package/src/lib/port-manager.ts +0 -80
  53. package/src/lib/router-logger.ts +0 -201
  54. package/src/lib/router-manager.ts +0 -414
  55. package/src/lib/router-server.ts +0 -538
  56. package/src/lib/state-manager.ts +0 -206
  57. package/src/lib/status-checker.ts +0 -113
  58. package/src/lib/system-collector.ts +0 -315
  59. package/src/tui/ConfigApp.ts +0 -1085
  60. package/src/tui/HistoricalMonitorApp.ts +0 -587
  61. package/src/tui/ModelsApp.ts +0 -368
  62. package/src/tui/MonitorApp.ts +0 -386
  63. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  64. package/src/tui/RootNavigator.ts +0 -74
  65. package/src/tui/SearchApp.ts +0 -511
  66. package/src/tui/SplashScreen.ts +0 -149
  67. package/src/types/admin-config.ts +0 -25
  68. package/src/types/global-config.ts +0 -26
  69. package/src/types/history-types.ts +0 -39
  70. package/src/types/model-info.ts +0 -8
  71. package/src/types/monitor-types.ts +0 -162
  72. package/src/types/router-config.ts +0 -25
  73. package/src/types/server-config.ts +0 -46
  74. package/src/utils/downsample-utils.ts +0 -128
  75. package/src/utils/file-utils.ts +0 -146
  76. package/src/utils/format-utils.ts +0 -98
  77. package/src/utils/log-parser.ts +0 -284
  78. package/src/utils/log-utils.ts +0 -178
  79. package/src/utils/process-utils.ts +0 -316
  80. package/src/utils/prompt-utils.ts +0 -47
  81. package/test-load.sh +0 -100
  82. package/tsconfig.json +0 -20
  83. package/web/eslint.config.js +0 -23
  84. package/web/llamacpp-web-dist.tar.gz +0 -0
  85. package/web/package-lock.json +0 -4017
  86. package/web/package.json +0 -38
  87. package/web/postcss.config.js +0 -6
  88. package/web/src/App.css +0 -42
  89. package/web/src/App.tsx +0 -86
  90. package/web/src/assets/react.svg +0 -1
  91. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  92. package/web/src/components/CreateServerModal.tsx +0 -372
  93. package/web/src/components/DownloadProgress.tsx +0 -123
  94. package/web/src/components/Nav.tsx +0 -89
  95. package/web/src/components/RouterConfigModal.tsx +0 -240
  96. package/web/src/components/SearchModal.tsx +0 -306
  97. package/web/src/components/ServerConfigModal.tsx +0 -291
  98. package/web/src/hooks/useApi.ts +0 -259
  99. package/web/src/index.css +0 -42
  100. package/web/src/lib/api.ts +0 -226
  101. package/web/src/main.tsx +0 -10
  102. package/web/src/pages/Dashboard.tsx +0 -103
  103. package/web/src/pages/Models.tsx +0 -258
  104. package/web/src/pages/Router.tsx +0 -270
  105. package/web/src/pages/RouterLogs.tsx +0 -201
  106. package/web/src/pages/ServerLogs.tsx +0 -553
  107. package/web/src/pages/Servers.tsx +0 -358
  108. package/web/src/types/api.ts +0 -140
  109. package/web/tailwind.config.js +0 -31
  110. package/web/tsconfig.app.json +0 -28
  111. package/web/tsconfig.json +0 -7
  112. package/web/tsconfig.node.json +0 -26
  113. package/web/vite.config.ts +0 -25
  114. /package/web/{public → dist}/vite.svg +0 -0
@@ -1,587 +0,0 @@
1
- import blessed from 'blessed';
2
- import * as asciichart from 'asciichart';
3
- import { ServerConfig } from '../types/server-config.js';
4
- import { HistoryManager } from '../lib/history-manager.js';
5
- import { HistorySnapshot } from '../types/history-types.js';
6
- import {
7
- downsampleMaxTimeWithFullHour,
8
- downsampleMeanTimeWithFullHour,
9
- getDownsampleRatio,
10
- TimeSeriesPoint
11
- } from '../utils/downsample-utils.js';
12
-
13
- type ViewMode = 'recent' | 'hour';
14
-
15
- // Shared view mode across both history screens - persists for the session
16
- let sharedViewMode: ViewMode = 'recent';
17
-
18
- interface ChartStats {
19
- avg: number;
20
- max: number;
21
- min: number;
22
- stddev: number;
23
- }
24
-
25
- interface ChartConfig {
26
- title: string;
27
- color: typeof asciichart.cyan;
28
- formatValue: (x: number) => string;
29
- isPercentage: boolean;
30
- noDataMessage: string;
31
- }
32
-
33
- /**
34
- * Calculate statistics for a set of values.
35
- */
36
- function calculateStats(values: number[]): ChartStats {
37
- if (values.length === 0) {
38
- return { avg: 0, max: 0, min: 0, stddev: 0 };
39
- }
40
-
41
- const avg = values.reduce((a, b) => a + b, 0) / values.length;
42
- const max = Math.max(...values);
43
- const min = Math.min(...values);
44
- const variance = values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;
45
- const stddev = Math.sqrt(variance);
46
-
47
- return { avg, max, min, stddev };
48
- }
49
-
50
- /**
51
- * Calculate expanded range for chart y-axis to prevent duplicate labels.
52
- */
53
- function getExpandedRange(data: number[], isPercentage: boolean): { min: number; max: number } {
54
- if (isPercentage) return { min: 0, max: 100 };
55
- if (data.length === 0) return { min: 0, max: 10 };
56
-
57
- const dataMin = Math.min(...data);
58
- const dataMax = Math.max(...data);
59
- const range = dataMax - dataMin;
60
- const padding = Math.max(range * 0.3, 5);
61
-
62
- return {
63
- min: Math.max(0, Math.floor(dataMin - padding)),
64
- max: Math.ceil(dataMax + padding)
65
- };
66
- }
67
-
68
- /**
69
- * Create a scrollable content box for historical charts.
70
- */
71
- function createContentBox(): blessed.Widgets.BoxElement {
72
- return blessed.box({
73
- top: 0,
74
- left: 0,
75
- width: '100%',
76
- height: '100%',
77
- tags: true,
78
- scrollable: true,
79
- alwaysScroll: true,
80
- keys: true,
81
- vi: true,
82
- mouse: true,
83
- scrollbar: {
84
- ch: '\u2588',
85
- style: { fg: 'blue' },
86
- },
87
- });
88
- }
89
-
90
- /**
91
- * Render a single chart with statistics.
92
- */
93
- function renderChart(
94
- values: number[],
95
- rawValues: TimeSeriesPoint[],
96
- config: ChartConfig,
97
- chartHeight: number
98
- ): string {
99
- let content = `{bold}${config.title}{/bold}\n`;
100
- const validValues = values.filter(v => !isNaN(v) && v > 0);
101
- const plotData = values.map(v => isNaN(v) ? 0 : v);
102
-
103
- try {
104
- if (validValues.length >= 2) {
105
- const range = getExpandedRange(validValues, config.isPercentage);
106
- content += asciichart.plot(plotData, {
107
- height: chartHeight,
108
- colors: [config.color],
109
- format: config.formatValue,
110
- min: range.min,
111
- max: range.max,
112
- });
113
- content += '\n';
114
-
115
- const stats = calculateStats(validValues);
116
- const lastValue = rawValues[rawValues.length - 1]?.value ?? 0;
117
-
118
- if (config.title.includes('GB')) {
119
- content += ` Avg: ${stats.avg.toFixed(2)} GB (\u00b1${stats.stddev.toFixed(2)}) `;
120
- content += `Max: ${stats.max.toFixed(2)} GB `;
121
- content += `Min: ${stats.min.toFixed(2)} GB `;
122
- content += `Last: ${lastValue.toFixed(2)} GB\n\n`;
123
- } else if (config.isPercentage) {
124
- content += ` Avg: ${stats.avg.toFixed(1)}% (\u00b1${stats.stddev.toFixed(1)}) `;
125
- content += `Max: ${stats.max.toFixed(1)}% `;
126
- content += `Min: ${stats.min.toFixed(1)}% `;
127
- content += `Last: ${lastValue.toFixed(1)}%\n\n`;
128
- } else {
129
- content += ` Avg: ${stats.avg.toFixed(1)} tok/s (\u00b1${stats.stddev.toFixed(1)}) `;
130
- content += `Max: ${stats.max.toFixed(1)} tok/s `;
131
- content += `Last: ${lastValue.toFixed(1)} tok/s\n\n`;
132
- }
133
- } else {
134
- const defaultRange = config.isPercentage ? { min: 0, max: 100 } : { min: 0, max: 10 };
135
- content += asciichart.plot(plotData, {
136
- height: chartHeight,
137
- colors: [config.color],
138
- format: config.formatValue,
139
- min: defaultRange.min,
140
- max: defaultRange.max,
141
- });
142
- content += `\n{gray-fg} ${config.noDataMessage}{/gray-fg}\n\n`;
143
- }
144
- } catch {
145
- content += '{red-fg} Error rendering chart{/red-fg}\n\n';
146
- }
147
-
148
- return content;
149
- }
150
-
151
- export async function createHistoricalUI(
152
- screen: blessed.Widgets.Screen,
153
- server: ServerConfig,
154
- onBack: () => void
155
- ): Promise<void> {
156
- const historyManager = new HistoryManager(server.id);
157
- let refreshIntervalId: NodeJS.Timeout | null = null;
158
- const REFRESH_INTERVAL = 1000;
159
- let lastGoodRender: string | null = null;
160
- let consecutiveErrors = 0;
161
-
162
- const contentBox = createContentBox();
163
- screen.append(contentBox);
164
-
165
- // Chart configurations
166
- const chartConfigs: Record<string, ChartConfig> = {
167
- tokenSpeed: {
168
- title: 'Server Token Generation Speed (tok/s)',
169
- color: asciichart.cyan,
170
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
171
- isPercentage: false,
172
- noDataMessage: 'No generation activity in this time window',
173
- },
174
- cpu: {
175
- title: 'Server CPU Usage (%)',
176
- color: asciichart.blue,
177
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
178
- isPercentage: true,
179
- noDataMessage: 'No CPU data in this time window',
180
- },
181
- memory: {
182
- title: 'Server Memory Usage (GB)',
183
- color: asciichart.magenta,
184
- formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
185
- isPercentage: false,
186
- noDataMessage: 'No memory data in this time window',
187
- },
188
- gpu: {
189
- title: 'System GPU Usage (%)',
190
- color: asciichart.green,
191
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
192
- isPercentage: true,
193
- noDataMessage: 'No GPU data in this time window',
194
- },
195
- };
196
-
197
- async function render(): Promise<void> {
198
- try {
199
- const termWidth = (screen.width as number) || 80;
200
- const divider = '\u2500'.repeat(termWidth - 2);
201
- const chartHeight = 5;
202
- const chartWidth = Math.min(Math.max(termWidth - 20, 40), 80);
203
-
204
- if (chartWidth <= 0 || !Number.isFinite(chartWidth)) {
205
- contentBox.setContent('{red-fg}Error: Invalid chart width{/red-fg}\n');
206
- screen.render();
207
- return;
208
- }
209
-
210
- // Header
211
- const modeLabel = sharedViewMode === 'recent' ? 'Minute' : 'Hour';
212
- const modeColor = sharedViewMode === 'recent' ? 'cyan' : 'magenta';
213
- let content = `{bold}{blue-fg}\u2550\u2550\u2550 ${server.modelName} (${server.port}) {/blue-fg} `;
214
- content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
215
-
216
- const snapshots = await historyManager.loadHistoryByWindow('1h');
217
-
218
- if (snapshots.length === 0) {
219
- content += '{yellow-fg}No historical data available.{/yellow-fg}\n\n';
220
- content += 'Historical data is collected when you run the monitor command.\n';
221
- content += 'Start monitoring to begin collecting history.\n\n';
222
- content += divider + '\n';
223
- content += '{gray-fg}[ESC] Back [Q]uit{/gray-fg}';
224
- contentBox.setContent(content);
225
- screen.render();
226
- return;
227
- }
228
-
229
- const maxChartPoints = Math.min(chartWidth, 80);
230
- const displaySnapshots = sharedViewMode === 'recent' && snapshots.length > maxChartPoints
231
- ? snapshots.slice(-maxChartPoints)
232
- : snapshots;
233
-
234
- // Extract time-series data
235
- const rawData = {
236
- tokenSpeed: [] as TimeSeriesPoint[],
237
- gpu: [] as TimeSeriesPoint[],
238
- cpu: [] as TimeSeriesPoint[],
239
- memory: [] as TimeSeriesPoint[],
240
- };
241
-
242
- for (const snapshot of displaySnapshots) {
243
- const ts = snapshot.timestamp;
244
- rawData.tokenSpeed.push({ timestamp: ts, value: snapshot.server.avgGenerateSpeed || 0 });
245
- rawData.gpu.push({ timestamp: ts, value: snapshot.system?.gpuUsage || 0 });
246
- rawData.cpu.push({ timestamp: ts, value: snapshot.server.processCpuUsage || 0 });
247
- rawData.memory.push({
248
- timestamp: ts,
249
- value: snapshot.server.processMemory ? snapshot.server.processMemory / (1024 ** 3) : 0
250
- });
251
- }
252
-
253
- // Apply downsampling based on view mode
254
- const useDownsampling = sharedViewMode === 'hour';
255
- const values = {
256
- tokenSpeed: useDownsampling
257
- ? downsampleMaxTimeWithFullHour(rawData.tokenSpeed, maxChartPoints)
258
- : rawData.tokenSpeed.map(p => p.value),
259
- cpu: useDownsampling
260
- ? downsampleMaxTimeWithFullHour(rawData.cpu, maxChartPoints)
261
- : rawData.cpu.map(p => p.value),
262
- memory: useDownsampling
263
- ? downsampleMeanTimeWithFullHour(rawData.memory, maxChartPoints)
264
- : rawData.memory.map(p => p.value),
265
- gpu: useDownsampling
266
- ? downsampleMaxTimeWithFullHour(rawData.gpu, maxChartPoints)
267
- : rawData.gpu.map(p => p.value),
268
- };
269
-
270
- // Render all charts
271
- content += renderChart(values.tokenSpeed, rawData.tokenSpeed, chartConfigs.tokenSpeed, chartHeight);
272
- content += renderChart(values.cpu, rawData.cpu, chartConfigs.cpu, chartHeight);
273
- content += renderChart(values.memory, rawData.memory, chartConfigs.memory, chartHeight);
274
- content += renderChart(values.gpu, rawData.gpu, chartConfigs.gpu, chartHeight);
275
-
276
- // Footer
277
- content += divider + '\n';
278
- content += `{gray-fg}[T]oggle Hour View [ESC] Back [Q]uit{/gray-fg}`;
279
-
280
- contentBox.setContent(content);
281
- screen.render();
282
-
283
- lastGoodRender = content;
284
- consecutiveErrors = 0;
285
- } catch (error) {
286
- consecutiveErrors++;
287
- if (lastGoodRender && consecutiveErrors < 5) {
288
- contentBox.setContent(lastGoodRender);
289
- } else {
290
- const errorMsg = error instanceof Error ? error.message : String(error);
291
- contentBox.setContent(
292
- '{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
293
- `{red-fg}${errorMsg}{/red-fg}\n\n` +
294
- `Consecutive errors: ${consecutiveErrors}\n\n` +
295
- '{gray-fg}[ESC] Back [Q]uit{/gray-fg}'
296
- );
297
- }
298
- screen.render();
299
- }
300
- }
301
-
302
- function cleanup(): void {
303
- if (refreshIntervalId) {
304
- clearInterval(refreshIntervalId);
305
- refreshIntervalId = null;
306
- }
307
- unregisterHandlers();
308
- }
309
-
310
- // Key handler functions (stored for unregistration)
311
- const keyHandlers = {
312
- toggle: () => {
313
- sharedViewMode = sharedViewMode === 'recent' ? 'hour' : 'recent';
314
- render();
315
- },
316
- escape: () => {
317
- cleanup();
318
- screen.remove(contentBox);
319
- onBack();
320
- },
321
- quit: () => {
322
- cleanup();
323
- screen.destroy();
324
- process.exit(0);
325
- },
326
- };
327
-
328
- function registerHandlers(): void {
329
- screen.key(['t', 'T'], keyHandlers.toggle);
330
- screen.key(['escape'], keyHandlers.escape);
331
- screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
332
- }
333
-
334
- function unregisterHandlers(): void {
335
- screen.unkey('t', keyHandlers.toggle);
336
- screen.unkey('T', keyHandlers.toggle);
337
- screen.unkey('escape', keyHandlers.escape);
338
- screen.unkey('q', keyHandlers.quit);
339
- screen.unkey('Q', keyHandlers.quit);
340
- screen.unkey('C-c', keyHandlers.quit);
341
- }
342
-
343
- registerHandlers();
344
-
345
- contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
346
- screen.render();
347
- await render();
348
-
349
- refreshIntervalId = setInterval(render, REFRESH_INTERVAL);
350
- }
351
-
352
- // Multi-server historical view
353
- export async function createMultiServerHistoricalUI(
354
- screen: blessed.Widgets.Screen,
355
- servers: ServerConfig[],
356
- _selectedIndex: number,
357
- onBack: () => void
358
- ): Promise<void> {
359
- let refreshIntervalId: NodeJS.Timeout | null = null;
360
- const REFRESH_INTERVAL = 3000;
361
- let lastGoodRender: string | null = null;
362
- let consecutiveErrors = 0;
363
-
364
- const contentBox = createContentBox();
365
- screen.append(contentBox);
366
-
367
- // Chart configurations for multi-server view
368
- const chartConfigs: Record<string, ChartConfig> = {
369
- tokenSpeed: {
370
- title: 'Total Server Token Generation Speed (tok/s)',
371
- color: asciichart.cyan,
372
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
373
- isPercentage: false,
374
- noDataMessage: 'No generation activity in this time window',
375
- },
376
- cpu: {
377
- title: 'Total Server CPU Usage (%)',
378
- color: asciichart.blue,
379
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
380
- isPercentage: true,
381
- noDataMessage: 'No CPU data in this time window',
382
- },
383
- memory: {
384
- title: 'Total Server Memory Usage (GB)',
385
- color: asciichart.magenta,
386
- formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
387
- isPercentage: false,
388
- noDataMessage: 'No memory data in this time window',
389
- },
390
- gpu: {
391
- title: 'System GPU Usage (%)',
392
- color: asciichart.green,
393
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
394
- isPercentage: true,
395
- noDataMessage: 'No GPU data in this time window',
396
- },
397
- };
398
-
399
- async function render(): Promise<void> {
400
- try {
401
- const termWidth = (screen.width as number) || 80;
402
- const divider = '\u2500'.repeat(termWidth - 2);
403
- const chartWidth = Math.min(Math.max(40, termWidth - 20), 80);
404
- const chartHeight = 5;
405
-
406
- // Header
407
- const modeLabel = sharedViewMode === 'recent' ? 'Minute' : 'Hour';
408
- const modeColor = sharedViewMode === 'recent' ? 'cyan' : 'magenta';
409
- let content = `{bold}{blue-fg}\u2550\u2550\u2550 All servers (${servers.length}){/blue-fg} `;
410
- content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
411
-
412
- // Load and aggregate history for all servers
413
- const serverHistories = await Promise.all(
414
- servers.map(async (server) => {
415
- const manager = new HistoryManager(server.id);
416
- return manager.loadHistoryByWindow('1h');
417
- })
418
- );
419
-
420
- // Aggregate data across all servers at each timestamp
421
- const ALIGNMENT_INTERVAL = 2000;
422
- const timestampMap = new Map<number, {
423
- tokensPerSec: number[];
424
- gpuUsage: number[];
425
- cpuUsage: number[];
426
- memoryGB: number[];
427
- }>();
428
-
429
- for (const snapshots of serverHistories) {
430
- for (const snapshot of snapshots) {
431
- const timestamp = Math.round(snapshot.timestamp / ALIGNMENT_INTERVAL) * ALIGNMENT_INTERVAL;
432
-
433
- if (!timestampMap.has(timestamp)) {
434
- timestampMap.set(timestamp, {
435
- tokensPerSec: [],
436
- gpuUsage: [],
437
- cpuUsage: [],
438
- memoryGB: [],
439
- });
440
- }
441
-
442
- const data = timestampMap.get(timestamp)!;
443
-
444
- if (snapshot.server.avgGenerateSpeed && snapshot.server.avgGenerateSpeed > 0) {
445
- data.tokensPerSec.push(snapshot.server.avgGenerateSpeed);
446
- }
447
- if (snapshot.system?.gpuUsage !== undefined) {
448
- data.gpuUsage.push(snapshot.system.gpuUsage);
449
- }
450
- if (snapshot.server.processCpuUsage !== undefined) {
451
- data.cpuUsage.push(snapshot.server.processCpuUsage);
452
- }
453
- if (snapshot.server.processMemory) {
454
- data.memoryGB.push(snapshot.server.processMemory / (1024 ** 3));
455
- }
456
- }
457
- }
458
-
459
- // Sort timestamps and aggregate
460
- const timestamps = Array.from(timestampMap.keys()).sort((a, b) => a - b);
461
- const aggregatedData = timestamps.map(ts => {
462
- const data = timestampMap.get(ts)!;
463
- return {
464
- timestamp: ts,
465
- totalTokS: data.tokensPerSec.reduce((a, b) => a + b, 0),
466
- avgGpu: data.gpuUsage.length > 0
467
- ? data.gpuUsage.reduce((a, b) => a + b, 0) / data.gpuUsage.length
468
- : 0,
469
- totalCpu: data.cpuUsage.reduce((a, b) => a + b, 0),
470
- totalMemoryGB: data.memoryGB.reduce((a, b) => a + b, 0),
471
- };
472
- });
473
-
474
- if (aggregatedData.length > 0) {
475
- const maxPoints = Math.min(chartWidth, 80);
476
- const displayData = sharedViewMode === 'recent' && aggregatedData.length > maxPoints
477
- ? aggregatedData.slice(-maxPoints)
478
- : aggregatedData;
479
-
480
- const useDownsampling = sharedViewMode === 'hour';
481
-
482
- // Extract time-series data
483
- const rawData = {
484
- tokenSpeed: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalTokS })),
485
- cpu: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalCpu })),
486
- memory: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalMemoryGB })),
487
- gpu: displayData.map(d => ({ timestamp: d.timestamp, value: d.avgGpu })),
488
- };
489
-
490
- // Apply downsampling
491
- const values = {
492
- tokenSpeed: useDownsampling
493
- ? downsampleMaxTimeWithFullHour(rawData.tokenSpeed, chartWidth)
494
- : rawData.tokenSpeed.map(d => d.value),
495
- cpu: useDownsampling
496
- ? downsampleMaxTimeWithFullHour(rawData.cpu, chartWidth)
497
- : rawData.cpu.map(d => d.value),
498
- memory: useDownsampling
499
- ? downsampleMeanTimeWithFullHour(rawData.memory, chartWidth)
500
- : rawData.memory.map(d => d.value),
501
- gpu: useDownsampling
502
- ? downsampleMaxTimeWithFullHour(rawData.gpu, chartWidth)
503
- : rawData.gpu.map(d => d.value),
504
- };
505
-
506
- // Render all charts
507
- content += renderChart(values.tokenSpeed, rawData.tokenSpeed, chartConfigs.tokenSpeed, chartHeight);
508
- content += renderChart(values.cpu, rawData.cpu, chartConfigs.cpu, chartHeight);
509
- content += renderChart(values.memory, rawData.memory, chartConfigs.memory, chartHeight);
510
- content += renderChart(values.gpu, rawData.gpu, chartConfigs.gpu, chartHeight);
511
- }
512
-
513
- // Footer
514
- content += divider + '\n';
515
- content += `{gray-fg}[T]oggle Hour View [ESC] Back [Q]uit{/gray-fg}`;
516
-
517
- contentBox.setContent(content);
518
- screen.render();
519
-
520
- lastGoodRender = content;
521
- consecutiveErrors = 0;
522
- } catch (error) {
523
- consecutiveErrors++;
524
- if (lastGoodRender && consecutiveErrors < 5) {
525
- contentBox.setContent(lastGoodRender);
526
- } else {
527
- const errorMsg = error instanceof Error ? error.message : String(error);
528
- contentBox.setContent(
529
- '{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
530
- `{red-fg}${errorMsg}{/red-fg}\n\n` +
531
- `Consecutive errors: ${consecutiveErrors}\n\n` +
532
- '{gray-fg}[ESC] Back [Q]uit{/gray-fg}'
533
- );
534
- }
535
- screen.render();
536
- }
537
- }
538
-
539
- function cleanup(): void {
540
- if (refreshIntervalId) {
541
- clearInterval(refreshIntervalId);
542
- refreshIntervalId = null;
543
- }
544
- unregisterHandlers();
545
- }
546
-
547
- // Key handler functions (stored for unregistration)
548
- const keyHandlers = {
549
- toggle: () => {
550
- sharedViewMode = sharedViewMode === 'recent' ? 'hour' : 'recent';
551
- render();
552
- },
553
- escape: () => {
554
- cleanup();
555
- screen.remove(contentBox);
556
- onBack();
557
- },
558
- quit: () => {
559
- cleanup();
560
- screen.destroy();
561
- process.exit(0);
562
- },
563
- };
564
-
565
- function registerHandlers(): void {
566
- screen.key(['t', 'T'], keyHandlers.toggle);
567
- screen.key(['escape'], keyHandlers.escape);
568
- screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
569
- }
570
-
571
- function unregisterHandlers(): void {
572
- screen.unkey('t', keyHandlers.toggle);
573
- screen.unkey('T', keyHandlers.toggle);
574
- screen.unkey('escape', keyHandlers.escape);
575
- screen.unkey('q', keyHandlers.quit);
576
- screen.unkey('Q', keyHandlers.quit);
577
- screen.unkey('C-c', keyHandlers.quit);
578
- }
579
-
580
- registerHandlers();
581
-
582
- contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
583
- screen.render();
584
- await render();
585
-
586
- refreshIntervalId = setInterval(render, REFRESH_INTERVAL);
587
- }