@appkit/llamacpp-cli 1.8.0 → 1.10.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.
- package/CHANGELOG.md +58 -0
- package/README.md +249 -40
- package/dist/cli.js +154 -10
- package/dist/cli.js.map +1 -1
- package/dist/commands/completion.d.ts +9 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +83 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/monitor.js +1 -1
- package/dist/commands/monitor.js.map +1 -1
- package/dist/commands/ps.d.ts +1 -3
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +36 -115
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/router/config.d.ts +11 -0
- package/dist/commands/router/config.d.ts.map +1 -0
- package/dist/commands/router/config.js +100 -0
- package/dist/commands/router/config.js.map +1 -0
- package/dist/commands/router/logs.d.ts +12 -0
- package/dist/commands/router/logs.d.ts.map +1 -0
- package/dist/commands/router/logs.js +238 -0
- package/dist/commands/router/logs.js.map +1 -0
- package/dist/commands/router/restart.d.ts +2 -0
- package/dist/commands/router/restart.d.ts.map +1 -0
- package/dist/commands/router/restart.js +39 -0
- package/dist/commands/router/restart.js.map +1 -0
- package/dist/commands/router/start.d.ts +2 -0
- package/dist/commands/router/start.d.ts.map +1 -0
- package/dist/commands/router/start.js +60 -0
- package/dist/commands/router/start.js.map +1 -0
- package/dist/commands/router/status.d.ts +2 -0
- package/dist/commands/router/status.d.ts.map +1 -0
- package/dist/commands/router/status.js +116 -0
- package/dist/commands/router/status.js.map +1 -0
- package/dist/commands/router/stop.d.ts +2 -0
- package/dist/commands/router/stop.d.ts.map +1 -0
- package/dist/commands/router/stop.js +36 -0
- package/dist/commands/router/stop.js.map +1 -0
- package/dist/commands/tui.d.ts +2 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +27 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/lib/completion.d.ts +5 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +195 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +5 -1
- package/dist/lib/model-downloader.d.ts.map +1 -1
- package/dist/lib/model-downloader.js +53 -20
- package/dist/lib/model-downloader.js.map +1 -1
- package/dist/lib/router-logger.d.ts +61 -0
- package/dist/lib/router-logger.d.ts.map +1 -0
- package/dist/lib/router-logger.js +200 -0
- package/dist/lib/router-logger.js.map +1 -0
- package/dist/lib/router-manager.d.ts +103 -0
- package/dist/lib/router-manager.d.ts.map +1 -0
- package/dist/lib/router-manager.js +394 -0
- package/dist/lib/router-manager.js.map +1 -0
- package/dist/lib/router-server.d.ts +61 -0
- package/dist/lib/router-server.d.ts.map +1 -0
- package/dist/lib/router-server.js +485 -0
- package/dist/lib/router-server.js.map +1 -0
- package/dist/tui/ConfigApp.d.ts +7 -0
- package/dist/tui/ConfigApp.d.ts.map +1 -0
- package/dist/tui/ConfigApp.js +1002 -0
- package/dist/tui/ConfigApp.js.map +1 -0
- package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
- package/dist/tui/HistoricalMonitorApp.js +85 -49
- package/dist/tui/HistoricalMonitorApp.js.map +1 -1
- package/dist/tui/ModelsApp.d.ts +7 -0
- package/dist/tui/ModelsApp.d.ts.map +1 -0
- package/dist/tui/ModelsApp.js +362 -0
- package/dist/tui/ModelsApp.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
- package/dist/tui/MultiServerMonitorApp.js +1038 -122
- package/dist/tui/MultiServerMonitorApp.js.map +1 -1
- package/dist/tui/RootNavigator.d.ts +7 -0
- package/dist/tui/RootNavigator.d.ts.map +1 -0
- package/dist/tui/RootNavigator.js +55 -0
- package/dist/tui/RootNavigator.js.map +1 -0
- package/dist/tui/SearchApp.d.ts +6 -0
- package/dist/tui/SearchApp.d.ts.map +1 -0
- package/dist/tui/SearchApp.js +451 -0
- package/dist/tui/SearchApp.js.map +1 -0
- package/dist/tui/SplashScreen.d.ts +16 -0
- package/dist/tui/SplashScreen.d.ts.map +1 -0
- package/dist/tui/SplashScreen.js +129 -0
- package/dist/tui/SplashScreen.js.map +1 -0
- package/dist/types/router-config.d.ts +19 -0
- package/dist/types/router-config.d.ts.map +1 -0
- package/dist/types/router-config.js +3 -0
- package/dist/types/router-config.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +121 -10
- package/src/commands/monitor.ts +1 -1
- package/src/commands/ps.ts +44 -133
- package/src/commands/router/config.ts +116 -0
- package/src/commands/router/logs.ts +256 -0
- package/src/commands/router/restart.ts +36 -0
- package/src/commands/router/start.ts +60 -0
- package/src/commands/router/status.ts +119 -0
- package/src/commands/router/stop.ts +33 -0
- package/src/commands/tui.ts +25 -0
- package/src/lib/model-downloader.ts +57 -20
- package/src/lib/router-logger.ts +201 -0
- package/src/lib/router-manager.ts +414 -0
- package/src/lib/router-server.ts +538 -0
- package/src/tui/ConfigApp.ts +1085 -0
- package/src/tui/HistoricalMonitorApp.ts +88 -49
- package/src/tui/ModelsApp.ts +368 -0
- package/src/tui/MultiServerMonitorApp.ts +1163 -122
- package/src/tui/RootNavigator.ts +74 -0
- package/src/tui/SearchApp.ts +511 -0
- package/src/tui/SplashScreen.ts +149 -0
- package/src/types/router-config.ts +25 -0
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
|
|
13
13
|
type ViewMode = 'recent' | 'hour';
|
|
14
14
|
|
|
15
|
+
// Shared view mode across both history screens - persists for the session
|
|
16
|
+
let sharedViewMode: ViewMode = 'recent';
|
|
17
|
+
|
|
15
18
|
interface ChartStats {
|
|
16
19
|
avg: number;
|
|
17
20
|
max: number;
|
|
@@ -155,7 +158,6 @@ export async function createHistoricalUI(
|
|
|
155
158
|
const REFRESH_INTERVAL = 1000;
|
|
156
159
|
let lastGoodRender: string | null = null;
|
|
157
160
|
let consecutiveErrors = 0;
|
|
158
|
-
let viewMode: ViewMode = 'recent';
|
|
159
161
|
|
|
160
162
|
const contentBox = createContentBox();
|
|
161
163
|
screen.append(contentBox);
|
|
@@ -163,21 +165,21 @@ export async function createHistoricalUI(
|
|
|
163
165
|
// Chart configurations
|
|
164
166
|
const chartConfigs: Record<string, ChartConfig> = {
|
|
165
167
|
tokenSpeed: {
|
|
166
|
-
title: '
|
|
168
|
+
title: 'Server Token Generation Speed (tok/s)',
|
|
167
169
|
color: asciichart.cyan,
|
|
168
170
|
formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
|
|
169
171
|
isPercentage: false,
|
|
170
172
|
noDataMessage: 'No generation activity in this time window',
|
|
171
173
|
},
|
|
172
174
|
cpu: {
|
|
173
|
-
title: '
|
|
175
|
+
title: 'Server CPU Usage (%)',
|
|
174
176
|
color: asciichart.blue,
|
|
175
177
|
formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
|
|
176
178
|
isPercentage: true,
|
|
177
179
|
noDataMessage: 'No CPU data in this time window',
|
|
178
180
|
},
|
|
179
181
|
memory: {
|
|
180
|
-
title: '
|
|
182
|
+
title: 'Server Memory Usage (GB)',
|
|
181
183
|
color: asciichart.magenta,
|
|
182
184
|
formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
|
|
183
185
|
isPercentage: false,
|
|
@@ -206,8 +208,8 @@ export async function createHistoricalUI(
|
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
// Header
|
|
209
|
-
const modeLabel =
|
|
210
|
-
const modeColor =
|
|
211
|
+
const modeLabel = sharedViewMode === 'recent' ? 'Minute' : 'Hour';
|
|
212
|
+
const modeColor = sharedViewMode === 'recent' ? 'cyan' : 'magenta';
|
|
211
213
|
let content = `{bold}{blue-fg}\u2550\u2550\u2550 ${server.modelName} (${server.port}) {/blue-fg} `;
|
|
212
214
|
content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
|
|
213
215
|
|
|
@@ -218,14 +220,14 @@ export async function createHistoricalUI(
|
|
|
218
220
|
content += 'Historical data is collected when you run the monitor command.\n';
|
|
219
221
|
content += 'Start monitoring to begin collecting history.\n\n';
|
|
220
222
|
content += divider + '\n';
|
|
221
|
-
content += '{gray-fg}ESC
|
|
223
|
+
content += '{gray-fg}[ESC] Back [Q]uit{/gray-fg}';
|
|
222
224
|
contentBox.setContent(content);
|
|
223
225
|
screen.render();
|
|
224
226
|
return;
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
const maxChartPoints = Math.min(chartWidth, 80);
|
|
228
|
-
const displaySnapshots =
|
|
230
|
+
const displaySnapshots = sharedViewMode === 'recent' && snapshots.length > maxChartPoints
|
|
229
231
|
? snapshots.slice(-maxChartPoints)
|
|
230
232
|
: snapshots;
|
|
231
233
|
|
|
@@ -249,7 +251,7 @@ export async function createHistoricalUI(
|
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
// Apply downsampling based on view mode
|
|
252
|
-
const useDownsampling =
|
|
254
|
+
const useDownsampling = sharedViewMode === 'hour';
|
|
253
255
|
const values = {
|
|
254
256
|
tokenSpeed: useDownsampling
|
|
255
257
|
? downsampleMaxTimeWithFullHour(rawData.tokenSpeed, maxChartPoints)
|
|
@@ -273,7 +275,7 @@ export async function createHistoricalUI(
|
|
|
273
275
|
|
|
274
276
|
// Footer
|
|
275
277
|
content += divider + '\n';
|
|
276
|
-
content += `{gray-fg}
|
|
278
|
+
content += `{gray-fg}[T]oggle Hour View [ESC] Back [Q]uit{/gray-fg}`;
|
|
277
279
|
|
|
278
280
|
contentBox.setContent(content);
|
|
279
281
|
screen.render();
|
|
@@ -290,7 +292,7 @@ export async function createHistoricalUI(
|
|
|
290
292
|
'{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
|
|
291
293
|
`{red-fg}${errorMsg}{/red-fg}\n\n` +
|
|
292
294
|
`Consecutive errors: ${consecutiveErrors}\n\n` +
|
|
293
|
-
'{gray-fg}ESC
|
|
295
|
+
'{gray-fg}[ESC] Back [Q]uit{/gray-fg}'
|
|
294
296
|
);
|
|
295
297
|
}
|
|
296
298
|
screen.render();
|
|
@@ -302,24 +304,43 @@ export async function createHistoricalUI(
|
|
|
302
304
|
clearInterval(refreshIntervalId);
|
|
303
305
|
refreshIntervalId = null;
|
|
304
306
|
}
|
|
307
|
+
unregisterHandlers();
|
|
305
308
|
}
|
|
306
309
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
};
|
|
311
327
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
screen.
|
|
315
|
-
|
|
316
|
-
}
|
|
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
|
+
}
|
|
317
333
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
screen.
|
|
321
|
-
|
|
322
|
-
|
|
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();
|
|
323
344
|
|
|
324
345
|
contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
|
|
325
346
|
screen.render();
|
|
@@ -339,7 +360,6 @@ export async function createMultiServerHistoricalUI(
|
|
|
339
360
|
const REFRESH_INTERVAL = 3000;
|
|
340
361
|
let lastGoodRender: string | null = null;
|
|
341
362
|
let consecutiveErrors = 0;
|
|
342
|
-
let viewMode: ViewMode = 'recent';
|
|
343
363
|
|
|
344
364
|
const contentBox = createContentBox();
|
|
345
365
|
screen.append(contentBox);
|
|
@@ -347,21 +367,21 @@ export async function createMultiServerHistoricalUI(
|
|
|
347
367
|
// Chart configurations for multi-server view
|
|
348
368
|
const chartConfigs: Record<string, ChartConfig> = {
|
|
349
369
|
tokenSpeed: {
|
|
350
|
-
title: 'Total
|
|
370
|
+
title: 'Total Server Token Generation Speed (tok/s)',
|
|
351
371
|
color: asciichart.cyan,
|
|
352
372
|
formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
|
|
353
373
|
isPercentage: false,
|
|
354
374
|
noDataMessage: 'No generation activity in this time window',
|
|
355
375
|
},
|
|
356
376
|
cpu: {
|
|
357
|
-
title: 'Total
|
|
377
|
+
title: 'Total Server CPU Usage (%)',
|
|
358
378
|
color: asciichart.blue,
|
|
359
379
|
formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
|
|
360
380
|
isPercentage: true,
|
|
361
381
|
noDataMessage: 'No CPU data in this time window',
|
|
362
382
|
},
|
|
363
383
|
memory: {
|
|
364
|
-
title: 'Total
|
|
384
|
+
title: 'Total Server Memory Usage (GB)',
|
|
365
385
|
color: asciichart.magenta,
|
|
366
386
|
formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
|
|
367
387
|
isPercentage: false,
|
|
@@ -384,8 +404,8 @@ export async function createMultiServerHistoricalUI(
|
|
|
384
404
|
const chartHeight = 5;
|
|
385
405
|
|
|
386
406
|
// Header
|
|
387
|
-
const modeLabel =
|
|
388
|
-
const modeColor =
|
|
407
|
+
const modeLabel = sharedViewMode === 'recent' ? 'Minute' : 'Hour';
|
|
408
|
+
const modeColor = sharedViewMode === 'recent' ? 'cyan' : 'magenta';
|
|
389
409
|
let content = `{bold}{blue-fg}\u2550\u2550\u2550 All servers (${servers.length}){/blue-fg} `;
|
|
390
410
|
content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
|
|
391
411
|
|
|
@@ -453,11 +473,11 @@ export async function createMultiServerHistoricalUI(
|
|
|
453
473
|
|
|
454
474
|
if (aggregatedData.length > 0) {
|
|
455
475
|
const maxPoints = Math.min(chartWidth, 80);
|
|
456
|
-
const displayData =
|
|
476
|
+
const displayData = sharedViewMode === 'recent' && aggregatedData.length > maxPoints
|
|
457
477
|
? aggregatedData.slice(-maxPoints)
|
|
458
478
|
: aggregatedData;
|
|
459
479
|
|
|
460
|
-
const useDownsampling =
|
|
480
|
+
const useDownsampling = sharedViewMode === 'hour';
|
|
461
481
|
|
|
462
482
|
// Extract time-series data
|
|
463
483
|
const rawData = {
|
|
@@ -492,7 +512,7 @@ export async function createMultiServerHistoricalUI(
|
|
|
492
512
|
|
|
493
513
|
// Footer
|
|
494
514
|
content += divider + '\n';
|
|
495
|
-
content += `{gray-fg}
|
|
515
|
+
content += `{gray-fg}[T]oggle Hour View [ESC] Back [Q]uit{/gray-fg}`;
|
|
496
516
|
|
|
497
517
|
contentBox.setContent(content);
|
|
498
518
|
screen.render();
|
|
@@ -509,7 +529,7 @@ export async function createMultiServerHistoricalUI(
|
|
|
509
529
|
'{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
|
|
510
530
|
`{red-fg}${errorMsg}{/red-fg}\n\n` +
|
|
511
531
|
`Consecutive errors: ${consecutiveErrors}\n\n` +
|
|
512
|
-
'{gray-fg}ESC
|
|
532
|
+
'{gray-fg}[ESC] Back [Q]uit{/gray-fg}'
|
|
513
533
|
);
|
|
514
534
|
}
|
|
515
535
|
screen.render();
|
|
@@ -521,24 +541,43 @@ export async function createMultiServerHistoricalUI(
|
|
|
521
541
|
clearInterval(refreshIntervalId);
|
|
522
542
|
refreshIntervalId = null;
|
|
523
543
|
}
|
|
544
|
+
unregisterHandlers();
|
|
524
545
|
}
|
|
525
546
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
};
|
|
530
564
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
screen.
|
|
534
|
-
|
|
535
|
-
}
|
|
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
|
+
}
|
|
536
570
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
screen.
|
|
540
|
-
|
|
541
|
-
|
|
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();
|
|
542
581
|
|
|
543
582
|
contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
|
|
544
583
|
screen.render();
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { modelScanner } from '../lib/model-scanner.js';
|
|
3
|
+
import { stateManager } from '../lib/state-manager.js';
|
|
4
|
+
import { launchctlManager } from '../lib/launchctl-manager.js';
|
|
5
|
+
import { ModelInfo } from '../types/model-info.js';
|
|
6
|
+
import { ServerConfig } from '../types/server-config.js';
|
|
7
|
+
import { formatBytes, formatDateShort } from '../utils/format-utils.js';
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import { createSearchUI } from './SearchApp.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Models management TUI
|
|
13
|
+
* Display installed models and allow deletion
|
|
14
|
+
*/
|
|
15
|
+
export async function createModelsUI(
|
|
16
|
+
screen: blessed.Widgets.Screen,
|
|
17
|
+
onBack: () => void,
|
|
18
|
+
onSearch: () => void
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
let models: ModelInfo[] = [];
|
|
21
|
+
let selectedIndex = 0;
|
|
22
|
+
let isLoading = false;
|
|
23
|
+
|
|
24
|
+
// Create content box
|
|
25
|
+
const contentBox = blessed.box({
|
|
26
|
+
top: 0,
|
|
27
|
+
left: 0,
|
|
28
|
+
width: '100%',
|
|
29
|
+
height: '100%',
|
|
30
|
+
tags: true,
|
|
31
|
+
scrollable: true,
|
|
32
|
+
alwaysScroll: true,
|
|
33
|
+
keys: true,
|
|
34
|
+
vi: true,
|
|
35
|
+
mouse: true,
|
|
36
|
+
scrollbar: {
|
|
37
|
+
ch: '█',
|
|
38
|
+
style: {
|
|
39
|
+
fg: 'blue',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
screen.append(contentBox);
|
|
44
|
+
|
|
45
|
+
// Render models view
|
|
46
|
+
async function render() {
|
|
47
|
+
const termWidth = (screen.width as number) || 80;
|
|
48
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
49
|
+
let content = '';
|
|
50
|
+
|
|
51
|
+
// Header
|
|
52
|
+
content += '{bold}{blue-fg}═══ Models Management{/blue-fg}{/bold}\n\n';
|
|
53
|
+
|
|
54
|
+
if (isLoading) {
|
|
55
|
+
content += '{cyan-fg}⏳ Loading models...{/cyan-fg}\n';
|
|
56
|
+
contentBox.setContent(content);
|
|
57
|
+
screen.render();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (models.length === 0) {
|
|
62
|
+
content += '{yellow-fg}No models found{/yellow-fg}\n\n';
|
|
63
|
+
content += '{dim}Models directory: ' + await stateManager.getModelsDirectory() + '{/dim}\n';
|
|
64
|
+
content += '{dim}Download models: Press [S] to search HuggingFace{/dim}\n';
|
|
65
|
+
content += '\n' + divider + '\n';
|
|
66
|
+
content += `{gray-fg}[S]earch [ESC] Back [Q]uit{/gray-fg}`;
|
|
67
|
+
contentBox.setContent(content);
|
|
68
|
+
screen.render();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// System info
|
|
73
|
+
const totalSize = models.reduce((sum, m) => sum + m.size, 0);
|
|
74
|
+
content += `{bold}Total: ${models.length} models{/bold} - ${formatBytes(totalSize)}\n`;
|
|
75
|
+
content += divider + '\n';
|
|
76
|
+
|
|
77
|
+
// Get all servers to check dependencies
|
|
78
|
+
const allServers = await stateManager.getAllServers();
|
|
79
|
+
|
|
80
|
+
// Table header
|
|
81
|
+
content += '{bold} │ Model File │ Size │ Modified │ Servers{/bold}\n';
|
|
82
|
+
content += divider + '\n';
|
|
83
|
+
|
|
84
|
+
// Model rows
|
|
85
|
+
for (let i = 0; i < models.length; i++) {
|
|
86
|
+
const model = models[i];
|
|
87
|
+
const isSelected = i === selectedIndex;
|
|
88
|
+
|
|
89
|
+
// Count servers using this model
|
|
90
|
+
const serversUsingModel = allServers.filter(s => s.modelPath === model.path);
|
|
91
|
+
const serverCount = serversUsingModel.length;
|
|
92
|
+
|
|
93
|
+
// Selection indicator
|
|
94
|
+
const indicator = isSelected ? '►' : ' ';
|
|
95
|
+
|
|
96
|
+
// Model filename (truncate if too long)
|
|
97
|
+
const maxFilenameLen = 46;
|
|
98
|
+
let filename = model.filename;
|
|
99
|
+
if (filename.length > maxFilenameLen) {
|
|
100
|
+
filename = filename.substring(0, maxFilenameLen - 3) + '...';
|
|
101
|
+
}
|
|
102
|
+
filename = filename.padEnd(maxFilenameLen);
|
|
103
|
+
|
|
104
|
+
// Size
|
|
105
|
+
const size = model.sizeFormatted.padStart(11);
|
|
106
|
+
|
|
107
|
+
// Modified date
|
|
108
|
+
const modified = formatDateShort(model.modified).padStart(11);
|
|
109
|
+
|
|
110
|
+
// Servers count with color coding
|
|
111
|
+
let serversText = '';
|
|
112
|
+
let serversTextPlain = '';
|
|
113
|
+
if (serverCount === 0) {
|
|
114
|
+
serversText = '{green-fg}0 servers{/green-fg}';
|
|
115
|
+
serversTextPlain = '0 servers';
|
|
116
|
+
} else {
|
|
117
|
+
const runningCount = serversUsingModel.filter(s => s.status === 'running').length;
|
|
118
|
+
if (runningCount > 0) {
|
|
119
|
+
serversText = `{yellow-fg}${serverCount} (${runningCount} running){/yellow-fg}`;
|
|
120
|
+
serversTextPlain = `${serverCount} (${runningCount} running)`;
|
|
121
|
+
} else {
|
|
122
|
+
serversText = `{gray-fg}${serverCount} stopped{/gray-fg}`;
|
|
123
|
+
serversTextPlain = `${serverCount} stopped`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build row content
|
|
128
|
+
let rowContent = '';
|
|
129
|
+
if (isSelected) {
|
|
130
|
+
// Selected row: cyan background with bright white text
|
|
131
|
+
rowContent = `{cyan-bg}{15-fg}${indicator} │ ${filename} │ ${size} │ ${modified} │ ${serversTextPlain}{/15-fg}{/cyan-bg}`;
|
|
132
|
+
} else {
|
|
133
|
+
// Normal row: with colored server text
|
|
134
|
+
rowContent = `${indicator} │ ${filename} │ ${size} │ ${modified} │ ${serversText}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
content += rowContent + '\n';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Footer
|
|
141
|
+
content += '\n' + divider + '\n';
|
|
142
|
+
content += '{gray-fg}[↑/↓] Navigate [D]elete [S]earch [R]efresh [ESC] Back [Q]uit{/gray-fg}';
|
|
143
|
+
|
|
144
|
+
contentBox.setContent(content);
|
|
145
|
+
screen.render();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Load models
|
|
149
|
+
async function loadModels() {
|
|
150
|
+
isLoading = true;
|
|
151
|
+
await render();
|
|
152
|
+
|
|
153
|
+
models = await modelScanner.scanModels();
|
|
154
|
+
selectedIndex = Math.min(selectedIndex, Math.max(0, models.length - 1));
|
|
155
|
+
|
|
156
|
+
isLoading = false;
|
|
157
|
+
await render();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Delete selected model
|
|
161
|
+
async function deleteModel() {
|
|
162
|
+
if (models.length === 0) return;
|
|
163
|
+
|
|
164
|
+
const model = models[selectedIndex];
|
|
165
|
+
const allServers = await stateManager.getAllServers();
|
|
166
|
+
const serversUsingModel = allServers.filter(s => s.modelPath === model.path);
|
|
167
|
+
|
|
168
|
+
// Show confirmation dialog
|
|
169
|
+
const confirmBox = blessed.message({
|
|
170
|
+
parent: screen,
|
|
171
|
+
top: 'center',
|
|
172
|
+
left: 'center',
|
|
173
|
+
width: '60%',
|
|
174
|
+
height: 'shrink',
|
|
175
|
+
border: { type: 'line' },
|
|
176
|
+
style: {
|
|
177
|
+
border: { fg: 'red' },
|
|
178
|
+
fg: 'white',
|
|
179
|
+
},
|
|
180
|
+
tags: true,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
let confirmText = `{bold}Delete model: ${model.filename}?{/bold}\n\n`;
|
|
184
|
+
confirmText += `Size: ${model.sizeFormatted}\n\n`;
|
|
185
|
+
|
|
186
|
+
if (serversUsingModel.length > 0) {
|
|
187
|
+
confirmText += `{yellow-fg}⚠️ This model has ${serversUsingModel.length} server(s) configured:{/yellow-fg}\n`;
|
|
188
|
+
for (const server of serversUsingModel) {
|
|
189
|
+
const statusColor = server.status === 'running' ? 'green-fg' : 'gray-fg';
|
|
190
|
+
confirmText += ` - ${server.id} ({${statusColor}}${server.status}{/${statusColor}})\n`;
|
|
191
|
+
}
|
|
192
|
+
confirmText += `\n{yellow-fg}These servers will be deleted before removing the model.{/yellow-fg}\n\n`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
confirmText += `Type 'yes' to confirm:\n\n\n\n`; // Extra lines for input box space
|
|
196
|
+
|
|
197
|
+
// Create input box for confirmation
|
|
198
|
+
const inputBox = blessed.textbox({
|
|
199
|
+
parent: confirmBox,
|
|
200
|
+
bottom: 1,
|
|
201
|
+
left: 2,
|
|
202
|
+
right: 2,
|
|
203
|
+
height: 3,
|
|
204
|
+
inputOnFocus: true,
|
|
205
|
+
border: { type: 'line' },
|
|
206
|
+
style: {
|
|
207
|
+
border: { fg: 'cyan' },
|
|
208
|
+
focus: { border: { fg: 'green' } },
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
confirmBox.setContent(confirmText);
|
|
213
|
+
screen.append(confirmBox);
|
|
214
|
+
confirmBox.focus();
|
|
215
|
+
inputBox.focus();
|
|
216
|
+
screen.render();
|
|
217
|
+
|
|
218
|
+
inputBox.on('submit', async (value: string) => {
|
|
219
|
+
screen.remove(confirmBox);
|
|
220
|
+
|
|
221
|
+
if (value.toLowerCase() !== 'yes') {
|
|
222
|
+
await render();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Show deleting message
|
|
227
|
+
isLoading = true;
|
|
228
|
+
contentBox.setContent('{cyan-fg}⏳ Deleting model...{/cyan-fg}');
|
|
229
|
+
screen.render();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// Delete all servers using this model
|
|
233
|
+
for (const server of serversUsingModel) {
|
|
234
|
+
// Unload service (stops and removes from launchd)
|
|
235
|
+
try {
|
|
236
|
+
await launchctlManager.unloadService(server.plistPath);
|
|
237
|
+
if (server.status === 'running') {
|
|
238
|
+
await launchctlManager.waitForServiceStop(server.label, 5000);
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// Continue even if unload fails
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Delete plist
|
|
245
|
+
await launchctlManager.deletePlist(server.plistPath);
|
|
246
|
+
|
|
247
|
+
// Delete server config
|
|
248
|
+
await stateManager.deleteServerConfig(server.id);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Delete model file
|
|
252
|
+
await fs.unlink(model.path);
|
|
253
|
+
|
|
254
|
+
// Reload models
|
|
255
|
+
await loadModels();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// Show error
|
|
258
|
+
const errorBox = blessed.message({
|
|
259
|
+
parent: screen,
|
|
260
|
+
top: 'center',
|
|
261
|
+
left: 'center',
|
|
262
|
+
width: '60%',
|
|
263
|
+
height: 'shrink',
|
|
264
|
+
border: { type: 'line' },
|
|
265
|
+
style: {
|
|
266
|
+
border: { fg: 'red' },
|
|
267
|
+
fg: 'red',
|
|
268
|
+
},
|
|
269
|
+
tags: true,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
273
|
+
errorBox.display(`{bold}Delete failed{/bold}\n\n${errorMsg}\n\nPress any key to continue`, () => {
|
|
274
|
+
screen.remove(errorBox);
|
|
275
|
+
isLoading = false;
|
|
276
|
+
render();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
inputBox.on('cancel', () => {
|
|
282
|
+
screen.remove(confirmBox);
|
|
283
|
+
render();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
inputBox.key(['escape'], () => {
|
|
287
|
+
screen.remove(confirmBox);
|
|
288
|
+
render();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Store key handler references for cleanup
|
|
293
|
+
const keyHandlers = {
|
|
294
|
+
up: () => {
|
|
295
|
+
if (models.length === 0) return;
|
|
296
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
297
|
+
render();
|
|
298
|
+
},
|
|
299
|
+
down: () => {
|
|
300
|
+
if (models.length === 0) return;
|
|
301
|
+
selectedIndex = Math.min(models.length - 1, selectedIndex + 1);
|
|
302
|
+
render();
|
|
303
|
+
},
|
|
304
|
+
delete: () => {
|
|
305
|
+
deleteModel();
|
|
306
|
+
},
|
|
307
|
+
search: async () => {
|
|
308
|
+
// Cleanup current handlers before switching views
|
|
309
|
+
cleanup();
|
|
310
|
+
|
|
311
|
+
// Open search view
|
|
312
|
+
await createSearchUI(screen, async () => {
|
|
313
|
+
// onBack callback - return to models view
|
|
314
|
+
// Re-register handlers
|
|
315
|
+
registerHandlers();
|
|
316
|
+
screen.append(contentBox);
|
|
317
|
+
await loadModels();
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
refresh: () => {
|
|
321
|
+
loadModels();
|
|
322
|
+
},
|
|
323
|
+
escape: async () => {
|
|
324
|
+
cleanup();
|
|
325
|
+
await onBack();
|
|
326
|
+
},
|
|
327
|
+
quit: () => {
|
|
328
|
+
screen.destroy();
|
|
329
|
+
process.exit(0);
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Cleanup function to unregister all handlers
|
|
334
|
+
function cleanup() {
|
|
335
|
+
screen.unkey('up', keyHandlers.up);
|
|
336
|
+
screen.unkey('k', keyHandlers.up);
|
|
337
|
+
screen.unkey('down', keyHandlers.down);
|
|
338
|
+
screen.unkey('j', keyHandlers.down);
|
|
339
|
+
screen.unkey('d', keyHandlers.delete);
|
|
340
|
+
screen.unkey('D', keyHandlers.delete);
|
|
341
|
+
screen.unkey('s', keyHandlers.search);
|
|
342
|
+
screen.unkey('S', keyHandlers.search);
|
|
343
|
+
screen.unkey('r', keyHandlers.refresh);
|
|
344
|
+
screen.unkey('R', keyHandlers.refresh);
|
|
345
|
+
screen.unkey('escape', keyHandlers.escape);
|
|
346
|
+
screen.unkey('q', keyHandlers.quit);
|
|
347
|
+
screen.unkey('Q', keyHandlers.quit);
|
|
348
|
+
screen.unkey('C-c', keyHandlers.quit);
|
|
349
|
+
screen.remove(contentBox);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Register key handlers
|
|
353
|
+
function registerHandlers() {
|
|
354
|
+
screen.key(['up', 'k'], keyHandlers.up);
|
|
355
|
+
screen.key(['down', 'j'], keyHandlers.down);
|
|
356
|
+
screen.key(['d', 'D'], keyHandlers.delete);
|
|
357
|
+
screen.key(['s', 'S'], keyHandlers.search);
|
|
358
|
+
screen.key(['r', 'R'], keyHandlers.refresh);
|
|
359
|
+
screen.key(['escape'], keyHandlers.escape);
|
|
360
|
+
screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Register initial handlers
|
|
364
|
+
registerHandlers();
|
|
365
|
+
|
|
366
|
+
// Initial load
|
|
367
|
+
await loadModels();
|
|
368
|
+
}
|