@appkit/llamacpp-cli 1.5.0 → 1.7.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 (124) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/MONITORING-ACCURACY-FIX.md +199 -0
  3. package/PER-PROCESS-METRICS.md +190 -0
  4. package/README.md +124 -9
  5. package/dist/cli.js +32 -7
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/config.d.ts.map +1 -1
  8. package/dist/commands/config.js +15 -1
  9. package/dist/commands/config.js.map +1 -1
  10. package/dist/commands/create.d.ts.map +1 -1
  11. package/dist/commands/create.js +12 -4
  12. package/dist/commands/create.js.map +1 -1
  13. package/dist/commands/delete.js +12 -10
  14. package/dist/commands/delete.js.map +1 -1
  15. package/dist/commands/logs-all.d.ts +9 -0
  16. package/dist/commands/logs-all.d.ts.map +1 -0
  17. package/dist/commands/logs-all.js +209 -0
  18. package/dist/commands/logs-all.js.map +1 -0
  19. package/dist/commands/logs.d.ts +4 -0
  20. package/dist/commands/logs.d.ts.map +1 -1
  21. package/dist/commands/logs.js +108 -2
  22. package/dist/commands/logs.js.map +1 -1
  23. package/dist/commands/monitor.d.ts.map +1 -1
  24. package/dist/commands/monitor.js +51 -1
  25. package/dist/commands/monitor.js.map +1 -1
  26. package/dist/commands/ps.d.ts +3 -1
  27. package/dist/commands/ps.d.ts.map +1 -1
  28. package/dist/commands/ps.js +75 -5
  29. package/dist/commands/ps.js.map +1 -1
  30. package/dist/commands/rm.d.ts.map +1 -1
  31. package/dist/commands/rm.js +5 -12
  32. package/dist/commands/rm.js.map +1 -1
  33. package/dist/commands/server-show.d.ts.map +1 -1
  34. package/dist/commands/server-show.js +30 -3
  35. package/dist/commands/server-show.js.map +1 -1
  36. package/dist/commands/start.d.ts.map +1 -1
  37. package/dist/commands/start.js +34 -7
  38. package/dist/commands/start.js.map +1 -1
  39. package/dist/commands/stop.js +3 -3
  40. package/dist/commands/stop.js.map +1 -1
  41. package/dist/lib/history-manager.d.ts +46 -0
  42. package/dist/lib/history-manager.d.ts.map +1 -0
  43. package/dist/lib/history-manager.js +157 -0
  44. package/dist/lib/history-manager.js.map +1 -0
  45. package/dist/lib/metrics-aggregator.d.ts +2 -1
  46. package/dist/lib/metrics-aggregator.d.ts.map +1 -1
  47. package/dist/lib/metrics-aggregator.js +15 -4
  48. package/dist/lib/metrics-aggregator.js.map +1 -1
  49. package/dist/lib/system-collector.d.ts +9 -4
  50. package/dist/lib/system-collector.d.ts.map +1 -1
  51. package/dist/lib/system-collector.js +29 -28
  52. package/dist/lib/system-collector.js.map +1 -1
  53. package/dist/tui/HistoricalMonitorApp.d.ts +5 -0
  54. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -0
  55. package/dist/tui/HistoricalMonitorApp.js +490 -0
  56. package/dist/tui/HistoricalMonitorApp.js.map +1 -0
  57. package/dist/tui/MonitorApp.d.ts.map +1 -1
  58. package/dist/tui/MonitorApp.js +84 -62
  59. package/dist/tui/MonitorApp.js.map +1 -1
  60. package/dist/tui/MultiServerMonitorApp.d.ts +1 -1
  61. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  62. package/dist/tui/MultiServerMonitorApp.js +293 -77
  63. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  64. package/dist/types/history-types.d.ts +30 -0
  65. package/dist/types/history-types.d.ts.map +1 -0
  66. package/dist/types/history-types.js +11 -0
  67. package/dist/types/history-types.js.map +1 -0
  68. package/dist/types/monitor-types.d.ts +1 -0
  69. package/dist/types/monitor-types.d.ts.map +1 -1
  70. package/dist/types/server-config.d.ts +1 -0
  71. package/dist/types/server-config.d.ts.map +1 -1
  72. package/dist/types/server-config.js.map +1 -1
  73. package/dist/utils/downsample-utils.d.ts +35 -0
  74. package/dist/utils/downsample-utils.d.ts.map +1 -0
  75. package/dist/utils/downsample-utils.js +107 -0
  76. package/dist/utils/downsample-utils.js.map +1 -0
  77. package/dist/utils/file-utils.d.ts +6 -0
  78. package/dist/utils/file-utils.d.ts.map +1 -1
  79. package/dist/utils/file-utils.js +38 -0
  80. package/dist/utils/file-utils.js.map +1 -1
  81. package/dist/utils/log-utils.d.ts +43 -0
  82. package/dist/utils/log-utils.d.ts.map +1 -0
  83. package/dist/utils/log-utils.js +190 -0
  84. package/dist/utils/log-utils.js.map +1 -0
  85. package/dist/utils/process-utils.d.ts +19 -1
  86. package/dist/utils/process-utils.d.ts.map +1 -1
  87. package/dist/utils/process-utils.js +79 -1
  88. package/dist/utils/process-utils.js.map +1 -1
  89. package/docs/images/.gitkeep +1 -0
  90. package/package.json +3 -1
  91. package/src/cli.ts +32 -7
  92. package/src/commands/config.ts +15 -1
  93. package/src/commands/create.ts +14 -5
  94. package/src/commands/delete.ts +10 -10
  95. package/src/commands/logs-all.ts +251 -0
  96. package/src/commands/logs.ts +138 -2
  97. package/src/commands/monitor.ts +21 -1
  98. package/src/commands/ps.ts +88 -5
  99. package/src/commands/rm.ts +5 -12
  100. package/src/commands/server-show.ts +35 -3
  101. package/src/commands/start.ts +35 -7
  102. package/src/commands/stop.ts +3 -3
  103. package/src/lib/history-manager.ts +172 -0
  104. package/src/lib/metrics-aggregator.ts +18 -5
  105. package/src/lib/system-collector.ts +31 -28
  106. package/src/tui/HistoricalMonitorApp.ts +548 -0
  107. package/src/tui/MonitorApp.ts +89 -64
  108. package/src/tui/MultiServerMonitorApp.ts +348 -103
  109. package/src/types/history-types.ts +39 -0
  110. package/src/types/monitor-types.ts +1 -0
  111. package/src/types/server-config.ts +1 -0
  112. package/src/utils/downsample-utils.ts +128 -0
  113. package/src/utils/file-utils.ts +40 -0
  114. package/src/utils/log-utils.ts +178 -0
  115. package/src/utils/process-utils.ts +85 -1
  116. package/test-load.sh +100 -0
  117. package/dist/tui/components/ErrorState.d.ts +0 -8
  118. package/dist/tui/components/ErrorState.d.ts.map +0 -1
  119. package/dist/tui/components/ErrorState.js +0 -22
  120. package/dist/tui/components/ErrorState.js.map +0 -1
  121. package/dist/tui/components/LoadingState.d.ts +0 -8
  122. package/dist/tui/components/LoadingState.d.ts.map +0 -1
  123. package/dist/tui/components/LoadingState.js +0 -21
  124. package/dist/tui/components/LoadingState.js.map +0 -1
@@ -3,6 +3,8 @@ import { ServerConfig } from '../types/server-config.js';
3
3
  import { MetricsAggregator } from '../lib/metrics-aggregator.js';
4
4
  import { SystemCollector } from '../lib/system-collector.js';
5
5
  import { MonitorData, SystemMetrics } from '../types/monitor-types.js';
6
+ import { HistoryManager } from '../lib/history-manager.js';
7
+ import { createHistoricalUI, createMultiServerHistoricalUI } from './HistoricalMonitorApp.js';
6
8
 
7
9
  type ViewMode = 'list' | 'detail';
8
10
 
@@ -14,14 +16,19 @@ interface ServerMonitorData {
14
16
 
15
17
  export async function createMultiServerMonitorUI(
16
18
  screen: blessed.Widgets.Screen,
17
- servers: ServerConfig[]
19
+ servers: ServerConfig[],
20
+ _fromPs: boolean = false,
21
+ directJumpIndex?: number
18
22
  ): Promise<void> {
19
23
  let updateInterval = 2000;
20
24
  let intervalId: NodeJS.Timeout | null = null;
21
- let viewMode: ViewMode = 'list';
22
- let selectedServerIndex = 0;
25
+ let viewMode: ViewMode = directJumpIndex !== undefined ? 'detail' : 'list';
26
+ let selectedServerIndex = directJumpIndex ?? 0;
27
+ let selectedRowIndex = directJumpIndex ?? 0; // Track which row is highlighted in list view
23
28
  let isLoading = false;
24
29
  let lastSystemMetrics: SystemMetrics | null = null;
30
+ let cameFromDirectJump = directJumpIndex !== undefined; // Track if we entered via ps <id>
31
+ let inHistoricalView = false; // Track whether we're in historical view to prevent key conflicts
25
32
 
26
33
  // Spinner animation
27
34
  const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -30,11 +37,13 @@ export async function createMultiServerMonitorUI(
30
37
 
31
38
  const systemCollector = new SystemCollector();
32
39
  const aggregators = new Map<string, MetricsAggregator>();
40
+ const historyManagers = new Map<string, HistoryManager>();
33
41
  const serverDataMap = new Map<string, ServerMonitorData>();
34
42
 
35
- // Initialize aggregators for each server
43
+ // Initialize aggregators and history managers for each server
36
44
  for (const server of servers) {
37
45
  aggregators.set(server.id, new MetricsAggregator(server));
46
+ historyManagers.set(server.id, new HistoryManager(server.id));
38
47
  serverDataMap.set(server.id, {
39
48
  server,
40
49
  data: null,
@@ -70,7 +79,7 @@ export async function createMultiServerMonitorUI(
70
79
  return '[' + '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty)) + ']';
71
80
  }
72
81
 
73
- // Render system resources section
82
+ // Render system resources section (system-wide for list view)
74
83
  function renderSystemResources(systemMetrics: SystemMetrics | null): string {
75
84
  let content = '';
76
85
 
@@ -120,6 +129,97 @@ export async function createMultiServerMonitorUI(
120
129
  return content;
121
130
  }
122
131
 
132
+ // Render aggregate model resources (all running servers in list view)
133
+ function renderAggregateModelResources(): string {
134
+ let content = '';
135
+
136
+ content += '{bold}Model Resources{/bold}\n';
137
+ const termWidth = (screen.width as number) || 80;
138
+ const divider = '─'.repeat(termWidth - 2);
139
+ content += divider + '\n';
140
+
141
+ // Aggregate CPU and memory across all running servers (skip stopped servers)
142
+ let totalCpu = 0;
143
+ let totalMemoryBytes = 0;
144
+ let serverCount = 0;
145
+
146
+ for (const serverData of serverDataMap.values()) {
147
+ // Only count running servers with valid data
148
+ if (serverData.server.status === 'running' && serverData.data?.server && !serverData.data.server.stale) {
149
+ if (serverData.data.server.processCpuUsage !== undefined) {
150
+ totalCpu += serverData.data.server.processCpuUsage;
151
+ serverCount++;
152
+ }
153
+ if (serverData.data.server.processMemory !== undefined) {
154
+ totalMemoryBytes += serverData.data.server.processMemory;
155
+ }
156
+ }
157
+ }
158
+
159
+ if (serverCount === 0) {
160
+ content += '{gray-fg}No running servers{/gray-fg}\n';
161
+ return content;
162
+ }
163
+
164
+ // CPU: Sum of all process CPU percentages
165
+ const cpuBar = createProgressBar(Math.min(totalCpu, 100));
166
+ content += `CPU: {cyan-fg}${cpuBar}{/cyan-fg} ${Math.round(totalCpu)}%`;
167
+ content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
168
+
169
+ // Memory: Sum of all process memory
170
+ const totalMemoryGB = totalMemoryBytes / (1024 ** 3);
171
+ const estimatedMaxGB = serverCount * 8; // Assume ~8GB per server max
172
+ const memoryPercentage = Math.min((totalMemoryGB / estimatedMaxGB) * 100, 100);
173
+ const memoryBar = createProgressBar(memoryPercentage);
174
+ content += `Memory: {cyan-fg}${memoryBar}{/cyan-fg} ${totalMemoryGB.toFixed(2)} GB`;
175
+ content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
176
+
177
+ return content;
178
+ }
179
+
180
+ // Render model resources section (per-process for detail view)
181
+ function renderModelResources(data: MonitorData): string {
182
+ let content = '';
183
+
184
+ content += '{bold}Model Resources{/bold}\n';
185
+ const termWidth = (screen.width as number) || 80;
186
+ const divider = '─'.repeat(termWidth - 2);
187
+ content += divider + '\n';
188
+
189
+ // GPU: System-wide (can't get per-process on macOS)
190
+ if (data.system && data.system.gpuUsage !== undefined) {
191
+ const bar = createProgressBar(data.system.gpuUsage);
192
+ content += `GPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.system.gpuUsage)}% {gray-fg}(system){/gray-fg}`;
193
+
194
+ if (data.system.temperature !== undefined) {
195
+ content += ` - ${Math.round(data.system.temperature)}°C`;
196
+ }
197
+
198
+ content += '\n';
199
+ }
200
+
201
+ // CPU: Per-process
202
+ if (data.server.processCpuUsage !== undefined) {
203
+ const bar = createProgressBar(data.server.processCpuUsage);
204
+ content += `CPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.server.processCpuUsage)}%\n`;
205
+ }
206
+
207
+ // Memory: Per-process
208
+ if (data.server.processMemory !== undefined) {
209
+ const memoryGB = data.server.processMemory / (1024 ** 3);
210
+ const estimatedMax = 8;
211
+ const memoryPercentage = Math.min((memoryGB / estimatedMax) * 100, 100);
212
+ const bar = createProgressBar(memoryPercentage);
213
+ content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${memoryGB.toFixed(2)} GB\n`;
214
+ }
215
+
216
+ if (data.system && data.system.warnings && data.system.warnings.length > 0) {
217
+ content += `\n{yellow-fg}⚠ ${data.system.warnings.join(', ')}{/yellow-fg}\n`;
218
+ }
219
+
220
+ return content;
221
+ }
222
+
123
223
  // Show loading spinner
124
224
  function showLoading(): void {
125
225
  if (isLoading) return; // Already loading
@@ -169,53 +269,76 @@ export async function createMultiServerMonitorUI(
169
269
  let content = '';
170
270
 
171
271
  // Header
172
- content += '{bold}{blue-fg}═══ llama.cpp Multi-Server Monitor ═══{/blue-fg}{/bold}\n';
173
-
174
- // Status line with optional spinner
175
- const statusPlainText = 'Press 1-9 for details | [F] Filter | [Q] Quit';
176
- const spinnerChar = isLoading ? spinnerFrames[spinnerFrameIndex] : '';
177
- const spinnerText = spinnerChar ? ` {cyan-fg}${spinnerChar}{/cyan-fg}` : '';
178
-
179
- content += `{gray-fg}${statusPlainText}${spinnerText}{/gray-fg}\n\n`;
272
+ content += '{bold}{blue-fg}═══ llama.cpp{/blue-fg}{/bold}\n\n';
180
273
 
181
274
  // System resources
182
275
  content += renderSystemResources(systemMetrics);
183
276
  content += '\n';
184
277
 
278
+ // Aggregate model resources (CPU + memory for all running servers)
279
+ content += renderAggregateModelResources();
280
+ content += '\n';
281
+
185
282
  // Server list header
186
283
  const runningCount = servers.filter(s => s.status === 'running').length;
187
284
  const stoppedCount = servers.filter(s => s.status !== 'running').length;
188
285
  content += `{bold}Servers (${runningCount} running, ${stoppedCount} stopped){/bold}\n`;
189
- content += '{gray-fg}Press number for details{/gray-fg}\n';
286
+ content += '{gray-fg}Use arrow keys to navigate, Enter to view details{/gray-fg}\n';
190
287
  content += divider + '\n';
191
288
 
192
- // Table header
193
- content += '{bold}# │ Server ID │ Port │ Status │ Slots │ tok/s │ Memory{/bold}\n';
289
+ // Calculate Server ID column width (variable based on screen width)
290
+ // Fixed columns breakdown:
291
+ // indicator(1) + " │ "(3) + " │ "(3) + port(4) + " │ "(3) + status(6) + "│ "(2) +
292
+ // slots(5) + " │ "(3) + tok/s(6) + " │ "(3) + memory(7) = 46
293
+ const fixedColumnsWidth = 48; // Add 2 extra for safety margin
294
+ const minServerIdWidth = 20;
295
+ const maxServerIdWidth = 60;
296
+ const serverIdWidth = Math.max(
297
+ minServerIdWidth,
298
+ Math.min(maxServerIdWidth, termWidth - fixedColumnsWidth)
299
+ );
300
+
301
+ // Table header with variable Server ID width
302
+ const serverIdHeader = 'Server ID'.padEnd(serverIdWidth);
303
+ content += `{bold} │ ${serverIdHeader}│ Port │ Status │ Slots │ tok/s │ Memory{/bold}\n`;
194
304
  content += divider + '\n';
195
305
 
196
306
  // Server rows
197
307
  servers.forEach((server, index) => {
198
308
  const serverData = serverDataMap.get(server.id);
199
- const num = index + 1;
309
+ const isSelected = index === selectedRowIndex;
310
+
311
+ // Selection indicator (arrow for selected row)
312
+ // Use plain arrow for selected (will be white), colored for unselected indicator
313
+ const indicator = isSelected ? '►' : ' ';
200
314
 
201
- // Server ID (truncate if needed)
202
- const serverId = server.id.padEnd(16).substring(0, 16);
315
+ // Server ID (variable width, truncate if longer than available space)
316
+ const serverId = server.id.padEnd(serverIdWidth).substring(0, serverIdWidth);
203
317
 
204
318
  // Port
205
319
  const port = server.port.toString().padStart(4);
206
320
 
207
- // Status
321
+ // Status - Check actual server status first, then health
322
+ // Build two versions: colored for normal, plain for selected
208
323
  let status = '';
209
- if (serverData?.data) {
324
+ let statusPlain = '';
325
+ if (server.status !== 'running') {
326
+ // Server is stopped according to config
327
+ status = '{gray-fg}○ OFF{/gray-fg} ';
328
+ statusPlain = '○ OFF ';
329
+ } else if (serverData?.data) {
330
+ // Server is running and we have data
210
331
  if (serverData.data.server.healthy) {
211
332
  status = '{green-fg}● RUN{/green-fg} ';
333
+ statusPlain = '● RUN ';
212
334
  } else {
213
335
  status = '{red-fg}● ERR{/red-fg} ';
336
+ statusPlain = '● ERR ';
214
337
  }
215
- } else if (server.status === 'running') {
216
- status = '{yellow-fg}● ...{/yellow-fg} ';
217
338
  } else {
218
- status = '{gray-fg}○ STOP{/gray-fg}';
339
+ // Server is running but no data yet (still loading)
340
+ status = '{yellow-fg}● ...{/yellow-fg} ';
341
+ statusPlain = '● ... ';
219
342
  }
220
343
 
221
344
  // Slots
@@ -247,13 +370,23 @@ export async function createMultiServerMonitorUI(
247
370
  }
248
371
  }
249
372
 
250
- content += `${num} ${serverId} ${port} │ ${status} ${slots} │ ${tokensPerSec} │ ${memory}\n`;
373
+ // Build row content - use plain status for selected rows
374
+ let rowContent = '';
375
+ if (isSelected) {
376
+ // Use color code 15 (bright white) with cyan background
377
+ // When white-bg worked, it was probably auto-selecting bright white fg
378
+ rowContent = `{cyan-bg}{15-fg}${indicator} │ ${serverId} │ ${port} │ ${statusPlain}│ ${slots} │ ${tokensPerSec} │ ${memory}{/15-fg}{/cyan-bg}`;
379
+ } else {
380
+ // Use colored status for normal rows
381
+ rowContent = `${indicator} │ ${serverId} │ ${port} │ ${status}│ ${slots} │ ${tokensPerSec} │ ${memory}`;
382
+ }
383
+
384
+ content += rowContent + '\n';
251
385
  });
252
386
 
253
387
  // Footer
254
388
  content += '\n' + divider + '\n';
255
- content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | `;
256
- content += `Interval: ${updateInterval}ms | [R]efresh [+/-]Speed{/gray-fg}`;
389
+ content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | [H]istory [Q]uit{/gray-fg}`;
257
390
 
258
391
  return content;
259
392
  }
@@ -267,18 +400,50 @@ export async function createMultiServerMonitorUI(
267
400
  let content = '';
268
401
 
269
402
  // Header
270
- content += `{bold}{blue-fg}═══ Server #${selectedServerIndex + 1}: ${server.id} (${server.port}) ═══{/blue-fg}{/bold}\n`;
403
+ content += `{bold}{blue-fg}═══ ${server.id} (${server.port}){/blue-fg}{/bold}\n\n`;
271
404
 
272
- // Status line with optional spinner
273
- const statusPlainText = '[ESC] Back to list | [Q] Quit';
274
- const spinnerChar = isLoading ? spinnerFrames[spinnerFrameIndex] : '';
275
- const spinnerText = spinnerChar ? ` {cyan-fg}${spinnerChar}{/cyan-fg}` : '';
405
+ // Check if server is stopped
406
+ if (server.status !== 'running') {
407
+ // Show stopped server configuration (no metrics)
408
+ content += '{bold}Server Information{/bold}\n';
409
+ content += divider + '\n';
410
+ content += `Status: {gray-fg}○ STOPPED{/gray-fg}\n`;
411
+ content += `Model: ${server.modelName}\n`;
412
+ const displayHost = server.host || '127.0.0.1';
413
+ content += `Endpoint: http://${displayHost}:${server.port}\n`;
414
+ content += '\n';
276
415
 
277
- content += `{gray-fg}${statusPlainText}${spinnerText}{/gray-fg}\n\n`;
416
+ content += '{bold}Configuration{/bold}\n';
417
+ content += divider + '\n';
418
+ content += `Threads: ${server.threads}\n`;
419
+ content += `Context: ${server.ctxSize} tokens\n`;
420
+ content += `GPU Layers: ${server.gpuLayers}\n`;
421
+ if (server.verbose) {
422
+ content += `Verbose: Enabled\n`;
423
+ }
424
+ if (server.customFlags && server.customFlags.length > 0) {
425
+ content += `Flags: ${server.customFlags.join(', ')}\n`;
426
+ }
427
+ content += '\n';
278
428
 
279
- // System resources
280
- content += renderSystemResources(systemMetrics);
281
- content += '\n';
429
+ if (server.lastStarted) {
430
+ content += '{bold}Last Activity{/bold}\n';
431
+ content += divider + '\n';
432
+ content += `Started: ${new Date(server.lastStarted).toLocaleString()}\n`;
433
+ if (server.lastStopped) {
434
+ content += `Stopped: ${new Date(server.lastStopped).toLocaleString()}\n`;
435
+ }
436
+ content += '\n';
437
+ }
438
+
439
+ content += '{bold}Quick Actions{/bold}\n';
440
+ content += divider + '\n';
441
+ content += `{dim}Start server: llamacpp server start ${server.port}{/dim}\n`;
442
+ content += `{dim}Update config: llamacpp server config ${server.port} [options]{/dim}\n`;
443
+ content += `{dim}View logs: llamacpp server logs ${server.port}{/dim}\n`;
444
+
445
+ return content;
446
+ }
282
447
 
283
448
  if (!serverData?.data) {
284
449
  content += '{yellow-fg}Loading server data...{/yellow-fg}\n';
@@ -287,6 +452,10 @@ export async function createMultiServerMonitorUI(
287
452
 
288
453
  const data = serverData.data;
289
454
 
455
+ // Model resources (per-process)
456
+ content += renderModelResources(data);
457
+ content += '\n';
458
+
290
459
  // Server Information
291
460
  content += '{bold}Server Information{/bold}\n';
292
461
  content += divider + '\n';
@@ -308,21 +477,7 @@ export async function createMultiServerMonitorUI(
308
477
 
309
478
  // Handle null host (legacy configs) by defaulting to 127.0.0.1
310
479
  const displayHost = server.host || '127.0.0.1';
311
- content += `Endpoint: http://${displayHost}:${server.port}`;
312
-
313
- // Add actual process memory (if available)
314
- if (data.server.processMemory) {
315
- const bytes = data.server.processMemory;
316
- let memStr;
317
- if (bytes >= 1024 * 1024 * 1024) {
318
- memStr = `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
319
- } else {
320
- memStr = `${Math.round(bytes / (1024 * 1024))} MB`;
321
- }
322
- content += ` Memory: ${memStr}\n`;
323
- } else {
324
- content += '\n';
325
- }
480
+ content += `Endpoint: http://${displayHost}:${server.port}\n`;
326
481
 
327
482
  content += `Slots: ${data.server.activeSlots} active / ${data.server.totalSlots} total\n`;
328
483
  content += '\n';
@@ -377,8 +532,7 @@ export async function createMultiServerMonitorUI(
377
532
 
378
533
  // Footer
379
534
  content += divider + '\n';
380
- content += `{gray-fg}Updated: ${data.lastUpdated.toLocaleTimeString()} | `;
381
- content += `Interval: ${updateInterval}ms | [R]efresh [+/-]Speed{/gray-fg}`;
535
+ content += `{gray-fg}Updated: ${data.lastUpdated.toLocaleTimeString()} | [H]istory [ESC] Back [Q]uit{/gray-fg}`;
382
536
 
383
537
  return content;
384
538
  }
@@ -390,51 +544,68 @@ export async function createMultiServerMonitorUI(
390
544
  // This prevents spawning multiple macmon processes
391
545
  const systemMetricsPromise = systemCollector.collectSystemMetrics();
392
546
 
393
- // Batch collect process memory for ALL servers in one top call
547
+ // Batch collect process memory and CPU for ALL servers in parallel
394
548
  // This prevents spawning multiple top processes (5x speedup)
395
- const { getBatchProcessMemory } = await import('../utils/process-utils.js');
549
+ const { getBatchProcessMemory, getBatchProcessCpu } = await import('../utils/process-utils.js');
396
550
  const pids = servers.filter(s => s.pid).map(s => s.pid!);
397
551
  const memoryMapPromise = pids.length > 0
398
552
  ? getBatchProcessMemory(pids)
399
553
  : Promise.resolve(new Map<number, number | null>());
554
+ const cpuMapPromise = pids.length > 0
555
+ ? getBatchProcessCpu(pids)
556
+ : Promise.resolve(new Map<number, number | null>());
400
557
 
401
- // Wait for memory batch to complete
402
- const memoryMap = await memoryMapPromise;
403
-
404
- // Collect server metrics only (NOT system metrics) for each server
405
- const promises = servers.map(async (server) => {
406
- const aggregator = aggregators.get(server.id)!;
407
- try {
408
- // Use collectServerMetrics instead of collectMonitorData
409
- // to avoid spawning macmon per server
410
- // Pass pre-fetched memory to avoid spawning top per server
411
- const serverMetrics = await aggregator.collectServerMetrics(
412
- server,
413
- server.pid ? memoryMap.get(server.pid) ?? null : null
414
- );
415
-
416
- // Build MonitorData manually with shared system metrics
417
- const data: MonitorData = {
418
- server: serverMetrics,
419
- system: undefined, // Will be set after system metrics resolve
420
- lastUpdated: new Date(),
421
- updateInterval,
422
- consecutiveFailures: 0,
423
- };
558
+ // Wait for both batches to complete
559
+ const [memoryMap, cpuMap] = await Promise.all([memoryMapPromise, cpuMapPromise]);
560
+
561
+ // Collect server metrics only for RUNNING servers (skip stopped servers)
562
+ const promises = servers
563
+ .filter(server => server.status === 'running')
564
+ .map(async (server) => {
565
+ const aggregator = aggregators.get(server.id)!;
566
+ try {
567
+ // Use collectServerMetrics instead of collectMonitorData
568
+ // to avoid spawning macmon per server
569
+ // Pass pre-fetched memory and CPU to avoid spawning top per server
570
+ const serverMetrics = await aggregator.collectServerMetrics(
571
+ server,
572
+ server.pid ? memoryMap.get(server.pid) ?? null : null,
573
+ server.pid ? cpuMap.get(server.pid) ?? null : null
574
+ );
575
+
576
+ // Build MonitorData manually with shared system metrics
577
+ const data: MonitorData = {
578
+ server: serverMetrics,
579
+ system: undefined, // Will be set after system metrics resolve
580
+ lastUpdated: new Date(),
581
+ updateInterval,
582
+ consecutiveFailures: 0,
583
+ };
584
+
585
+ serverDataMap.set(server.id, {
586
+ server,
587
+ data,
588
+ error: null,
589
+ });
590
+ } catch (err) {
591
+ serverDataMap.set(server.id, {
592
+ server,
593
+ data: null,
594
+ error: err instanceof Error ? err.message : 'Unknown error',
595
+ });
596
+ }
597
+ });
424
598
 
425
- serverDataMap.set(server.id, {
426
- server,
427
- data,
428
- error: null,
429
- });
430
- } catch (err) {
599
+ // Set null data for stopped servers (no metrics collection)
600
+ servers
601
+ .filter(server => server.status !== 'running')
602
+ .forEach(server => {
431
603
  serverDataMap.set(server.id, {
432
604
  server,
433
605
  data: null,
434
- error: err instanceof Error ? err.message : 'Unknown error',
606
+ error: null,
435
607
  });
436
- }
437
- });
608
+ });
438
609
 
439
610
  // Wait for both system metrics and server metrics to complete
440
611
  const systemMetrics = await systemMetricsPromise;
@@ -450,6 +621,19 @@ export async function createMultiServerMonitorUI(
450
621
  }
451
622
  }
452
623
 
624
+ // Append to history for each server (silent failure)
625
+ // Only save history for servers that are healthy and not stale
626
+ for (const [serverId, serverData] of serverDataMap) {
627
+ if (serverData.data && !serverData.data.server.stale && serverData.data.server.healthy) {
628
+ const manager = historyManagers.get(serverId);
629
+ manager?.appendSnapshot(serverData.data.server, serverData.data.system)
630
+ .catch(err => {
631
+ // Don't interrupt monitoring on history write failure
632
+ console.error(`Failed to save history for ${serverId}:`, err);
633
+ });
634
+ }
635
+ }
636
+
453
637
  // Render once with complete data
454
638
  let content = '';
455
639
  if (viewMode === 'list') {
@@ -485,12 +669,32 @@ export async function createMultiServerMonitorUI(
485
669
  intervalId = setInterval(fetchData, updateInterval);
486
670
  }
487
671
 
488
- // Keyboard shortcuts - List view
489
- screen.key(['1', '2', '3', '4', '5', '6', '7', '8', '9'], (ch) => {
490
- const index = parseInt(ch, 10) - 1;
491
- if (index >= 0 && index < servers.length) {
672
+ // Keyboard shortcuts - List view navigation with arrow keys
673
+ screen.key(['up', 'k'], () => {
674
+ if (viewMode === 'list') {
675
+ selectedRowIndex = Math.max(0, selectedRowIndex - 1);
676
+ // Re-render immediately for responsive feel
677
+ const content = renderListView(lastSystemMetrics);
678
+ contentBox.setContent(content);
679
+ screen.render();
680
+ }
681
+ });
682
+
683
+ screen.key(['down', 'j'], () => {
684
+ if (viewMode === 'list') {
685
+ selectedRowIndex = Math.min(servers.length - 1, selectedRowIndex + 1);
686
+ // Re-render immediately for responsive feel
687
+ const content = renderListView(lastSystemMetrics);
688
+ contentBox.setContent(content);
689
+ screen.render();
690
+ }
691
+ });
692
+
693
+ // Enter key to view details for selected server
694
+ screen.key(['enter'], () => {
695
+ if (viewMode === 'list') {
492
696
  showLoading();
493
- selectedServerIndex = index;
697
+ selectedServerIndex = selectedRowIndex;
494
698
  viewMode = 'detail';
495
699
  fetchData();
496
700
  }
@@ -498,27 +702,68 @@ export async function createMultiServerMonitorUI(
498
702
 
499
703
  // Keyboard shortcuts - Detail view
500
704
  screen.key(['escape'], () => {
705
+ // Don't handle ESC if we're in historical view - let historical view handle it
706
+ if (inHistoricalView) return;
707
+
501
708
  if (viewMode === 'detail') {
502
709
  showLoading();
503
710
  viewMode = 'list';
711
+ cameFromDirectJump = false; // Clear direct jump flag when returning to list
504
712
  fetchData();
713
+ } else if (viewMode === 'list') {
714
+ // ESC in list view - exit
715
+ showLoading();
716
+ if (intervalId) clearInterval(intervalId);
717
+ if (spinnerIntervalId) clearInterval(spinnerIntervalId);
718
+ setTimeout(() => {
719
+ screen.destroy();
720
+ process.exit(0);
721
+ }, 100);
505
722
  }
506
723
  });
507
724
 
508
725
  // Keyboard shortcuts - Common
509
- screen.key(['r', 'R'], () => {
510
- showLoading();
511
- fetchData();
512
- });
513
726
 
514
- screen.key(['+', '='], () => {
515
- updateInterval = Math.max(500, updateInterval - 500);
516
- startPolling();
517
- });
727
+ screen.key(['h', 'H'], async () => {
728
+ // Prevent entering historical view if already there
729
+ if (inHistoricalView) return;
730
+
731
+ // Keep polling in background for live historical updates
732
+ // Stop spinner if running
733
+ if (spinnerIntervalId) clearInterval(spinnerIntervalId);
518
734
 
519
- screen.key(['-', '_'], () => {
520
- updateInterval = Math.min(10000, updateInterval + 500);
521
- startPolling();
735
+ // Remove current content box
736
+ screen.remove(contentBox);
737
+
738
+ // Mark that we're in historical view
739
+ inHistoricalView = true;
740
+
741
+ if (viewMode === 'list') {
742
+ // Show multi-server historical view
743
+ await createMultiServerHistoricalUI(screen, servers, selectedServerIndex, () => {
744
+ // Mark that we've left historical view
745
+ inHistoricalView = false;
746
+ // Re-attach content box when returning from history
747
+ screen.append(contentBox);
748
+ // Re-render the list view
749
+ const content = renderListView(lastSystemMetrics);
750
+ contentBox.setContent(content);
751
+ screen.render();
752
+ });
753
+ } else {
754
+ // Show single-server historical view for selected server
755
+ const selectedServer = servers[selectedServerIndex];
756
+ await createHistoricalUI(screen, selectedServer, () => {
757
+ // Mark that we've left historical view
758
+ inHistoricalView = false;
759
+ // Re-attach content box when returning from history
760
+ screen.append(contentBox);
761
+ // Re-render the detail view
762
+ const content = renderDetailView(lastSystemMetrics);
763
+ contentBox.setContent(content);
764
+ screen.render();
765
+ });
766
+ }
522
767
  });
523
768
 
524
769
  screen.key(['q', 'Q', 'C-c'], () => {
@@ -0,0 +1,39 @@
1
+ // Historical monitoring data types
2
+
3
+ export interface HistorySnapshot {
4
+ timestamp: number; // Unix timestamp in milliseconds
5
+ server: {
6
+ healthy: boolean;
7
+ uptime?: string;
8
+ activeSlots: number;
9
+ idleSlots: number;
10
+ totalSlots: number;
11
+ avgPromptSpeed?: number; // Tokens per second
12
+ avgGenerateSpeed?: number; // Tokens per second
13
+ processMemory?: number; // Bytes (RSS)
14
+ processCpuUsage?: number; // Percentage (0-100+) from ps
15
+ };
16
+ system?: {
17
+ gpuUsage?: number; // Percentage (0-100)
18
+ cpuUsage?: number; // Percentage (0-100)
19
+ aneUsage?: number; // Percentage (0-100)
20
+ temperature?: number; // Celsius
21
+ memoryUsed: number; // Bytes
22
+ memoryTotal: number; // Bytes
23
+ };
24
+ }
25
+
26
+ export interface HistoryData {
27
+ serverId: string;
28
+ snapshots: HistorySnapshot[];
29
+ }
30
+
31
+ export type TimeWindow = '1h' | '6h' | '24h';
32
+
33
+ export const TIME_WINDOW_HOURS: Record<TimeWindow, number> = {
34
+ '1h': 1,
35
+ '6h': 6,
36
+ '24h': 24,
37
+ };
38
+
39
+ export const TIME_WINDOWS: TimeWindow[] = ['1h', '6h', '24h'];
@@ -123,6 +123,7 @@ export interface ServerMetrics {
123
123
 
124
124
  // Process metrics
125
125
  processMemory?: number; // Bytes (actual RSS from top command)
126
+ processCpuUsage?: number; // Percentage (0-100+) from ps command
126
127
 
127
128
  // Timestamp
128
129
  timestamp: number;
@@ -22,6 +22,7 @@ export interface ServerConfig {
22
22
  createdAt: string; // ISO timestamp
23
23
  lastStarted?: string; // ISO timestamp
24
24
  lastStopped?: string; // ISO timestamp
25
+ metalMemoryMB?: number; // Metal (GPU) memory allocated in MB (parsed from logs)
25
26
 
26
27
  // launchctl metadata
27
28
  plistPath: string; // Full path to plist file