@appkit/llamacpp-cli 2.0.0 → 2.1.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 (229) hide show
  1. package/README.md +271 -277
  2. package/dist/cli.js +133 -23
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/admin/config.d.ts +1 -1
  5. package/dist/commands/admin/config.js +5 -5
  6. package/dist/commands/admin/config.js.map +1 -1
  7. package/dist/commands/admin/log-config.d.ts +11 -0
  8. package/dist/commands/admin/log-config.d.ts.map +1 -0
  9. package/dist/commands/admin/log-config.js +159 -0
  10. package/dist/commands/admin/log-config.js.map +1 -0
  11. package/dist/commands/admin/logs.d.ts +2 -3
  12. package/dist/commands/admin/logs.d.ts.map +1 -1
  13. package/dist/commands/admin/logs.js +6 -48
  14. package/dist/commands/admin/logs.js.map +1 -1
  15. package/dist/commands/admin/status.d.ts.map +1 -1
  16. package/dist/commands/admin/status.js +1 -0
  17. package/dist/commands/admin/status.js.map +1 -1
  18. package/dist/commands/config.d.ts +1 -0
  19. package/dist/commands/config.d.ts.map +1 -1
  20. package/dist/commands/config.js +63 -196
  21. package/dist/commands/config.js.map +1 -1
  22. package/dist/commands/create.d.ts +3 -2
  23. package/dist/commands/create.d.ts.map +1 -1
  24. package/dist/commands/create.js +24 -97
  25. package/dist/commands/create.js.map +1 -1
  26. package/dist/commands/delete.d.ts.map +1 -1
  27. package/dist/commands/delete.js +7 -24
  28. package/dist/commands/delete.js.map +1 -1
  29. package/dist/commands/internal/server-wrapper.d.ts +15 -0
  30. package/dist/commands/internal/server-wrapper.d.ts.map +1 -0
  31. package/dist/commands/internal/server-wrapper.js +126 -0
  32. package/dist/commands/internal/server-wrapper.js.map +1 -0
  33. package/dist/commands/logs-all.d.ts +0 -2
  34. package/dist/commands/logs-all.d.ts.map +1 -1
  35. package/dist/commands/logs-all.js +1 -61
  36. package/dist/commands/logs-all.js.map +1 -1
  37. package/dist/commands/logs.d.ts +2 -5
  38. package/dist/commands/logs.d.ts.map +1 -1
  39. package/dist/commands/logs.js +104 -120
  40. package/dist/commands/logs.js.map +1 -1
  41. package/dist/commands/migrate-labels.d.ts +12 -0
  42. package/dist/commands/migrate-labels.d.ts.map +1 -0
  43. package/dist/commands/migrate-labels.js +160 -0
  44. package/dist/commands/migrate-labels.js.map +1 -0
  45. package/dist/commands/ps.d.ts.map +1 -1
  46. package/dist/commands/ps.js +2 -1
  47. package/dist/commands/ps.js.map +1 -1
  48. package/dist/commands/rm.d.ts.map +1 -1
  49. package/dist/commands/rm.js +22 -48
  50. package/dist/commands/rm.js.map +1 -1
  51. package/dist/commands/router/config.d.ts +1 -1
  52. package/dist/commands/router/config.js +6 -6
  53. package/dist/commands/router/config.js.map +1 -1
  54. package/dist/commands/router/logs.d.ts +2 -4
  55. package/dist/commands/router/logs.d.ts.map +1 -1
  56. package/dist/commands/router/logs.js +34 -189
  57. package/dist/commands/router/logs.js.map +1 -1
  58. package/dist/commands/router/status.d.ts.map +1 -1
  59. package/dist/commands/router/status.js +1 -0
  60. package/dist/commands/router/status.js.map +1 -1
  61. package/dist/commands/server-show.d.ts.map +1 -1
  62. package/dist/commands/server-show.js +3 -0
  63. package/dist/commands/server-show.js.map +1 -1
  64. package/dist/commands/start.d.ts.map +1 -1
  65. package/dist/commands/start.js +21 -72
  66. package/dist/commands/start.js.map +1 -1
  67. package/dist/commands/stop.d.ts.map +1 -1
  68. package/dist/commands/stop.js +10 -26
  69. package/dist/commands/stop.js.map +1 -1
  70. package/dist/launchers/llamacpp-admin +8 -0
  71. package/dist/launchers/llamacpp-router +8 -0
  72. package/dist/launchers/llamacpp-server +8 -0
  73. package/dist/lib/admin-manager.d.ts +4 -0
  74. package/dist/lib/admin-manager.d.ts.map +1 -1
  75. package/dist/lib/admin-manager.js +42 -18
  76. package/dist/lib/admin-manager.js.map +1 -1
  77. package/dist/lib/admin-server.d.ts +48 -1
  78. package/dist/lib/admin-server.d.ts.map +1 -1
  79. package/dist/lib/admin-server.js +632 -238
  80. package/dist/lib/admin-server.js.map +1 -1
  81. package/dist/lib/config-generator.d.ts +1 -0
  82. package/dist/lib/config-generator.d.ts.map +1 -1
  83. package/dist/lib/config-generator.js +12 -5
  84. package/dist/lib/config-generator.js.map +1 -1
  85. package/dist/lib/keyboard-manager.d.ts +162 -0
  86. package/dist/lib/keyboard-manager.d.ts.map +1 -0
  87. package/dist/lib/keyboard-manager.js +247 -0
  88. package/dist/lib/keyboard-manager.js.map +1 -0
  89. package/dist/lib/label-migration.d.ts +65 -0
  90. package/dist/lib/label-migration.d.ts.map +1 -0
  91. package/dist/lib/label-migration.js +458 -0
  92. package/dist/lib/label-migration.js.map +1 -0
  93. package/dist/lib/launchctl-manager.d.ts +9 -0
  94. package/dist/lib/launchctl-manager.d.ts.map +1 -1
  95. package/dist/lib/launchctl-manager.js +65 -19
  96. package/dist/lib/launchctl-manager.js.map +1 -1
  97. package/dist/lib/log-management-service.d.ts +51 -0
  98. package/dist/lib/log-management-service.d.ts.map +1 -0
  99. package/dist/lib/log-management-service.js +124 -0
  100. package/dist/lib/log-management-service.js.map +1 -0
  101. package/dist/lib/log-workers.d.ts +70 -0
  102. package/dist/lib/log-workers.d.ts.map +1 -0
  103. package/dist/lib/log-workers.js +217 -0
  104. package/dist/lib/log-workers.js.map +1 -0
  105. package/dist/lib/model-downloader.d.ts +9 -1
  106. package/dist/lib/model-downloader.d.ts.map +1 -1
  107. package/dist/lib/model-downloader.js +98 -1
  108. package/dist/lib/model-downloader.js.map +1 -1
  109. package/dist/lib/model-management-service.d.ts +60 -0
  110. package/dist/lib/model-management-service.d.ts.map +1 -0
  111. package/dist/lib/model-management-service.js +246 -0
  112. package/dist/lib/model-management-service.js.map +1 -0
  113. package/dist/lib/model-management-service.test.d.ts +2 -0
  114. package/dist/lib/model-management-service.test.d.ts.map +1 -0
  115. package/dist/lib/model-management-service.test.js.map +1 -0
  116. package/dist/lib/model-scanner.d.ts +15 -3
  117. package/dist/lib/model-scanner.d.ts.map +1 -1
  118. package/dist/lib/model-scanner.js +174 -17
  119. package/dist/lib/model-scanner.js.map +1 -1
  120. package/dist/lib/openapi-spec.d.ts +1335 -0
  121. package/dist/lib/openapi-spec.d.ts.map +1 -0
  122. package/dist/lib/openapi-spec.js +1017 -0
  123. package/dist/lib/openapi-spec.js.map +1 -0
  124. package/dist/lib/router-logger.d.ts +1 -1
  125. package/dist/lib/router-logger.d.ts.map +1 -1
  126. package/dist/lib/router-logger.js +13 -11
  127. package/dist/lib/router-logger.js.map +1 -1
  128. package/dist/lib/router-manager.d.ts +4 -0
  129. package/dist/lib/router-manager.d.ts.map +1 -1
  130. package/dist/lib/router-manager.js +30 -18
  131. package/dist/lib/router-manager.js.map +1 -1
  132. package/dist/lib/router-server.d.ts.map +1 -1
  133. package/dist/lib/router-server.js +22 -12
  134. package/dist/lib/router-server.js.map +1 -1
  135. package/dist/lib/server-config-service.d.ts +51 -0
  136. package/dist/lib/server-config-service.d.ts.map +1 -0
  137. package/dist/lib/server-config-service.js +310 -0
  138. package/dist/lib/server-config-service.js.map +1 -0
  139. package/dist/lib/server-config-service.test.d.ts +2 -0
  140. package/dist/lib/server-config-service.test.d.ts.map +1 -0
  141. package/dist/lib/server-config-service.test.js.map +1 -0
  142. package/dist/lib/server-lifecycle-service.d.ts +172 -0
  143. package/dist/lib/server-lifecycle-service.d.ts.map +1 -0
  144. package/dist/lib/server-lifecycle-service.js +619 -0
  145. package/dist/lib/server-lifecycle-service.js.map +1 -0
  146. package/dist/lib/state-manager.d.ts +18 -1
  147. package/dist/lib/state-manager.d.ts.map +1 -1
  148. package/dist/lib/state-manager.js +51 -2
  149. package/dist/lib/state-manager.js.map +1 -1
  150. package/dist/lib/status-checker.d.ts +11 -4
  151. package/dist/lib/status-checker.d.ts.map +1 -1
  152. package/dist/lib/status-checker.js +34 -1
  153. package/dist/lib/status-checker.js.map +1 -1
  154. package/dist/lib/validation-service.d.ts +43 -0
  155. package/dist/lib/validation-service.d.ts.map +1 -0
  156. package/dist/lib/validation-service.js +112 -0
  157. package/dist/lib/validation-service.js.map +1 -0
  158. package/dist/lib/validation-service.test.d.ts +2 -0
  159. package/dist/lib/validation-service.test.d.ts.map +1 -0
  160. package/dist/lib/validation-service.test.js.map +1 -0
  161. package/dist/scripts/http-log-filter.sh +8 -0
  162. package/dist/tui/ConfigApp.d.ts.map +1 -1
  163. package/dist/tui/ConfigApp.js +222 -184
  164. package/dist/tui/ConfigApp.js.map +1 -1
  165. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
  166. package/dist/tui/HistoricalMonitorApp.js +12 -0
  167. package/dist/tui/HistoricalMonitorApp.js.map +1 -1
  168. package/dist/tui/ModelsApp.d.ts.map +1 -1
  169. package/dist/tui/ModelsApp.js +93 -17
  170. package/dist/tui/ModelsApp.js.map +1 -1
  171. package/dist/tui/MonitorApp.d.ts.map +1 -1
  172. package/dist/tui/MonitorApp.js +1 -3
  173. package/dist/tui/MonitorApp.js.map +1 -1
  174. package/dist/tui/MultiServerMonitorApp.d.ts +3 -3
  175. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  176. package/dist/tui/MultiServerMonitorApp.js +724 -508
  177. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  178. package/dist/tui/RootNavigator.d.ts.map +1 -1
  179. package/dist/tui/RootNavigator.js +17 -1
  180. package/dist/tui/RootNavigator.js.map +1 -1
  181. package/dist/tui/RouterApp.d.ts +6 -0
  182. package/dist/tui/RouterApp.d.ts.map +1 -0
  183. package/dist/tui/RouterApp.js +928 -0
  184. package/dist/tui/RouterApp.js.map +1 -0
  185. package/dist/tui/SearchApp.d.ts.map +1 -1
  186. package/dist/tui/SearchApp.js +27 -6
  187. package/dist/tui/SearchApp.js.map +1 -1
  188. package/dist/tui/shared/modal-controller.d.ts +65 -0
  189. package/dist/tui/shared/modal-controller.d.ts.map +1 -0
  190. package/dist/tui/shared/modal-controller.js +625 -0
  191. package/dist/tui/shared/modal-controller.js.map +1 -0
  192. package/dist/tui/shared/overlay-utils.d.ts +7 -0
  193. package/dist/tui/shared/overlay-utils.d.ts.map +1 -0
  194. package/dist/tui/shared/overlay-utils.js +54 -0
  195. package/dist/tui/shared/overlay-utils.js.map +1 -0
  196. package/dist/types/admin-config.d.ts +15 -2
  197. package/dist/types/admin-config.d.ts.map +1 -1
  198. package/dist/types/model-info.d.ts +5 -0
  199. package/dist/types/model-info.d.ts.map +1 -1
  200. package/dist/types/router-config.d.ts +2 -2
  201. package/dist/types/router-config.d.ts.map +1 -1
  202. package/dist/types/server-config.d.ts +8 -0
  203. package/dist/types/server-config.d.ts.map +1 -1
  204. package/dist/types/server-config.js +25 -0
  205. package/dist/types/server-config.js.map +1 -1
  206. package/dist/utils/http-log-filter.d.ts +10 -0
  207. package/dist/utils/http-log-filter.d.ts.map +1 -0
  208. package/dist/utils/http-log-filter.js +84 -0
  209. package/dist/utils/http-log-filter.js.map +1 -0
  210. package/dist/utils/log-parser.d.ts.map +1 -1
  211. package/dist/utils/log-parser.js +7 -4
  212. package/dist/utils/log-parser.js.map +1 -1
  213. package/dist/utils/log-utils.d.ts +59 -4
  214. package/dist/utils/log-utils.d.ts.map +1 -1
  215. package/dist/utils/log-utils.js +150 -11
  216. package/dist/utils/log-utils.js.map +1 -1
  217. package/dist/utils/shard-utils.d.ts +72 -0
  218. package/dist/utils/shard-utils.d.ts.map +1 -0
  219. package/dist/utils/shard-utils.js +168 -0
  220. package/dist/utils/shard-utils.js.map +1 -0
  221. package/package.json +18 -4
  222. package/src/launchers/llamacpp-admin +8 -0
  223. package/src/launchers/llamacpp-router +8 -0
  224. package/src/launchers/llamacpp-server +8 -0
  225. package/web/dist/assets/index-Byhoy86V.css +1 -0
  226. package/web/dist/assets/index-HSrgvray.js +50 -0
  227. package/web/dist/index.html +2 -2
  228. package/web/dist/assets/index-Bin89Lwr.css +0 -1
  229. package/web/dist/assets/index-CVmonw3T.js +0 -17
@@ -47,6 +47,8 @@ const port_manager_js_1 = require("../lib/port-manager.js");
47
47
  const model_scanner_js_1 = require("../lib/model-scanner.js");
48
48
  const file_utils_js_1 = require("../utils/file-utils.js");
49
49
  const log_utils_js_1 = require("../utils/log-utils.js");
50
+ const modal_controller_js_1 = require("./shared/modal-controller.js");
51
+ const keyboard_manager_js_1 = require("../lib/keyboard-manager.js");
50
52
  /**
51
53
  * Config screen TUI for editing server configuration
52
54
  */
@@ -61,6 +63,28 @@ async function createConfigUI(screen, server, onBack) {
61
63
  value: server.modelName,
62
64
  originalValue: server.modelName,
63
65
  },
66
+ {
67
+ key: 'alias',
68
+ label: 'Alias',
69
+ type: 'text',
70
+ value: server.alias || '',
71
+ originalValue: server.alias || '',
72
+ validation: async (value) => {
73
+ // Empty string is valid (means remove alias)
74
+ if (value.trim() === '')
75
+ return null;
76
+ // Validate format
77
+ const formatError = (0, server_config_js_1.validateAlias)(value.trim());
78
+ if (formatError)
79
+ return formatError;
80
+ // Check uniqueness (exclude current server)
81
+ const conflictingServerId = await state_manager_js_1.stateManager.isAliasAvailable(value.trim(), server.id);
82
+ if (conflictingServerId) {
83
+ return `Alias already used by server: ${conflictingServerId}`;
84
+ }
85
+ return null;
86
+ },
87
+ },
64
88
  {
65
89
  key: 'host',
66
90
  label: 'Host',
@@ -118,8 +142,8 @@ async function createConfigUI(screen, server, onBack) {
118
142
  value: server.gpuLayers,
119
143
  originalValue: server.gpuLayers,
120
144
  validation: (value) => {
121
- if (value < 0)
122
- return 'Must be at least 0';
145
+ if (value < -1)
146
+ return 'Must be -1 (all) or a non-negative integer';
123
147
  if (value > 999)
124
148
  return 'Must be at most 999';
125
149
  return null;
@@ -144,6 +168,9 @@ async function createConfigUI(screen, server, onBack) {
144
168
  selectedIndex: 0,
145
169
  hasChanges: false,
146
170
  };
171
+ // Keyboard manager and modal controller for centralized keyboard handling
172
+ const keyboardManager = new keyboard_manager_js_1.KeyboardManager(screen);
173
+ const modalController = new modal_controller_js_1.ModalController(screen, keyboardManager);
147
174
  // Create content box
148
175
  const contentBox = blessed_1.default.box({
149
176
  top: 0,
@@ -265,6 +292,7 @@ async function createConfigUI(screen, server, onBack) {
265
292
  screen.render();
266
293
  }
267
294
  // Create a centered modal box
295
+ // Helper to create modal element (for custom modals with special logic)
268
296
  function createModal(title, height = 'shrink') {
269
297
  return blessed_1.default.box({
270
298
  parent: screen,
@@ -281,11 +309,34 @@ async function createConfigUI(screen, server, onBack) {
281
309
  label: ` ${title} `,
282
310
  });
283
311
  }
284
- // Number input modal
285
- async function editNumber(field) {
312
+ // Helper to create semi-transparent overlay
313
+ function createOverlay() {
314
+ return blessed_1.default.box({
315
+ parent: screen,
316
+ top: 0,
317
+ left: 0,
318
+ width: '100%',
319
+ height: '100%',
320
+ style: {
321
+ bg: 'black',
322
+ transparent: true,
323
+ },
324
+ });
325
+ }
326
+ /**
327
+ * Number input modal with context size support
328
+ * Note: This function unregisters handlers before opening modal.
329
+ * Handlers are re-registered when modal closes via callback.
330
+ */
331
+ async function editNumber(field, onClose) {
286
332
  unregisterHandlers();
287
333
  return new Promise((resolve) => {
334
+ const handleClose = onClose || (() => {
335
+ registerHandlers();
336
+ render();
337
+ });
288
338
  const isCtxSize = field.key === 'ctxSize';
339
+ const overlay = createOverlay();
289
340
  const modal = createModal(`Edit ${field.label}`, isCtxSize ? 11 : 10);
290
341
  const currentDisplay = isCtxSize
291
342
  ? formatContextSize(field.value)
@@ -332,9 +383,11 @@ async function createConfigUI(screen, server, onBack) {
332
383
  ? formatContextSize(field.value)
333
384
  : String(field.value);
334
385
  inputBox.setValue(initialValue);
386
+ screen.append(overlay);
387
+ screen.append(modal);
335
388
  screen.render();
336
389
  inputBox.focus();
337
- inputBox.on('submit', (value) => {
390
+ inputBox.on('submit', async (value) => {
338
391
  let numValue;
339
392
  if (isCtxSize) {
340
393
  numValue = parseContextSize(value);
@@ -346,9 +399,17 @@ async function createConfigUI(screen, server, onBack) {
346
399
  }
347
400
  if (numValue !== null) {
348
401
  if (field.validation) {
349
- const error = field.validation(numValue);
350
- if (error) {
351
- infoText.setContent(`{red-fg}Error: ${error}{/red-fg}`);
402
+ try {
403
+ const error = await field.validation(numValue);
404
+ if (error) {
405
+ infoText.setContent(`{red-fg}Error: ${error}{/red-fg}`);
406
+ screen.render();
407
+ inputBox.focus();
408
+ return;
409
+ }
410
+ }
411
+ catch (err) {
412
+ infoText.setContent(`{red-fg}Validation error: ${err.message}{/red-fg}`);
352
413
  screen.render();
353
414
  inputBox.focus();
354
415
  return;
@@ -358,34 +419,37 @@ async function createConfigUI(screen, server, onBack) {
358
419
  updateHasChanges();
359
420
  }
360
421
  screen.remove(modal);
361
- registerHandlers();
362
- render();
422
+ screen.remove(overlay);
423
+ handleClose();
363
424
  resolve();
364
425
  });
365
426
  inputBox.on('cancel', () => {
366
427
  screen.remove(modal);
367
- registerHandlers();
368
- render();
369
- resolve();
370
- });
371
- inputBox.key(['escape'], () => {
372
- screen.remove(modal);
373
- registerHandlers();
374
- render();
428
+ screen.remove(overlay);
429
+ handleClose();
375
430
  resolve();
376
431
  });
377
432
  });
378
433
  }
379
- // Toggle/select modal
380
- async function editSelect(field) {
434
+ /**
435
+ * Toggle/select modal
436
+ * Note: This function unregisters handlers before opening modal.
437
+ * Handlers are re-registered when modal closes via callback.
438
+ */
439
+ async function editSelect(field, onClose) {
381
440
  unregisterHandlers();
382
441
  return new Promise((resolve) => {
442
+ const handleClose = onClose || (() => {
443
+ registerHandlers();
444
+ render();
445
+ });
383
446
  const options = field.options || [];
384
447
  let selectedOption = field.type === 'toggle'
385
448
  ? (field.value ? 1 : 0)
386
449
  : options.indexOf(String(field.value));
387
450
  if (selectedOption < 0)
388
451
  selectedOption = 0;
452
+ const overlay = createOverlay();
389
453
  const modal = createModal(field.label, options.length + 6);
390
454
  function renderOptions() {
391
455
  let content = '\n';
@@ -407,6 +471,8 @@ async function createConfigUI(screen, server, onBack) {
407
471
  modal.setContent(content);
408
472
  screen.render();
409
473
  }
474
+ screen.append(overlay);
475
+ screen.append(modal);
410
476
  renderOptions();
411
477
  modal.focus();
412
478
  modal.key(['up', 'k'], () => {
@@ -426,28 +492,43 @@ async function createConfigUI(screen, server, onBack) {
426
492
  }
427
493
  updateHasChanges();
428
494
  screen.remove(modal);
429
- registerHandlers();
430
- render();
495
+ screen.remove(overlay);
496
+ handleClose();
431
497
  resolve();
432
498
  });
433
499
  modal.key(['escape'], () => {
434
500
  screen.remove(modal);
435
- registerHandlers();
436
- render();
501
+ screen.remove(overlay);
502
+ handleClose();
437
503
  resolve();
438
504
  });
439
505
  });
440
506
  }
441
- // Text input modal
442
- async function editText(field) {
507
+ /**
508
+ * Text input modal
509
+ * Note: This function unregisters handlers before opening modal.
510
+ * Handlers are re-registered when modal closes via callback.
511
+ */
512
+ async function editText(field, onClose) {
443
513
  unregisterHandlers();
444
514
  return new Promise((resolve) => {
515
+ const handleClose = onClose || (() => {
516
+ registerHandlers();
517
+ render();
518
+ });
519
+ const overlay = createOverlay();
445
520
  const modal = createModal(`Edit ${field.label}`, 10);
521
+ // Customize info text based on field type
522
+ const infoContent = field.key === 'customFlags'
523
+ ? 'Enter comma-separated flags:'
524
+ : field.key === 'alias'
525
+ ? 'Enter alias (or leave empty to remove):'
526
+ : `Enter ${field.label.toLowerCase()}:`;
446
527
  const infoText = blessed_1.default.text({
447
528
  parent: modal,
448
529
  top: 1,
449
530
  left: 2,
450
- content: 'Enter comma-separated flags:',
531
+ content: infoContent,
451
532
  tags: true,
452
533
  });
453
534
  const inputBox = blessed_1.default.textbox({
@@ -471,45 +552,72 @@ async function createConfigUI(screen, server, onBack) {
471
552
  tags: true,
472
553
  });
473
554
  inputBox.setValue(field.value || '');
555
+ screen.append(overlay);
556
+ screen.append(modal);
474
557
  screen.render();
475
558
  inputBox.focus();
476
- inputBox.on('submit', (value) => {
477
- field.value = value.trim();
559
+ inputBox.on('submit', async (value) => {
560
+ const trimmedValue = value.trim();
561
+ // Run validation if present (handle both sync and async)
562
+ if (field.validation) {
563
+ try {
564
+ const error = await field.validation(trimmedValue);
565
+ if (error) {
566
+ infoText.setContent(`{red-fg}Error: ${error}{/red-fg}`);
567
+ screen.render();
568
+ inputBox.focus();
569
+ return;
570
+ }
571
+ }
572
+ catch (err) {
573
+ infoText.setContent(`{red-fg}Validation error: ${err.message}{/red-fg}`);
574
+ screen.render();
575
+ inputBox.focus();
576
+ return;
577
+ }
578
+ }
579
+ field.value = trimmedValue;
478
580
  updateHasChanges();
479
581
  screen.remove(modal);
480
- registerHandlers();
481
- render();
582
+ screen.remove(overlay);
583
+ handleClose();
482
584
  resolve();
483
585
  });
484
586
  inputBox.on('cancel', () => {
485
587
  screen.remove(modal);
486
- registerHandlers();
487
- render();
488
- resolve();
489
- });
490
- inputBox.key(['escape'], () => {
491
- screen.remove(modal);
492
- registerHandlers();
493
- render();
588
+ screen.remove(overlay);
589
+ handleClose();
494
590
  resolve();
495
591
  });
496
592
  });
497
593
  }
498
594
  // Model picker modal
499
- async function editModel(field) {
595
+ /**
596
+ * Model selection modal
597
+ * Note: This function unregisters handlers before opening modal.
598
+ * Handlers are re-registered when modal closes via callback.
599
+ */
600
+ async function editModel(field, onClose) {
500
601
  unregisterHandlers();
501
602
  return new Promise(async (resolve) => {
603
+ const handleClose = onClose || (() => {
604
+ registerHandlers();
605
+ render();
606
+ });
502
607
  const models = await model_scanner_js_1.modelScanner.scanModels();
503
608
  if (models.length === 0) {
504
609
  // Show error modal
610
+ const errorOverlay = createOverlay();
505
611
  const errorModal = createModal('Error', 7);
506
612
  errorModal.setContent('\n {red-fg}No models found in ~/models{/red-fg}\n\n {gray-fg}[ESC] Close{/gray-fg}');
613
+ screen.append(errorOverlay);
614
+ screen.append(errorModal);
507
615
  screen.render();
508
616
  errorModal.focus();
509
617
  errorModal.key(['escape', 'enter'], () => {
510
618
  screen.remove(errorModal);
511
- registerHandlers();
512
- render();
619
+ screen.remove(errorOverlay);
620
+ handleClose();
513
621
  resolve();
514
622
  });
515
623
  return;
@@ -519,6 +627,7 @@ async function createConfigUI(screen, server, onBack) {
519
627
  selectedIndex = 0;
520
628
  let scrollOffset = 0;
521
629
  const maxVisible = 8;
630
+ const overlay = createOverlay();
522
631
  const modal = createModal('Select Model', maxVisible + 6);
523
632
  function renderModels() {
524
633
  // Adjust scroll offset
@@ -559,6 +668,8 @@ async function createConfigUI(screen, server, onBack) {
559
668
  modal.setContent(content);
560
669
  screen.render();
561
670
  }
671
+ screen.append(overlay);
672
+ screen.append(modal);
562
673
  renderModels();
563
674
  modal.focus();
564
675
  modal.key(['up', 'k'], () => {
@@ -573,153 +684,69 @@ async function createConfigUI(screen, server, onBack) {
573
684
  field.value = models[selectedIndex].filename;
574
685
  updateHasChanges();
575
686
  screen.remove(modal);
576
- registerHandlers();
577
- render();
687
+ screen.remove(overlay);
688
+ handleClose();
578
689
  resolve();
579
690
  });
580
691
  modal.key(['escape'], () => {
581
692
  screen.remove(modal);
582
- registerHandlers();
583
- render();
693
+ screen.remove(overlay);
694
+ handleClose();
584
695
  resolve();
585
696
  });
586
697
  });
587
698
  }
588
- // Show unsaved changes dialog
589
- async function showUnsavedDialog() {
590
- unregisterHandlers();
591
- return new Promise((resolve) => {
592
- const modal = createModal('Unsaved Changes', 10);
593
- let selectedOption = 0;
594
- const options = [
595
- { key: 'save', label: '[S]ave and exit' },
596
- { key: 'discard', label: '[D]iscard changes' },
597
- { key: 'continue', label: '[C]ontinue editing' },
598
- ];
599
- function renderDialog() {
600
- let content = '\n';
601
- for (let i = 0; i < options.length; i++) {
602
- const isSelected = i === selectedOption;
603
- if (isSelected) {
604
- content += ` {cyan-fg}► ${options[i].label}{/cyan-fg}\n`;
605
- }
606
- else {
607
- content += ` ${options[i].label}\n`;
608
- }
609
- }
610
- modal.setContent(content);
611
- screen.render();
612
- }
613
- renderDialog();
614
- modal.focus();
615
- modal.key(['up', 'k'], () => {
616
- selectedOption = Math.max(0, selectedOption - 1);
617
- renderDialog();
618
- });
619
- modal.key(['down', 'j'], () => {
620
- selectedOption = Math.min(options.length - 1, selectedOption + 1);
621
- renderDialog();
622
- });
623
- modal.key(['enter'], () => {
624
- screen.remove(modal);
625
- registerHandlers();
626
- resolve(options[selectedOption].key);
627
- });
628
- modal.key(['s', 'S'], () => {
629
- screen.remove(modal);
630
- registerHandlers();
631
- resolve('save');
632
- });
633
- modal.key(['d', 'D'], () => {
634
- screen.remove(modal);
635
- registerHandlers();
636
- resolve('discard');
637
- });
638
- modal.key(['c', 'C', 'escape'], () => {
639
- screen.remove(modal);
640
- registerHandlers();
641
- resolve('continue');
642
- });
643
- });
699
+ /**
700
+ * Show unsaved changes dialog
701
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
702
+ * Pass empty function for complex flows where outer function manages handlers.
703
+ */
704
+ async function showUnsavedDialog(onClose) {
705
+ return modalController.showUnsavedDialog(onClose || (() => {
706
+ registerHandlers();
707
+ render();
708
+ }));
644
709
  }
645
- // Show restart confirmation dialog
646
- async function showRestartDialog() {
647
- unregisterHandlers();
648
- return new Promise((resolve) => {
649
- const modal = createModal('Server is Running', 10);
650
- let selectedOption = 0;
651
- const options = [
652
- { key: true, label: '[Y]es - Restart now' },
653
- { key: false, label: '[N]o - Apply later' },
654
- ];
655
- function renderDialog() {
656
- let content = '\n Restart to apply changes?\n\n';
657
- for (let i = 0; i < options.length; i++) {
658
- const isSelected = i === selectedOption;
659
- if (isSelected) {
660
- content += ` {cyan-fg}► ${options[i].label}{/cyan-fg}\n`;
661
- }
662
- else {
663
- content += ` ${options[i].label}\n`;
664
- }
665
- }
666
- modal.setContent(content);
667
- screen.render();
668
- }
669
- renderDialog();
670
- modal.focus();
671
- modal.key(['up', 'k'], () => {
672
- selectedOption = Math.max(0, selectedOption - 1);
673
- renderDialog();
674
- });
675
- modal.key(['down', 'j'], () => {
676
- selectedOption = Math.min(options.length - 1, selectedOption + 1);
677
- renderDialog();
678
- });
679
- modal.key(['enter'], () => {
680
- screen.remove(modal);
681
- registerHandlers();
682
- resolve(options[selectedOption].key);
683
- });
684
- modal.key(['y', 'Y'], () => {
685
- screen.remove(modal);
686
- registerHandlers();
687
- resolve(true);
688
- });
689
- modal.key(['n', 'N', 'escape'], () => {
690
- screen.remove(modal);
691
- registerHandlers();
692
- resolve(false);
693
- });
694
- });
710
+ /**
711
+ * Show restart confirmation dialog
712
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
713
+ * Pass empty function for complex flows where outer function manages handlers.
714
+ */
715
+ async function showRestartDialog(onClose) {
716
+ return modalController.showRestartDialog(server.modelName, onClose || (() => {
717
+ registerHandlers();
718
+ render();
719
+ }));
695
720
  }
696
- // Show progress modal
697
- function showProgress(message) {
698
- const modal = createModal('Working', 6);
699
- modal.setContent(`\n {cyan-fg}${message}{/cyan-fg}`);
700
- screen.render();
701
- return modal;
721
+ /**
722
+ * Show error modal
723
+ * @param message - Error message to display
724
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
725
+ * Pass empty function for complex flows where outer function manages handlers.
726
+ */
727
+ async function showError(message, onClose) {
728
+ await modalController.showError(message, onClose || (() => {
729
+ registerHandlers();
730
+ render();
731
+ }));
702
732
  }
703
- // Show error message
704
- async function showError(message) {
705
- unregisterHandlers();
706
- return new Promise((resolve) => {
707
- const modal = createModal('Error', 8);
708
- modal.setContent(`\n {red-fg}❌ ${message}{/red-fg}\n\n {gray-fg}[Enter] Close{/gray-fg}`);
709
- screen.render();
710
- modal.focus();
711
- modal.key(['enter', 'escape'], () => {
712
- screen.remove(modal);
713
- registerHandlers();
714
- render();
715
- resolve();
716
- });
717
- });
733
+ /**
734
+ * Show success modal
735
+ * @param message - Success message to display
736
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
737
+ * Pass empty function for complex flows where outer function manages handlers.
738
+ */
739
+ async function showSuccess(message, onClose) {
740
+ await modalController.showSuccess(message, onClose || (() => {
741
+ registerHandlers();
742
+ render();
743
+ }));
718
744
  }
719
745
  // Save changes
720
746
  async function saveChanges() {
721
747
  // Build updates object
722
748
  const modelField = state.fields.find(f => f.key === 'model');
749
+ const aliasField = state.fields.find(f => f.key === 'alias');
723
750
  const hostField = state.fields.find(f => f.key === 'host');
724
751
  const portField = state.fields.find(f => f.key === 'port');
725
752
  const threadsField = state.fields.find(f => f.key === 'threads');
@@ -763,10 +790,13 @@ async function createConfigUI(screen, server, onBack) {
763
790
  const wasRunning = currentStatus.status === 'running';
764
791
  let shouldRestart = false;
765
792
  if (wasRunning) {
766
- shouldRestart = await showRestartDialog();
793
+ unregisterHandlers();
794
+ // Pass empty onClose - saveChanges manages handler registration
795
+ shouldRestart = await showRestartDialog(() => { });
796
+ registerHandlers();
767
797
  }
768
798
  // Show progress
769
- const progressModal = showProgress('Saving configuration...');
799
+ const progressModal = modalController.showProgress('Saving configuration...');
770
800
  try {
771
801
  // Parse custom flags
772
802
  const customFlags = customFlagsField.value
@@ -797,6 +827,7 @@ async function createConfigUI(screen, server, onBack) {
797
827
  id: newServerId,
798
828
  modelPath: newModelPath,
799
829
  modelName: newModelName,
830
+ ...(aliasField.value ? { alias: aliasField.value } : { alias: undefined }),
800
831
  host: hostField.value,
801
832
  port: portField.value,
802
833
  threads: threadsField.value,
@@ -804,8 +835,8 @@ async function createConfigUI(screen, server, onBack) {
804
835
  gpuLayers: gpuLayersField.value,
805
836
  verbose: verboseField.value,
806
837
  customFlags: customFlags && customFlags.length > 0 ? customFlags : undefined,
807
- label: `com.llama.${newServerId}`,
808
- plistPath: path.join(plistDir, `com.llama.${newServerId}.plist`),
838
+ label: `studio.appkit.llamacpp-cli.${newServerId}`,
839
+ plistPath: path.join(plistDir, `studio.appkit.llamacpp-cli.${newServerId}.plist`),
809
840
  stdoutPath: path.join(logsDir, `${newServerId}.stdout`),
810
841
  stderrPath: path.join(logsDir, `${newServerId}.stderr`),
811
842
  status: 'stopped',
@@ -823,10 +854,10 @@ async function createConfigUI(screen, server, onBack) {
823
854
  await launchctl_manager_js_1.launchctlManager.startService(newConfig.label);
824
855
  await new Promise(resolve => setTimeout(resolve, 2000));
825
856
  const finalStatus = await status_checker_js_1.statusChecker.updateServerStatus(newConfig);
826
- screen.remove(progressModal);
857
+ modalController.closeProgress(progressModal);
827
858
  return finalStatus;
828
859
  }
829
- screen.remove(progressModal);
860
+ modalController.closeProgress(progressModal);
830
861
  return newConfig;
831
862
  }
832
863
  else {
@@ -845,6 +876,7 @@ async function createConfigUI(screen, server, onBack) {
845
876
  gpuLayers: gpuLayersField.value,
846
877
  verbose: verboseField.value,
847
878
  customFlags: customFlags && customFlags.length > 0 ? customFlags : undefined,
879
+ ...(aliasField.value ? { alias: aliasField.value } : { alias: undefined }),
848
880
  };
849
881
  if (newModelPath && newModelName) {
850
882
  updatedConfig.modelPath = newModelPath;
@@ -871,20 +903,20 @@ async function createConfigUI(screen, server, onBack) {
871
903
  await launchctl_manager_js_1.launchctlManager.startService(fullConfig.label);
872
904
  await new Promise(resolve => setTimeout(resolve, 2000));
873
905
  const finalStatus = await status_checker_js_1.statusChecker.updateServerStatus(fullConfig);
874
- screen.remove(progressModal);
906
+ modalController.closeProgress(progressModal);
875
907
  return finalStatus;
876
908
  }
877
- screen.remove(progressModal);
909
+ modalController.closeProgress(progressModal);
878
910
  return fullConfig;
879
911
  }
880
912
  }
881
913
  }
882
914
  catch (err) {
883
- screen.remove(progressModal);
915
+ modalController.closeProgress(progressModal);
884
916
  await showError(err instanceof Error ? err.message : 'Unknown error');
885
917
  return null;
886
918
  }
887
- screen.remove(progressModal);
919
+ modalController.closeProgress(progressModal);
888
920
  return null;
889
921
  }
890
922
  // Handle edit action for selected field
@@ -908,8 +940,14 @@ async function createConfigUI(screen, server, onBack) {
908
940
  }
909
941
  // Handle escape/cancel
910
942
  async function handleEscape() {
943
+ // Don't handle if modal is open
944
+ if (modalController.isModalOpen())
945
+ return;
911
946
  if (state.hasChanges) {
912
- const result = await showUnsavedDialog();
947
+ unregisterHandlers();
948
+ // Pass empty onClose - handleEscape manages handler registration
949
+ const result = await showUnsavedDialog(() => { });
950
+ registerHandlers();
913
951
  if (result === 'save') {
914
952
  const updated = await saveChanges();
915
953
  cleanup();