@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
@@ -38,7 +38,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.createMultiServerMonitorUI = createMultiServerMonitorUI;
40
40
  const blessed_1 = __importDefault(require("blessed"));
41
- const path = __importStar(require("path"));
42
41
  const fs = __importStar(require("fs/promises"));
43
42
  const metrics_aggregator_js_1 = require("../lib/metrics-aggregator.js");
44
43
  const system_collector_js_1 = require("../lib/system-collector.js");
@@ -46,17 +45,19 @@ const history_manager_js_1 = require("../lib/history-manager.js");
46
45
  const HistoricalMonitorApp_js_1 = require("./HistoricalMonitorApp.js");
47
46
  const ConfigApp_js_1 = require("./ConfigApp.js");
48
47
  const state_manager_js_1 = require("../lib/state-manager.js");
49
- const launchctl_manager_js_1 = require("../lib/launchctl-manager.js");
50
- const status_checker_js_1 = require("../lib/status-checker.js");
48
+ const server_lifecycle_service_js_1 = require("../lib/server-lifecycle-service.js");
51
49
  const model_scanner_js_1 = require("../lib/model-scanner.js");
52
50
  const port_manager_js_1 = require("../lib/port-manager.js");
53
- const config_generator_js_1 = require("../lib/config-generator.js");
54
51
  const file_utils_js_1 = require("../utils/file-utils.js");
55
- const process_utils_js_1 = require("../utils/process-utils.js");
56
- async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage = false, directJumpIndex, onModels, onFirstRender) {
52
+ const modal_controller_js_1 = require("./shared/modal-controller.js");
53
+ const overlay_utils_js_1 = require("./shared/overlay-utils.js");
54
+ const keyboard_manager_js_1 = require("../lib/keyboard-manager.js");
55
+ const log_utils_js_1 = require("../utils/log-utils.js");
56
+ const log_parser_js_1 = require("../utils/log-parser.js");
57
+ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage = false, directJumpIndex, onModels, onRouter, onFirstRender) {
57
58
  let updateInterval = 5000;
58
59
  let intervalId = null;
59
- let viewMode = directJumpIndex !== undefined ? 'detail' : 'list';
60
+ let viewMode = directJumpIndex !== undefined ? "detail" : "list";
60
61
  let selectedServerIndex = directJumpIndex ?? 0;
61
62
  let selectedRowIndex = directJumpIndex ?? 0; // Track which row is highlighted in list view
62
63
  let isLoading = false;
@@ -64,8 +65,15 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
64
65
  let cameFromDirectJump = directJumpIndex !== undefined; // Track if we entered via ps <id>
65
66
  let inHistoricalView = false; // Track whether we're in historical view to prevent key conflicts
66
67
  let hasCalledFirstRender = false; // Track if we've called onFirstRender callback
68
+ let detailSubView = "status"; // Track sub-view within detail view
69
+ let logsLastUpdated = null;
70
+ let logsRefreshInterval = null;
71
+ let logType = 'stdout'; // Track which log type to display (activity/system)
72
+ // Keyboard manager for centralized keyboard event handling
73
+ const keyboardManager = new keyboard_manager_js_1.KeyboardManager(screen);
74
+ const modalController = new modal_controller_js_1.ModalController(screen, keyboardManager);
67
75
  // Spinner animation
68
- const spinnerFrames = ['', '', '', '', '', '', '', '', '', ''];
76
+ const spinnerFrames = ["", "", "", "", "", "", "", "", "", ""];
69
77
  let spinnerFrameIndex = 0;
70
78
  let spinnerIntervalId = null;
71
79
  const systemCollector = new system_collector_js_1.SystemCollector();
@@ -86,8 +94,8 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
86
94
  const contentBox = blessed_1.default.box({
87
95
  top: 0,
88
96
  left: 0,
89
- width: '100%',
90
- height: '100%',
97
+ width: "100%",
98
+ height: "100%",
91
99
  tags: true,
92
100
  scrollable: true,
93
101
  alwaysScroll: true,
@@ -95,9 +103,9 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
95
103
  vi: true,
96
104
  mouse: true,
97
105
  scrollbar: {
98
- ch: '',
106
+ ch: "",
99
107
  style: {
100
- fg: 'blue',
108
+ fg: "blue",
101
109
  },
102
110
  },
103
111
  });
@@ -106,15 +114,18 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
106
114
  function createProgressBar(percentage, width = 30) {
107
115
  const filled = Math.round((percentage / 100) * width);
108
116
  const empty = width - filled;
109
- return '[' + '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty)) + ']';
117
+ return ("[" +
118
+ "█".repeat(Math.max(0, filled)) +
119
+ "░".repeat(Math.max(0, empty)) +
120
+ "]");
110
121
  }
111
122
  // Render system resources section (system-wide for list view)
112
123
  function renderSystemResources(systemMetrics) {
113
- let content = '';
114
- content += '{bold}System Resources{/bold}\n';
124
+ let content = "";
125
+ content += "{bold}System Resources{/bold}\n";
115
126
  const termWidth = screen.width || 80;
116
- const divider = ''.repeat(termWidth - 2);
117
- content += divider + '\n';
127
+ const divider = "".repeat(termWidth - 2);
128
+ content += divider + "\n";
118
129
  if (systemMetrics) {
119
130
  if (systemMetrics.gpuUsage !== undefined) {
120
131
  const bar = createProgressBar(systemMetrics.gpuUsage);
@@ -122,7 +133,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
122
133
  if (systemMetrics.temperature !== undefined) {
123
134
  content += ` - ${Math.round(systemMetrics.temperature)}°C`;
124
135
  }
125
- content += '\n';
136
+ content += "\n";
126
137
  }
127
138
  if (systemMetrics.cpuUsage !== undefined) {
128
139
  const bar = createProgressBar(systemMetrics.cpuUsage);
@@ -133,36 +144,38 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
133
144
  content += `ANE: {cyan-fg}${bar}{/cyan-fg} ${Math.round(systemMetrics.aneUsage)}%\n`;
134
145
  }
135
146
  if (systemMetrics.memoryTotal > 0) {
136
- const memoryUsedGB = systemMetrics.memoryUsed / (1024 ** 3);
137
- const memoryTotalGB = systemMetrics.memoryTotal / (1024 ** 3);
147
+ const memoryUsedGB = systemMetrics.memoryUsed / 1024 ** 3;
148
+ const memoryTotalGB = systemMetrics.memoryTotal / 1024 ** 3;
138
149
  const memoryPercentage = (systemMetrics.memoryUsed / systemMetrics.memoryTotal) * 100;
139
150
  const bar = createProgressBar(memoryPercentage);
140
151
  content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${Math.round(memoryPercentage)}% `;
141
152
  content += `(${memoryUsedGB.toFixed(1)} / ${memoryTotalGB.toFixed(1)} GB)\n`;
142
153
  }
143
154
  if (systemMetrics.warnings && systemMetrics.warnings.length > 0) {
144
- content += `\n{yellow-fg}⚠ ${systemMetrics.warnings.join(', ')}{/yellow-fg}\n`;
155
+ content += `\n{yellow-fg}⚠ ${systemMetrics.warnings.join(", ")}{/yellow-fg}\n`;
145
156
  }
146
157
  }
147
158
  else {
148
- content += '{gray-fg}Collecting system metrics...{/gray-fg}\n';
159
+ content += "{gray-fg}Collecting system metrics...{/gray-fg}\n";
149
160
  }
150
161
  return content;
151
162
  }
152
163
  // Render aggregate model resources (all running servers in list view)
153
164
  function renderAggregateModelResources() {
154
- let content = '';
155
- content += '{bold}Server Resources{/bold}\n';
165
+ let content = "";
166
+ content += "{bold}Server Resources{/bold}\n";
156
167
  const termWidth = screen.width || 80;
157
- const divider = ''.repeat(termWidth - 2);
158
- content += divider + '\n';
168
+ const divider = "".repeat(termWidth - 2);
169
+ content += divider + "\n";
159
170
  // Aggregate CPU and memory across all running servers (skip stopped servers)
160
171
  let totalCpu = 0;
161
172
  let totalMemoryBytes = 0;
162
173
  let serverCount = 0;
163
174
  for (const serverData of serverDataMap.values()) {
164
175
  // Only count running servers with valid data
165
- if (serverData.server.status === 'running' && serverData.data?.server && !serverData.data.server.stale) {
176
+ if (serverData.server.status === "running" &&
177
+ serverData.data?.server &&
178
+ !serverData.data.server.stale) {
166
179
  if (serverData.data.server.processCpuUsage !== undefined) {
167
180
  totalCpu += serverData.data.server.processCpuUsage;
168
181
  serverCount++;
@@ -173,29 +186,29 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
173
186
  }
174
187
  }
175
188
  if (serverCount === 0) {
176
- content += '{gray-fg}No running servers{/gray-fg}\n';
189
+ content += "{gray-fg}No running servers{/gray-fg}\n";
177
190
  return content;
178
191
  }
179
192
  // CPU: Sum of all process CPU percentages
180
193
  const cpuBar = createProgressBar(Math.min(totalCpu, 100));
181
194
  content += `CPU: {cyan-fg}${cpuBar}{/cyan-fg} ${Math.round(totalCpu)}%`;
182
- content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
195
+ content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? "server" : "servers"}){/gray-fg}\n`;
183
196
  // Memory: Sum of all process memory
184
- const totalMemoryGB = totalMemoryBytes / (1024 ** 3);
197
+ const totalMemoryGB = totalMemoryBytes / 1024 ** 3;
185
198
  const estimatedMaxGB = serverCount * 8; // Assume ~8GB per server max
186
199
  const memoryPercentage = Math.min((totalMemoryGB / estimatedMaxGB) * 100, 100);
187
200
  const memoryBar = createProgressBar(memoryPercentage);
188
201
  content += `Memory: {cyan-fg}${memoryBar}{/cyan-fg} ${totalMemoryGB.toFixed(2)} GB`;
189
- content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
202
+ content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? "server" : "servers"}){/gray-fg}\n`;
190
203
  return content;
191
204
  }
192
205
  // Render model resources section (per-process for detail view)
193
206
  function renderModelResources(data) {
194
- let content = '';
195
- content += '{bold}Server Resources{/bold}\n';
207
+ let content = "";
208
+ content += "{bold}Server Resources{/bold}\n";
196
209
  const termWidth = screen.width || 80;
197
- const divider = ''.repeat(termWidth - 2);
198
- content += divider + '\n';
210
+ const divider = "".repeat(termWidth - 2);
211
+ content += divider + "\n";
199
212
  // GPU: System-wide (can't get per-process on macOS)
200
213
  if (data.system && data.system.gpuUsage !== undefined) {
201
214
  const bar = createProgressBar(data.system.gpuUsage);
@@ -203,7 +216,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
203
216
  if (data.system.temperature !== undefined) {
204
217
  content += ` - ${Math.round(data.system.temperature)}°C`;
205
218
  }
206
- content += '\n';
219
+ content += "\n";
207
220
  }
208
221
  // CPU: Per-process
209
222
  if (data.server.processCpuUsage !== undefined) {
@@ -212,14 +225,16 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
212
225
  }
213
226
  // Memory: Per-process
214
227
  if (data.server.processMemory !== undefined) {
215
- const memoryGB = data.server.processMemory / (1024 ** 3);
228
+ const memoryGB = data.server.processMemory / 1024 ** 3;
216
229
  const estimatedMax = 8;
217
230
  const memoryPercentage = Math.min((memoryGB / estimatedMax) * 100, 100);
218
231
  const bar = createProgressBar(memoryPercentage);
219
232
  content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${memoryGB.toFixed(2)} GB\n`;
220
233
  }
221
- if (data.system && data.system.warnings && data.system.warnings.length > 0) {
222
- content += `\n{yellow-fg}⚠ ${data.system.warnings.join(', ')}{/yellow-fg}\n`;
234
+ if (data.system &&
235
+ data.system.warnings &&
236
+ data.system.warnings.length > 0) {
237
+ content += `\n{yellow-fg}⚠ ${data.system.warnings.join(", ")}{/yellow-fg}\n`;
223
238
  }
224
239
  return content;
225
240
  }
@@ -233,8 +248,8 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
233
248
  spinnerIntervalId = setInterval(() => {
234
249
  spinnerFrameIndex = (spinnerFrameIndex + 1) % spinnerFrames.length;
235
250
  // Re-render current view with updated spinner frame
236
- let content = '';
237
- if (viewMode === 'list') {
251
+ let content = "";
252
+ if (viewMode === "list") {
238
253
  content = renderListView(lastSystemMetrics);
239
254
  }
240
255
  else {
@@ -244,8 +259,8 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
244
259
  screen.render();
245
260
  }, 80);
246
261
  // Immediate first render
247
- let content = '';
248
- if (viewMode === 'list') {
262
+ let content = "";
263
+ if (viewMode === "list") {
249
264
  content = renderListView(lastSystemMetrics);
250
265
  }
251
266
  else {
@@ -265,22 +280,23 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
265
280
  // Render list view
266
281
  function renderListView(systemMetrics) {
267
282
  const termWidth = screen.width || 80;
268
- const divider = ''.repeat(termWidth - 2);
269
- let content = '';
283
+ const divider = "".repeat(termWidth - 2);
284
+ let content = "";
270
285
  // Header
271
- content += '{bold}{blue-fg}═══ llama.cpp{/blue-fg}{/bold}\n\n';
286
+ content += "{bold}{blue-fg}═══ LLAMACPP{/blue-fg}{/bold}\n\n";
272
287
  // System resources
273
288
  content += renderSystemResources(systemMetrics);
274
- content += '\n';
289
+ content += "\n";
275
290
  // Aggregate model resources (CPU + memory for all running servers)
276
291
  content += renderAggregateModelResources();
277
- content += '\n';
292
+ content += "\n";
278
293
  // Server list header
279
- const runningCount = servers.filter(s => s.status === 'running').length;
280
- const stoppedCount = servers.filter(s => s.status !== 'running').length;
294
+ const runningCount = servers.filter((s) => s.status === "running").length;
295
+ const stoppedCount = servers.filter((s) => s.status !== "running").length;
281
296
  content += `{bold}Servers (${runningCount} running, ${stoppedCount} stopped){/bold}\n`;
282
- content += '{gray-fg}Use arrow keys to navigate, Enter to view details{/gray-fg}\n';
283
- content += divider + '\n';
297
+ content +=
298
+ "{gray-fg}Use arrow keys to navigate, Enter to view details{/gray-fg}\n";
299
+ content += divider + "\n";
284
300
  // Calculate Server ID column width (variable based on screen width)
285
301
  // Fixed columns breakdown:
286
302
  // indicator(1) + " │ "(3) + " │ "(3) + port(4) + " │ "(3) + status(6) + "│ "(2) +
@@ -290,60 +306,68 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
290
306
  const maxServerIdWidth = 60;
291
307
  const serverIdWidth = Math.max(minServerIdWidth, Math.min(maxServerIdWidth, termWidth - fixedColumnsWidth));
292
308
  // Table header with variable Server ID width
293
- const serverIdHeader = 'Server ID'.padEnd(serverIdWidth);
309
+ const serverIdHeader = "Server ID".padEnd(serverIdWidth);
294
310
  content += `{bold} │ ${serverIdHeader}│ Port │ Status │ Slots │ tok/s │ Memory{/bold}\n`;
295
- content += divider + '\n';
311
+ content += divider + "\n";
296
312
  // Server rows
297
313
  servers.forEach((server, index) => {
298
314
  const serverData = serverDataMap.get(server.id);
299
315
  const isSelected = index === selectedRowIndex;
300
316
  // Selection indicator (arrow for selected row)
301
317
  // Use plain arrow for selected (will be white), colored for unselected indicator
302
- const indicator = isSelected ? '' : ' ';
318
+ const indicator = isSelected ? "" : " ";
303
319
  // Server ID (variable width, truncate if longer than available space)
304
- const serverId = server.id.padEnd(serverIdWidth).substring(0, serverIdWidth);
320
+ // Show alias in parens if present
321
+ const serverIdText = server.alias
322
+ ? `${server.id} (${server.alias})`
323
+ : server.id;
324
+ const serverId = serverIdText
325
+ .padEnd(serverIdWidth)
326
+ .substring(0, serverIdWidth);
305
327
  // Port
306
328
  const port = server.port.toString().padStart(4);
307
329
  // Status - Check actual server status first, then health
308
330
  // Build two versions: colored for normal, plain for selected
309
- let status = '';
310
- let statusPlain = '';
311
- if (server.status !== 'running') {
331
+ let status = "";
332
+ let statusPlain = "";
333
+ if (server.status !== "running") {
312
334
  // Server is stopped according to config
313
- status = '{gray-fg}○ OFF{/gray-fg} ';
314
- statusPlain = '○ OFF ';
335
+ status = "{gray-fg}○ OFF{/gray-fg} ";
336
+ statusPlain = "○ OFF ";
315
337
  }
316
338
  else if (serverData?.data) {
317
339
  // Server is running and we have data
318
340
  if (serverData.data.server.healthy) {
319
- status = '{green-fg}● RUN{/green-fg} ';
320
- statusPlain = '● RUN ';
341
+ status = "{green-fg}● RUN{/green-fg} ";
342
+ statusPlain = "● RUN ";
321
343
  }
322
344
  else {
323
- status = '{red-fg}● ERR{/red-fg} ';
324
- statusPlain = '● ERR ';
345
+ status = "{red-fg}● ERR{/red-fg} ";
346
+ statusPlain = "● ERR ";
325
347
  }
326
348
  }
327
349
  else {
328
350
  // Server is running but no data yet (still loading)
329
- status = '{yellow-fg}● ...{/yellow-fg} ';
330
- statusPlain = '● ... ';
351
+ status = "{yellow-fg}● ...{/yellow-fg} ";
352
+ statusPlain = "● ... ";
331
353
  }
332
354
  // Slots
333
- let slots = '- ';
355
+ let slots = "- ";
334
356
  if (serverData?.data?.server) {
335
357
  const active = serverData.data.server.activeSlots;
336
358
  const total = serverData.data.server.totalSlots;
337
359
  slots = `${active}/${total}`.padStart(5);
338
360
  }
339
361
  // tok/s
340
- let tokensPerSec = '- ';
362
+ let tokensPerSec = "- ";
341
363
  if (serverData?.data?.server.avgGenerateSpeed !== undefined &&
342
364
  serverData.data.server.avgGenerateSpeed > 0) {
343
- tokensPerSec = Math.round(serverData.data.server.avgGenerateSpeed).toString().padStart(6);
365
+ tokensPerSec = Math.round(serverData.data.server.avgGenerateSpeed)
366
+ .toString()
367
+ .padStart(6);
344
368
  }
345
369
  // Memory (actual process memory from top command)
346
- let memory = '- ';
370
+ let memory = "- ";
347
371
  if (serverData?.data?.server.processMemory) {
348
372
  const bytes = serverData.data.server.processMemory;
349
373
  // Format as GB/MB depending on size
@@ -357,7 +381,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
357
381
  }
358
382
  }
359
383
  // Build row content - use plain status for selected rows
360
- let rowContent = '';
384
+ let rowContent = "";
361
385
  if (isSelected) {
362
386
  // Use color code 15 (bright white) with cyan background
363
387
  // When white-bg worked, it was probably auto-selecting bright white fg
@@ -367,11 +391,11 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
367
391
  // Use colored status for normal rows
368
392
  rowContent = `${indicator} │ ${serverId} │ ${port} │ ${status}│ ${slots} │ ${tokensPerSec} │ ${memory}`;
369
393
  }
370
- content += rowContent + '\n';
394
+ content += rowContent + "\n";
371
395
  });
372
396
  // Footer
373
- content += '\n' + divider + '\n';
374
- content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | [N]ew [M]odels [H]istory [Q]uit{/gray-fg}`;
397
+ content += "\n" + divider + "\n";
398
+ content += `{gray-fg}[N]ew [M]odels [R]outer [H]istory [Q]uit{/gray-fg}`;
375
399
  return content;
376
400
  }
377
401
  // Render detail view for selected server
@@ -379,72 +403,79 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
379
403
  const server = servers[selectedServerIndex];
380
404
  const serverData = serverDataMap.get(server.id);
381
405
  const termWidth = screen.width || 80;
382
- const divider = ''.repeat(termWidth - 2);
383
- let content = '';
406
+ const divider = "".repeat(termWidth - 2);
407
+ let content = "";
384
408
  // Header
385
- content += `{bold}{blue-fg}═══ ${server.id} (${server.port}){/blue-fg}{/bold}\n\n`;
409
+ const headerText = server.alias
410
+ ? `${server.id} (${server.alias})`
411
+ : server.id;
412
+ content += `{bold}{blue-fg}═══ ${headerText} (${server.port}){/blue-fg}{/bold}\n\n`;
386
413
  // Check if server is stopped
387
- if (server.status !== 'running') {
414
+ if (server.status !== "running") {
388
415
  // Show minimal stopped server info
389
- content += '{bold}Server Information{/bold}\n';
390
- content += divider + '\n';
416
+ content += "{bold}Server Information{/bold}\n";
417
+ content += divider + "\n";
391
418
  content += `Status: {gray-fg}○ STOPPED{/gray-fg}\n`;
392
419
  content += `Model: ${server.modelName}\n`;
393
- const displayHost = server.host || '127.0.0.1';
420
+ const displayHost = server.host || "127.0.0.1";
394
421
  content += `Endpoint: http://${displayHost}:${server.port}\n`;
395
422
  // Footer - show [S]tart for stopped servers
396
- content += '\n' + divider + '\n';
397
- content += `{gray-fg}[S]tart [C]onfig [R]emove [H]istory [ESC] Back [Q]uit{/gray-fg}`;
423
+ content += "\n" + divider + "\n";
424
+ content += `{gray-fg}[S]tart [C]onfig [R]emove [L]ogs [H]istory [ESC] Back [Q]uit{/gray-fg}`;
398
425
  return content;
399
426
  }
400
427
  if (!serverData?.data) {
401
- content += '{yellow-fg}Loading server data...{/yellow-fg}\n';
428
+ content += "{yellow-fg}Loading server data...{/yellow-fg}\n";
402
429
  return content;
403
430
  }
404
431
  const data = serverData.data;
405
432
  // Model resources (per-process)
406
433
  content += renderModelResources(data);
407
- content += '\n';
434
+ content += "\n";
408
435
  // Server Information
409
- content += '{bold}Server Information{/bold}\n';
410
- content += divider + '\n';
411
- const statusIcon = data.server.healthy ? '{green-fg}●{/green-fg}' : '{red-fg}●{/red-fg}';
412
- const statusText = data.server.healthy ? 'RUNNING' : 'UNHEALTHY';
436
+ content += "{bold}Server Information{/bold}\n";
437
+ content += divider + "\n";
438
+ const statusIcon = data.server.healthy
439
+ ? "{green-fg}●{/green-fg}"
440
+ : "{red-fg}●{/red-fg}";
441
+ const statusText = data.server.healthy ? "RUNNING" : "UNHEALTHY";
413
442
  content += `Status: ${statusIcon} ${statusText}`;
414
443
  if (data.server.uptime) {
415
444
  content += ` Uptime: ${data.server.uptime}`;
416
445
  }
417
- content += '\n';
446
+ content += "\n";
418
447
  content += `Model: ${server.modelName}`;
419
448
  if (data.server.contextSize) {
420
449
  content += ` Context: ${formatContextSize(data.server.contextSize)}/slot`;
421
450
  }
422
- content += '\n';
451
+ content += "\n";
423
452
  // Handle null host (legacy configs) by defaulting to 127.0.0.1
424
- const displayHost = server.host || '127.0.0.1';
453
+ const displayHost = server.host || "127.0.0.1";
425
454
  content += `Endpoint: http://${displayHost}:${server.port}\n`;
426
455
  content += `Slots: ${data.server.activeSlots} active / ${data.server.totalSlots} total\n`;
427
- content += '\n';
456
+ content += "\n";
428
457
  // Request Metrics
429
458
  if (data.server.totalSlots > 0) {
430
- content += '{bold}Request Metrics{/bold}\n';
431
- content += divider + '\n';
459
+ content += "{bold}Request Metrics{/bold}\n";
460
+ content += divider + "\n";
432
461
  content += `Active: ${data.server.activeSlots} / ${data.server.totalSlots}\n`;
433
462
  content += `Idle: ${data.server.idleSlots} / ${data.server.totalSlots}\n`;
434
- if (data.server.avgPromptSpeed !== undefined && data.server.avgPromptSpeed > 0) {
463
+ if (data.server.avgPromptSpeed !== undefined &&
464
+ data.server.avgPromptSpeed > 0) {
435
465
  content += `Prompt: ${Math.round(data.server.avgPromptSpeed)} tokens/sec\n`;
436
466
  }
437
- if (data.server.avgGenerateSpeed !== undefined && data.server.avgGenerateSpeed > 0) {
467
+ if (data.server.avgGenerateSpeed !== undefined &&
468
+ data.server.avgGenerateSpeed > 0) {
438
469
  content += `Generate: ${Math.round(data.server.avgGenerateSpeed)} tokens/sec\n`;
439
470
  }
440
- content += '\n';
471
+ content += "\n";
441
472
  }
442
473
  // Active Slots Detail
443
474
  if (data.server.slots.length > 0) {
444
- const activeSlots = data.server.slots.filter(s => s.state === 'processing');
475
+ const activeSlots = data.server.slots.filter((s) => s.state === "processing");
445
476
  if (activeSlots.length > 0) {
446
- content += '{bold}Active Slots{/bold}\n';
447
- content += divider + '\n';
477
+ content += "{bold}Active Slots{/bold}\n";
478
+ content += divider + "\n";
448
479
  activeSlots.forEach((slot) => {
449
480
  content += `Slot #${slot.id}: {yellow-fg}PROCESSING{/yellow-fg}`;
450
481
  if (slot.timings?.predicted_per_second) {
@@ -455,28 +486,246 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
455
486
  if (slot.n_ctx) {
456
487
  content += ` / ${slot.n_ctx}`;
457
488
  }
458
- content += ' tokens';
489
+ content += " tokens";
459
490
  }
460
- content += '\n';
491
+ content += "\n";
461
492
  });
462
- content += '\n';
493
+ content += "\n";
463
494
  }
464
495
  }
465
496
  // Footer - show [S]top for running servers
466
- content += divider + '\n';
467
- content += `{gray-fg}[S]top [C]onfig [R]emove [H]istory [ESC] Back [Q]uit{/gray-fg}`;
497
+ content += divider + "\n";
498
+ content += `{gray-fg}[S]top [C]onfig [R]emove [L]ogs [H]istory [ESC] Back [Q]uit{/gray-fg}`;
499
+ return content;
500
+ }
501
+ // Render logs view for selected server
502
+ async function renderServerLogs() {
503
+ const server = servers[selectedServerIndex];
504
+ const termWidth = screen.width || 80;
505
+ const divider = "─".repeat(termWidth - 2);
506
+ let content = "";
507
+ // Header
508
+ const headerText = server.alias
509
+ ? `${server.id} (${server.alias})`
510
+ : server.id;
511
+ const logTypeLabel = logType === 'stdout' ? 'Activity' : 'System';
512
+ content += `{bold}{blue-fg}═══ ${headerText} - ${logTypeLabel} Logs{/blue-fg}{/bold}\n`;
513
+ // Show refresh status
514
+ const refreshStatus = logsRefreshInterval ? "ON" : "OFF";
515
+ const refreshColor = logsRefreshInterval ? "green" : "gray";
516
+ content += `{gray-fg}Auto-refresh: {${refreshColor}-fg}${refreshStatus}{/${refreshColor}-fg}{/gray-fg}\n`;
517
+ // Show slot status
518
+ const serverData = serverDataMap.get(server.id);
519
+ if (serverData?.data?.server) {
520
+ const active = serverData.data.server.activeSlots;
521
+ const total = serverData.data.server.totalSlots;
522
+ if (total > 0) {
523
+ if (active > 0) {
524
+ const tokStr = serverData.data.server.avgGenerateSpeed
525
+ ? ` · ${Math.round(serverData.data.server.avgGenerateSpeed)} tok/s`
526
+ : '';
527
+ content += `{green-fg}● Slots: ${active}/${total} processing${tokStr}{/green-fg}\n`;
528
+ }
529
+ else {
530
+ content += `{gray-fg}○ Slots: ${total} idle{/gray-fg}\n`;
531
+ }
532
+ }
533
+ }
534
+ content += "\n";
535
+ const logPath = logType === 'stdout' ? server.httpLogPath : server.stderrPath;
536
+ // Check if log exists
537
+ if (!(await (0, file_utils_js_1.fileExists)(logPath))) {
538
+ content += `{yellow-fg}No ${logTypeLabel.toLowerCase()} logs found{/yellow-fg}\n`;
539
+ if (logType === 'stdout') {
540
+ content += `The server may not have processed any requests yet.\n`;
541
+ }
542
+ // Show notice if verbose logging is disabled for System logs
543
+ if (logType === 'stderr' && !server.verbose) {
544
+ content += `{yellow-fg}⚠ Verbose logging is disabled{/yellow-fg}\n`;
545
+ }
546
+ content += `Log file: ${logPath}\n\n`;
547
+ content += divider + "\n";
548
+ content += "{gray-fg}[T]oggle activity/system [R]efresh [ESC] Back{/gray-fg}";
549
+ return content;
550
+ }
551
+ // Show log size
552
+ const size = await (0, log_utils_js_1.getFileSize)(logPath);
553
+ content += `File: ${logPath}\n`;
554
+ content += `Size: ${(0, log_utils_js_1.formatFileSize)(size)}\n`;
555
+ // Show notice if verbose logging is disabled for System logs
556
+ if (logType === 'stderr' && !server.verbose) {
557
+ content += `{yellow-fg}⚠ Verbose logging is disabled{/yellow-fg}\n`;
558
+ }
559
+ content += `\n`;
560
+ // Show logs
561
+ try {
562
+ const { execSync } = require("child_process");
563
+ content += divider + "\n";
564
+ if (logType === 'stdout') {
565
+ // Activity logs: Read last 500 lines of HTTP log file (pre-parsed compact format)
566
+ // Using tail instead of cat avoids the 1MB default execSync buffer limit on large files
567
+ const output = execSync(`tail -n 500 "${logPath}"`, { encoding: "utf-8" });
568
+ const lines = output.split("\n").filter((l) => l.trim());
569
+ if (lines.length === 0) {
570
+ content += "{yellow-fg}No HTTP requests logged yet{/yellow-fg}\n";
571
+ content +=
572
+ "{gray-fg}Send requests to the server to see them here{/gray-fg}\n";
573
+ }
574
+ else {
575
+ // Filter out health check requests
576
+ const parser = new log_parser_js_1.LogParser();
577
+ const filteredLines = lines.filter((line) => !parser.isHealthCheckRequest(line));
578
+ if (filteredLines.length === 0) {
579
+ content += "{gray-fg}No requests logged yet{/gray-fg}\n";
580
+ }
581
+ else {
582
+ // Show last 30 lines newest-first, truncate to fit terminal width
583
+ const limitedLines = filteredLines.slice(-30).reverse();
584
+ const maxWidth = termWidth - 4;
585
+ for (const line of limitedLines) {
586
+ if (line.length > maxWidth) {
587
+ content += line.substring(0, maxWidth - 3) + "...\n";
588
+ }
589
+ else {
590
+ content += line + "\n";
591
+ }
592
+ }
593
+ }
594
+ }
595
+ }
596
+ else {
597
+ // System logs: Show last 30 lines of stderr, newest-first
598
+ const output = execSync(`tail -n 30 "${logPath}"`, { encoding: "utf-8" });
599
+ const lines = output.split("\n").filter((l) => l).reverse();
600
+ const maxWidth = termWidth - 4;
601
+ for (const line of lines) {
602
+ // Remove ANSI color codes to calculate visible length
603
+ const visibleLine = line.replace(/\x1b\[[0-9;]*m/g, '');
604
+ if (visibleLine.length > maxWidth) {
605
+ // Truncate and add ellipsis
606
+ let visibleLen = 0;
607
+ let truncated = '';
608
+ for (let i = 0; i < line.length && visibleLen < maxWidth - 3; i++) {
609
+ truncated += line[i];
610
+ // Only count visible characters
611
+ if (line[i] !== '\x1b' && !line.slice(i).match(/^\x1b\[[0-9;]*m/)) {
612
+ visibleLen++;
613
+ }
614
+ else if (line.slice(i).match(/^\x1b\[[0-9;]*m/)) {
615
+ // Skip the rest of the ANSI code
616
+ const match = line.slice(i).match(/^\x1b\[[0-9;]*m/);
617
+ if (match) {
618
+ truncated += match[0].slice(1);
619
+ i += match[0].length - 1;
620
+ }
621
+ }
622
+ }
623
+ content += truncated + '...\n';
624
+ }
625
+ else {
626
+ content += line + '\n';
627
+ }
628
+ }
629
+ }
630
+ content += divider + "\n";
631
+ }
632
+ catch (err) {
633
+ content += "{red-fg}Failed to read logs{/red-fg}\n";
634
+ }
635
+ const toggleRefreshText = logsRefreshInterval
636
+ ? "[F] Pause auto-refresh"
637
+ : "[F] Resume auto-refresh";
638
+ content += `{gray-fg}[T]oggle activity/system [R]efresh ${toggleRefreshText} [ESC] Back{/gray-fg}`;
639
+ // Update last updated time
640
+ logsLastUpdated = new Date();
468
641
  return content;
469
642
  }
643
+ // Start logs auto-refresh
644
+ function startLogsAutoRefresh() {
645
+ // Clear any existing interval
646
+ if (logsRefreshInterval) {
647
+ clearInterval(logsRefreshInterval);
648
+ }
649
+ // Set up auto-refresh every 3 seconds
650
+ logsRefreshInterval = setInterval(() => {
651
+ if (viewMode === "detail" && detailSubView === "logs") {
652
+ render();
653
+ }
654
+ }, 3000);
655
+ }
656
+ // Stop logs auto-refresh
657
+ function stopLogsAutoRefresh() {
658
+ if (logsRefreshInterval) {
659
+ clearInterval(logsRefreshInterval);
660
+ logsRefreshInterval = null;
661
+ }
662
+ }
663
+ // Render current view
664
+ async function render() {
665
+ // No more handler registration in render() - KeyboardManager handles this!
666
+ let content = "";
667
+ if (viewMode === "list") {
668
+ stopLogsAutoRefresh();
669
+ content = renderListView(lastSystemMetrics);
670
+ }
671
+ else if (viewMode === "detail") {
672
+ if (detailSubView === "status") {
673
+ stopLogsAutoRefresh();
674
+ content = renderDetailView(lastSystemMetrics);
675
+ }
676
+ else if (detailSubView === "logs") {
677
+ content = await renderServerLogs();
678
+ if (!logsRefreshInterval) {
679
+ startLogsAutoRefresh();
680
+ }
681
+ }
682
+ }
683
+ contentBox.setContent(content);
684
+ screen.render();
685
+ }
470
686
  // Fetch and update display
471
687
  async function fetchData() {
472
688
  try {
689
+ // Reload server configs from disk to pick up changes made via web UI or CLI
690
+ try {
691
+ const freshServers = await state_manager_js_1.stateManager.getAllServers();
692
+ // Create aggregators/historyManagers for any newly added servers
693
+ for (const server of freshServers) {
694
+ if (!aggregators.has(server.id)) {
695
+ aggregators.set(server.id, new metrics_aggregator_js_1.MetricsAggregator(server));
696
+ historyManagers.set(server.id, new history_manager_js_1.HistoryManager(server.id));
697
+ }
698
+ }
699
+ // Clean up removed servers
700
+ for (const id of aggregators.keys()) {
701
+ if (!freshServers.find((s) => s.id === id)) {
702
+ aggregators.delete(id);
703
+ historyManagers.delete(id);
704
+ serverDataMap.delete(id);
705
+ }
706
+ }
707
+ // Clamp selected index if server list shrank
708
+ if (selectedServerIndex >= freshServers.length) {
709
+ selectedServerIndex = Math.max(0, freshServers.length - 1);
710
+ selectedRowIndex = selectedServerIndex;
711
+ }
712
+ servers = freshServers;
713
+ }
714
+ catch {
715
+ // Keep using existing servers if the reload fails
716
+ }
717
+ // Skip fetching metrics if we're in logs view (don't need server data)
718
+ if (viewMode === "detail" && detailSubView === "logs") {
719
+ await render();
720
+ return;
721
+ }
473
722
  // Collect system metrics ONCE for all servers (not per-server)
474
723
  // This prevents spawning multiple macmon processes
475
724
  const systemMetricsPromise = systemCollector.collectSystemMetrics();
476
725
  // Batch collect process memory and CPU for ALL servers in parallel
477
726
  // This prevents spawning multiple top processes (5x speedup)
478
- const { getBatchProcessMemory, getBatchProcessCpu } = await Promise.resolve().then(() => __importStar(require('../utils/process-utils.js')));
479
- const pids = servers.filter(s => s.pid).map(s => s.pid);
727
+ const { getBatchProcessMemory, getBatchProcessCpu } = await Promise.resolve().then(() => __importStar(require("../utils/process-utils.js")));
728
+ const pids = servers.filter((s) => s.pid).map((s) => s.pid);
480
729
  const memoryMapPromise = pids.length > 0
481
730
  ? getBatchProcessMemory(pids)
482
731
  : Promise.resolve(new Map());
@@ -484,17 +733,20 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
484
733
  ? getBatchProcessCpu(pids)
485
734
  : Promise.resolve(new Map());
486
735
  // Wait for both batches to complete
487
- const [memoryMap, cpuMap] = await Promise.all([memoryMapPromise, cpuMapPromise]);
736
+ const [memoryMap, cpuMap] = await Promise.all([
737
+ memoryMapPromise,
738
+ cpuMapPromise,
739
+ ]);
488
740
  // Collect server metrics only for RUNNING servers (skip stopped servers)
489
741
  const promises = servers
490
- .filter(server => server.status === 'running')
742
+ .filter((server) => server.status === "running")
491
743
  .map(async (server) => {
492
744
  const aggregator = aggregators.get(server.id);
493
745
  try {
494
746
  // Use collectServerMetrics instead of collectMonitorData
495
747
  // to avoid spawning macmon per server
496
748
  // Pass pre-fetched memory and CPU to avoid spawning top per server
497
- const serverMetrics = await aggregator.collectServerMetrics(server, server.pid ? memoryMap.get(server.pid) ?? null : null, server.pid ? cpuMap.get(server.pid) ?? null : null);
749
+ const serverMetrics = await aggregator.collectServerMetrics(server, server.pid ? (memoryMap.get(server.pid) ?? null) : null, server.pid ? (cpuMap.get(server.pid) ?? null) : null);
498
750
  // Build MonitorData manually with shared system metrics
499
751
  const data = {
500
752
  server: serverMetrics,
@@ -513,14 +765,14 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
513
765
  serverDataMap.set(server.id, {
514
766
  server,
515
767
  data: null,
516
- error: err instanceof Error ? err.message : 'Unknown error',
768
+ error: err instanceof Error ? err.message : "Unknown error",
517
769
  });
518
770
  }
519
771
  });
520
772
  // Set null data for stopped servers (no metrics collection)
521
773
  servers
522
- .filter(server => server.status !== 'running')
523
- .forEach(server => {
774
+ .filter((server) => server.status !== "running")
775
+ .forEach((server) => {
524
776
  serverDataMap.set(server.id, {
525
777
  server,
526
778
  data: null,
@@ -541,38 +793,33 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
541
793
  // Append to history for each server (silent failure)
542
794
  // Only save history for servers that are healthy and not stale
543
795
  for (const [serverId, serverData] of serverDataMap) {
544
- if (serverData.data && !serverData.data.server.stale && serverData.data.server.healthy) {
796
+ if (serverData.data &&
797
+ !serverData.data.server.stale &&
798
+ serverData.data.server.healthy) {
545
799
  const manager = historyManagers.get(serverId);
546
- manager?.appendSnapshot(serverData.data.server, serverData.data.system)
547
- .catch(err => {
800
+ manager
801
+ ?.appendSnapshot(serverData.data.server, serverData.data.system)
802
+ .catch((err) => {
548
803
  // Don't interrupt monitoring on history write failure
549
804
  console.error(`Failed to save history for ${serverId}:`, err);
550
805
  });
551
806
  }
552
807
  }
553
- // Render once with complete data
554
- let content = '';
555
- if (viewMode === 'list') {
556
- content = renderListView(systemMetrics);
557
- }
558
- else {
559
- content = renderDetailView(systemMetrics);
560
- }
561
- contentBox.setContent(content);
562
808
  // Call onFirstRender callback before first render (to clean up splash screen)
563
809
  if (!hasCalledFirstRender && onFirstRender) {
564
810
  hasCalledFirstRender = true;
565
811
  onFirstRender();
566
812
  }
567
- screen.render();
813
+ // Render once with complete data
814
+ await render();
568
815
  // Clear loading state
569
816
  hideLoading();
570
817
  }
571
818
  catch (err) {
572
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
573
- contentBox.setContent('{bold}{red-fg}Error{/red-fg}{/bold}\n\n' +
819
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
820
+ contentBox.setContent("{bold}{red-fg}Error{/red-fg}{/bold}\n\n" +
574
821
  `{red-fg}${errorMsg}{/red-fg}\n\n` +
575
- '{gray-fg}Press [R] to retry or [Q] to quit{/gray-fg}');
822
+ "{gray-fg}Press [R] to retry or [Q] to quit{/gray-fg}");
576
823
  screen.render();
577
824
  // Clear loading state on error too
578
825
  isLoading = false;
@@ -592,7 +839,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
592
839
  if (!match)
593
840
  return null;
594
841
  const num = parseFloat(match[1]);
595
- const hasK = match[2] === 'k';
842
+ const hasK = match[2] === "k";
596
843
  if (isNaN(num) || num <= 0)
597
844
  return null;
598
845
  return hasK ? Math.round(num * 1024) : Math.round(num);
@@ -604,18 +851,18 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
604
851
  }
605
852
  return value.toLocaleString();
606
853
  }
607
- // Helper to create modal boxes
608
- function createModal(title, height = 'shrink', borderColor = 'cyan') {
854
+ // Helper to create modal boxes (matches ModalController styling)
855
+ function createModal(title, height = "shrink", borderColor = "cyan") {
609
856
  return blessed_1.default.box({
610
857
  parent: screen,
611
- top: 'center',
612
- left: 'center',
613
- width: '70%',
858
+ top: "center",
859
+ left: "center",
860
+ width: "70%",
614
861
  height,
615
- border: { type: 'line' },
862
+ border: { type: "line" },
616
863
  style: {
617
864
  border: { fg: borderColor },
618
- fg: 'white',
865
+ fg: "white", // Matches ModalController
619
866
  },
620
867
  tags: true,
621
868
  label: ` ${title} `,
@@ -623,23 +870,11 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
623
870
  }
624
871
  // Show progress modal
625
872
  function showProgressModal(message) {
626
- const modal = createModal('Working', 6);
627
- modal.setContent(`\n {cyan-fg}${message}{/cyan-fg}`);
628
- screen.render();
629
- return modal;
873
+ return modalController.showProgress(message);
630
874
  }
631
875
  // Show error modal
632
876
  async function showErrorModal(message) {
633
- return new Promise((resolve) => {
634
- const modal = createModal('Error', 8, 'red');
635
- modal.setContent(`\n {red-fg}❌ ${message}{/red-fg}\n\n {gray-fg}[Enter] Close{/gray-fg}`);
636
- screen.render();
637
- modal.focus();
638
- modal.key(['enter', 'escape'], () => {
639
- screen.remove(modal);
640
- resolve();
641
- });
642
- });
877
+ await modalController.showError(message, () => { });
643
878
  }
644
879
  // Remove server dialog
645
880
  async function showRemoveServerDialog(server) {
@@ -648,26 +883,30 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
648
883
  clearInterval(intervalId);
649
884
  if (spinnerIntervalId)
650
885
  clearInterval(spinnerIntervalId);
651
- unregisterHandlers();
886
+ // Push empty blocking context to prevent main handlers from firing
887
+ // (This modal uses blessed's modal.key() directly, not KeyboardManager)
888
+ keyboardManager.pushContext('remove-server-dialog', {}, true);
652
889
  // Check if other servers use the same model
653
890
  const allServers = await state_manager_js_1.stateManager.getAllServers();
654
- const otherServersWithSameModel = allServers.filter(s => s.id !== server.id && s.modelPath === server.modelPath);
891
+ const otherServersWithSameModel = allServers.filter((s) => s.id !== server.id && s.modelPath === server.modelPath);
655
892
  let deleteModelOption = false;
656
893
  const showDeleteModelOption = otherServersWithSameModel.length === 0;
657
894
  // 0 = checkbox (delete model), 1 = confirm button
658
895
  let selectedOption = showDeleteModelOption ? 0 : 1;
659
- const modal = createModal('Remove Server', showDeleteModelOption ? 18 : 14, 'red');
896
+ const overlay = (0, overlay_utils_js_1.createOverlay)(screen);
897
+ screen.append(overlay);
898
+ const modal = createModal("Remove Server", showDeleteModelOption ? 18 : 14, "red");
660
899
  function renderDialog() {
661
- let content = '\n';
900
+ let content = "\n";
662
901
  content += ` {bold}Remove server: ${server.id}{/bold}\n\n`;
663
902
  content += ` Model: ${server.modelName}\n`;
664
903
  content += ` Port: ${server.port}\n`;
665
- content += ` Status: ${server.status === 'running' ? '{green-fg}running{/green-fg}' : '{gray-fg}stopped{/gray-fg}'}\n\n`;
666
- if (server.status === 'running') {
904
+ content += ` Status: ${server.status === "running" ? "{green-fg}running{/green-fg}" : "{gray-fg}stopped{/gray-fg}"}\n\n`;
905
+ if (server.status === "running") {
667
906
  content += ` {yellow-fg}⚠ Server will be stopped{/yellow-fg}\n\n`;
668
907
  }
669
908
  if (showDeleteModelOption) {
670
- const checkbox = deleteModelOption ? '' : '';
909
+ const checkbox = deleteModelOption ? "" : "";
671
910
  const isCheckboxSelected = selectedOption === 0;
672
911
  if (isCheckboxSelected) {
673
912
  content += ` {cyan-bg}{15-fg}${checkbox} Also delete model file{/15-fg}{/cyan-bg}\n`;
@@ -694,71 +933,54 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
694
933
  renderDialog();
695
934
  modal.focus();
696
935
  return new Promise((resolve) => {
697
- modal.key(['up', 'k'], () => {
936
+ modal.key(["up", "k"], () => {
698
937
  if (showDeleteModelOption && selectedOption === 1) {
699
938
  selectedOption = 0;
700
939
  renderDialog();
701
940
  }
702
941
  });
703
- modal.key(['down', 'j'], () => {
942
+ modal.key(["down", "j"], () => {
704
943
  if (showDeleteModelOption && selectedOption === 0) {
705
944
  selectedOption = 1;
706
945
  renderDialog();
707
946
  }
708
947
  });
709
- modal.key(['space'], () => {
948
+ modal.key(["space"], () => {
710
949
  if (showDeleteModelOption && selectedOption === 0) {
711
950
  deleteModelOption = !deleteModelOption;
712
951
  renderDialog();
713
952
  }
714
953
  });
715
- modal.key(['escape'], () => {
954
+ modal.key(["escape"], () => {
716
955
  screen.remove(modal);
717
- registerHandlers();
956
+ screen.remove(overlay);
957
+ keyboardManager.popContext(); // Pop the blocking context
718
958
  startPolling();
719
959
  resolve();
720
960
  });
721
- modal.key(['enter'], async () => {
961
+ modal.key(["enter"], async () => {
722
962
  screen.remove(modal);
963
+ screen.remove(overlay);
723
964
  // Show progress
724
- const progressModal = showProgressModal('Removing server...');
965
+ const progressModal = showProgressModal("Removing server...");
725
966
  try {
726
- // Stop and unload service if running
727
- if (server.status === 'running') {
728
- progressModal.setContent('\n {cyan-fg}Stopping server...{/cyan-fg}');
729
- screen.render();
730
- try {
731
- await launchctl_manager_js_1.launchctlManager.unloadService(server.plistPath);
732
- await launchctl_manager_js_1.launchctlManager.waitForServiceStop(server.label, 5000);
733
- }
734
- catch (err) {
735
- // Continue even if unload fails
736
- }
737
- }
738
- else {
739
- // Still try to unload in case it's in a weird state
740
- try {
741
- await launchctl_manager_js_1.launchctlManager.unloadService(server.plistPath);
742
- }
743
- catch (err) {
744
- // Ignore
745
- }
746
- }
747
- // Delete plist
748
- progressModal.setContent('\n {cyan-fg}Removing configuration...{/cyan-fg}');
749
- screen.render();
750
- await launchctl_manager_js_1.launchctlManager.deletePlist(server.plistPath);
751
- // Delete server config
752
- await state_manager_js_1.stateManager.deleteServerConfig(server.id);
967
+ const result = await server_lifecycle_service_js_1.serverLifecycleService.deleteServer(server.id, {
968
+ onProgress: (message) => {
969
+ progressModal.setContent(`\n {cyan-fg}${message}{/cyan-fg}`);
970
+ screen.render();
971
+ },
972
+ });
973
+ if (!result.success)
974
+ throw new Error(result.error || 'Failed to delete server');
753
975
  // Delete model if requested
754
976
  if (deleteModelOption && showDeleteModelOption) {
755
- progressModal.setContent('\n {cyan-fg}Deleting model file...{/cyan-fg}');
977
+ progressModal.setContent("\n {cyan-fg}Deleting model file...{/cyan-fg}");
756
978
  screen.render();
757
979
  await fs.unlink(server.modelPath);
758
980
  }
759
- screen.remove(progressModal);
981
+ modalController.closeProgress(progressModal);
760
982
  // Remove server from our arrays
761
- const idx = servers.findIndex(s => s.id === server.id);
983
+ const idx = servers.findIndex((s) => s.id === server.id);
762
984
  if (idx !== -1) {
763
985
  servers.splice(idx, 1);
764
986
  aggregators.delete(server.id);
@@ -766,17 +988,18 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
766
988
  serverDataMap.delete(server.id);
767
989
  }
768
990
  // Go back to list view
769
- viewMode = 'list';
991
+ viewMode = "list";
770
992
  selectedRowIndex = Math.min(selectedRowIndex, Math.max(0, servers.length - 1));
771
993
  selectedServerIndex = selectedRowIndex;
772
- registerHandlers();
994
+ keyboardManager.popContext(); // Pop the blocking context
995
+ updateKeyboardContext(); // Update for list view
773
996
  startPolling();
774
997
  resolve();
775
998
  }
776
999
  catch (err) {
777
- screen.remove(progressModal);
778
- await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
779
- registerHandlers();
1000
+ modalController.closeProgress(progressModal);
1001
+ await showErrorModal(err instanceof Error ? err.message : "Unknown error");
1002
+ keyboardManager.popContext(); // Pop the blocking context
780
1003
  startPolling();
781
1004
  resolve();
782
1005
  }
@@ -790,26 +1013,29 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
790
1013
  clearInterval(intervalId);
791
1014
  if (spinnerIntervalId)
792
1015
  clearInterval(spinnerIntervalId);
793
- unregisterHandlers();
1016
+ // Push empty blocking context for create flow
1017
+ keyboardManager.pushContext('create-server-flow', {}, true);
794
1018
  // Step 1: Model selection
795
1019
  const models = await model_scanner_js_1.modelScanner.scanModels();
796
1020
  if (models.length === 0) {
797
- await showErrorModal('No models found in ~/models directory.\nUse [M]odels → [S]earch to download models.');
1021
+ await showErrorModal("No models found in ~/models directory.\nUse [M]odels → [S]earch to download models.");
798
1022
  // Immediately render with cached data for instant feedback
799
1023
  const content = renderListView(lastSystemMetrics);
800
1024
  contentBox.setContent(content);
801
1025
  screen.render();
802
- registerHandlers();
1026
+ keyboardManager.popContext(); // Pop the blocking context
803
1027
  startPolling();
804
1028
  return;
805
1029
  }
806
1030
  // Check which models already have servers
807
1031
  const allServers = await state_manager_js_1.stateManager.getAllServers();
808
- const modelsWithServers = new Set(allServers.map(s => s.modelPath));
1032
+ const modelsWithServers = new Set(allServers.map((s) => s.modelPath));
809
1033
  let selectedModelIndex = 0;
810
1034
  let scrollOffset = 0;
811
1035
  const maxVisible = 8;
812
- const modelModal = createModal('Create Server - Select Model', maxVisible + 8);
1036
+ const modelOverlay = (0, overlay_utils_js_1.createOverlay)(screen);
1037
+ screen.append(modelOverlay);
1038
+ const modelModal = createModal("Create Server - Select Model", maxVisible + 8);
813
1039
  function renderModelPicker() {
814
1040
  // Adjust scroll offset
815
1041
  if (selectedModelIndex < scrollOffset) {
@@ -818,25 +1044,27 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
818
1044
  else if (selectedModelIndex >= scrollOffset + maxVisible) {
819
1045
  scrollOffset = selectedModelIndex - maxVisible + 1;
820
1046
  }
821
- let content = '\n';
822
- content += ' {bold}Select a model to create a server for:{/bold}\n\n';
1047
+ let content = "\n";
1048
+ content += " {bold}Select a model to create a server for:{/bold}\n\n";
823
1049
  const visibleModels = models.slice(scrollOffset, scrollOffset + maxVisible);
824
1050
  for (let i = 0; i < visibleModels.length; i++) {
825
1051
  const model = visibleModels[i];
826
1052
  const actualIndex = scrollOffset + i;
827
1053
  const isSelected = actualIndex === selectedModelIndex;
828
1054
  const hasServer = modelsWithServers.has(model.path);
829
- const indicator = isSelected ? '' : ' ';
1055
+ const indicator = isSelected ? "" : " ";
830
1056
  // Truncate filename if too long
831
1057
  let displayName = model.filename;
832
1058
  const maxLen = 40;
833
1059
  if (displayName.length > maxLen) {
834
- displayName = displayName.substring(0, maxLen - 3) + '...';
1060
+ displayName = displayName.substring(0, maxLen - 3) + "...";
835
1061
  }
836
1062
  displayName = displayName.padEnd(maxLen);
837
1063
  const size = model.sizeFormatted.padStart(8);
838
- const serverIndicator = hasServer ? ' {yellow-fg}(has server){/yellow-fg}' : '';
839
- const serverIndicatorPlain = hasServer ? ' (has server)' : '';
1064
+ const serverIndicator = hasServer
1065
+ ? " {yellow-fg}(has server){/yellow-fg}"
1066
+ : "";
1067
+ const serverIndicatorPlain = hasServer ? " (has server)" : "";
840
1068
  if (isSelected) {
841
1069
  content += ` {cyan-bg}{15-fg}${indicator} ${displayName} ${size}${serverIndicatorPlain}{/15-fg}{/cyan-bg}\n`;
842
1070
  }
@@ -849,27 +1077,30 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
849
1077
  const scrollInfo = `${selectedModelIndex + 1}/${models.length}`;
850
1078
  content += `\n {gray-fg}${scrollInfo}{/gray-fg}`;
851
1079
  }
852
- content += '\n\n {gray-fg}[↑/↓] Navigate [Enter] Select [ESC] Cancel{/gray-fg}';
1080
+ content +=
1081
+ "\n\n {gray-fg}[↑/↓] Navigate [Enter] Select [ESC] Cancel{/gray-fg}";
853
1082
  modelModal.setContent(content);
854
1083
  screen.render();
855
1084
  }
856
1085
  renderModelPicker();
857
1086
  modelModal.focus();
858
1087
  const selectedModel = await new Promise((resolve) => {
859
- modelModal.key(['up', 'k'], () => {
1088
+ modelModal.key(["up", "k"], () => {
860
1089
  selectedModelIndex = Math.max(0, selectedModelIndex - 1);
861
1090
  renderModelPicker();
862
1091
  });
863
- modelModal.key(['down', 'j'], () => {
1092
+ modelModal.key(["down", "j"], () => {
864
1093
  selectedModelIndex = Math.min(models.length - 1, selectedModelIndex + 1);
865
1094
  renderModelPicker();
866
1095
  });
867
- modelModal.key(['escape'], () => {
1096
+ modelModal.key(["escape"], () => {
868
1097
  screen.remove(modelModal);
1098
+ screen.remove(modelOverlay);
869
1099
  resolve(null);
870
1100
  });
871
- modelModal.key(['enter'], () => {
1101
+ modelModal.key(["enter"], () => {
872
1102
  screen.remove(modelModal);
1103
+ screen.remove(modelOverlay);
873
1104
  resolve(models[selectedModelIndex]);
874
1105
  });
875
1106
  });
@@ -878,45 +1109,36 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
878
1109
  const content = renderListView(lastSystemMetrics);
879
1110
  contentBox.setContent(content);
880
1111
  screen.render();
881
- registerHandlers();
1112
+ keyboardManager.popContext(); // Pop the blocking context
882
1113
  startPolling();
883
1114
  return;
884
1115
  }
885
1116
  // Create a non-null reference for closures
886
1117
  const model = selectedModel;
887
- // Check if server already exists for this model
888
- const existingServer = allServers.find(s => s.modelPath === model.path);
889
- if (existingServer) {
890
- await showErrorModal(`Server already exists for this model.\nServer ID: ${existingServer.id}\nPort: ${existingServer.port}`);
891
- // Immediately render with cached data for instant feedback
892
- const content = renderListView(lastSystemMetrics);
893
- contentBox.setContent(content);
894
- screen.render();
895
- registerHandlers();
896
- startPolling();
897
- return;
898
- }
899
1118
  // Generate smart defaults
900
1119
  const defaultPort = await port_manager_js_1.portManager.findAvailablePort();
901
1120
  const modelSize = model.size;
902
1121
  // Smart context size based on model size
903
1122
  let defaultCtxSize = 4096;
904
- if (modelSize < 1024 * 1024 * 1024) { // < 1GB
1123
+ if (modelSize < 1024 * 1024 * 1024) {
1124
+ // < 1GB
905
1125
  defaultCtxSize = 2048;
906
1126
  }
907
- else if (modelSize < 3 * 1024 * 1024 * 1024) { // < 3GB
1127
+ else if (modelSize < 3 * 1024 * 1024 * 1024) {
1128
+ // < 3GB
908
1129
  defaultCtxSize = 4096;
909
1130
  }
910
- else if (modelSize < 6 * 1024 * 1024 * 1024) { // < 6GB
1131
+ else if (modelSize < 6 * 1024 * 1024 * 1024) {
1132
+ // < 6GB
911
1133
  defaultCtxSize = 8192;
912
1134
  }
913
1135
  else {
914
1136
  defaultCtxSize = 16384;
915
1137
  }
916
- const os = await Promise.resolve().then(() => __importStar(require('os')));
1138
+ const os = await Promise.resolve().then(() => __importStar(require("os")));
917
1139
  const defaultThreads = Math.max(1, Math.floor(os.cpus().length / 2));
918
1140
  const config = {
919
- host: '127.0.0.1',
1141
+ host: "127.0.0.1",
920
1142
  port: defaultPort,
921
1143
  threads: defaultThreads,
922
1144
  ctxSize: defaultCtxSize,
@@ -925,32 +1147,39 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
925
1147
  };
926
1148
  // Configuration fields
927
1149
  const fields = [
928
- { key: 'host', label: 'Host', type: 'select', options: ['127.0.0.1', '0.0.0.0'] },
929
- { key: 'port', label: 'Port', type: 'number' },
930
- { key: 'threads', label: 'Threads', type: 'number' },
931
- { key: 'ctxSize', label: 'Context Size', type: 'number' },
932
- { key: 'gpuLayers', label: 'GPU Layers', type: 'number' },
933
- { key: 'verbose', label: 'Verbose Logs', type: 'toggle' },
1150
+ {
1151
+ key: "host",
1152
+ label: "Host",
1153
+ type: "select",
1154
+ options: ["127.0.0.1", "0.0.0.0"],
1155
+ },
1156
+ { key: "port", label: "Port", type: "number" },
1157
+ { key: "threads", label: "Threads", type: "number" },
1158
+ { key: "ctxSize", label: "Context Size", type: "number" },
1159
+ { key: "gpuLayers", label: "GPU Layers", type: "number" },
1160
+ { key: "verbose", label: "Verbose Logs", type: "toggle" },
934
1161
  ];
935
1162
  let selectedFieldIndex = 0;
936
- const configModal = createModal('Create Server - Configuration', 20);
1163
+ const configOverlay = (0, overlay_utils_js_1.createOverlay)(screen);
1164
+ screen.append(configOverlay);
1165
+ const configModal = createModal("Create Server - Configuration", 20);
937
1166
  function formatConfigValue(key, value) {
938
- if (key === 'verbose')
939
- return value ? 'Enabled' : 'Disabled';
940
- if (key === 'ctxSize')
1167
+ if (key === "verbose")
1168
+ return value ? "Enabled" : "Disabled";
1169
+ if (key === "ctxSize")
941
1170
  return formatContextSize(value);
942
1171
  return String(value);
943
1172
  }
944
1173
  function renderConfigScreen() {
945
- let content = '\n';
1174
+ let content = "\n";
946
1175
  content += ` {bold}Model:{/bold} ${model.filename}\n`;
947
1176
  content += ` {bold}Size:{/bold} ${model.sizeFormatted}\n\n`;
948
- content += ' {bold}Server Configuration:{/bold}\n';
949
- content += ''.repeat(30) + '\n';
1177
+ content += " {bold}Server Configuration:{/bold}\n";
1178
+ content += "".repeat(30) + "\n";
950
1179
  for (let i = 0; i < fields.length; i++) {
951
1180
  const field = fields[i];
952
1181
  const isSelected = i === selectedFieldIndex;
953
- const indicator = isSelected ? '' : ' ';
1182
+ const indicator = isSelected ? "" : " ";
954
1183
  const label = field.label.padEnd(14);
955
1184
  const value = formatConfigValue(field.key, config[field.key]);
956
1185
  if (isSelected) {
@@ -960,7 +1189,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
960
1189
  content += ` ${indicator} ${label}{cyan-fg}${value}{/cyan-fg}\n`;
961
1190
  }
962
1191
  }
963
- content += '\n';
1192
+ content += "\n";
964
1193
  const createSelected = selectedFieldIndex === fields.length;
965
1194
  if (createSelected) {
966
1195
  content += ` {green-bg}{15-fg}[ Create Server ]{/15-fg}{/green-bg}\n`;
@@ -968,46 +1197,51 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
968
1197
  else {
969
1198
  content += ` {green-fg}[ Create Server ]{/green-fg}\n`;
970
1199
  }
971
- content += '\n {gray-fg}[↑/↓] Navigate [Enter] Edit/Create [ESC] Cancel{/gray-fg}';
1200
+ content +=
1201
+ "\n {gray-fg}[↑/↓] Navigate [Enter] Edit/Create [ESC] Cancel{/gray-fg}";
972
1202
  configModal.setContent(content);
973
1203
  screen.render();
974
1204
  }
975
1205
  renderConfigScreen();
976
1206
  configModal.focus();
977
1207
  const shouldCreate = await new Promise((resolve) => {
978
- configModal.key(['up', 'k'], () => {
1208
+ configModal.key(["up", "k"], () => {
979
1209
  selectedFieldIndex = Math.max(0, selectedFieldIndex - 1);
980
1210
  renderConfigScreen();
981
1211
  });
982
- configModal.key(['down', 'j'], () => {
1212
+ configModal.key(["down", "j"], () => {
983
1213
  selectedFieldIndex = Math.min(fields.length, selectedFieldIndex + 1);
984
1214
  renderConfigScreen();
985
1215
  });
986
- configModal.key(['escape'], () => {
1216
+ configModal.key(["escape"], () => {
987
1217
  screen.remove(configModal);
1218
+ screen.remove(configOverlay);
988
1219
  resolve(false);
989
1220
  });
990
- configModal.key(['enter'], async () => {
1221
+ configModal.key(["enter"], async () => {
991
1222
  if (selectedFieldIndex === fields.length) {
992
1223
  // Create button selected
993
1224
  screen.remove(configModal);
1225
+ screen.remove(configOverlay);
994
1226
  resolve(true);
995
1227
  }
996
1228
  else {
997
1229
  // Edit field
998
1230
  const field = fields[selectedFieldIndex];
999
- if (field.type === 'select') {
1231
+ if (field.type === "select") {
1000
1232
  // Show select dialog
1001
1233
  const options = field.options;
1002
1234
  let optionIndex = options.indexOf(config[field.key]);
1003
1235
  if (optionIndex < 0)
1004
1236
  optionIndex = 0;
1237
+ const selectOverlay = (0, overlay_utils_js_1.createOverlay)(screen);
1238
+ screen.append(selectOverlay);
1005
1239
  const selectModal = createModal(field.label, options.length + 6);
1006
1240
  function renderSelectOptions() {
1007
- let content = '\n';
1241
+ let content = "\n";
1008
1242
  for (let i = 0; i < options.length; i++) {
1009
1243
  const isOpt = i === optionIndex;
1010
- const ind = isOpt ? '' : '';
1244
+ const ind = isOpt ? "" : "";
1011
1245
  if (isOpt) {
1012
1246
  content += ` {cyan-fg}${ind} ${options[i]}{/cyan-fg}\n`;
1013
1247
  }
@@ -1015,44 +1249,50 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1015
1249
  content += ` {gray-fg}${ind} ${options[i]}{/gray-fg}\n`;
1016
1250
  }
1017
1251
  }
1018
- if (field.key === 'host' && options[optionIndex] === '0.0.0.0') {
1019
- content += '\n {yellow-fg}⚠ Warning: Exposes server to network{/yellow-fg}';
1252
+ if (field.key === "host" && options[optionIndex] === "0.0.0.0") {
1253
+ content +=
1254
+ "\n {yellow-fg}⚠ Warning: Exposes server to network{/yellow-fg}";
1020
1255
  }
1021
- content += '\n\n {gray-fg}[↑/↓] Select [Enter] Confirm{/gray-fg}';
1256
+ content +=
1257
+ "\n\n {gray-fg}[↑/↓] Select [Enter] Confirm{/gray-fg}";
1022
1258
  selectModal.setContent(content);
1023
1259
  screen.render();
1024
1260
  }
1025
1261
  renderSelectOptions();
1026
1262
  selectModal.focus();
1027
1263
  await new Promise((resolveSelect) => {
1028
- selectModal.key(['up', 'k'], () => {
1264
+ selectModal.key(["up", "k"], () => {
1029
1265
  optionIndex = Math.max(0, optionIndex - 1);
1030
1266
  renderSelectOptions();
1031
1267
  });
1032
- selectModal.key(['down', 'j'], () => {
1268
+ selectModal.key(["down", "j"], () => {
1033
1269
  optionIndex = Math.min(options.length - 1, optionIndex + 1);
1034
1270
  renderSelectOptions();
1035
1271
  });
1036
- selectModal.key(['enter'], () => {
1272
+ selectModal.key(["enter"], () => {
1037
1273
  config[field.key] = options[optionIndex];
1038
1274
  screen.remove(selectModal);
1275
+ screen.remove(selectOverlay);
1039
1276
  resolveSelect();
1040
1277
  });
1041
- selectModal.key(['escape'], () => {
1278
+ selectModal.key(["escape"], () => {
1042
1279
  screen.remove(selectModal);
1280
+ screen.remove(selectOverlay);
1043
1281
  resolveSelect();
1044
1282
  });
1045
1283
  });
1046
1284
  renderConfigScreen();
1047
1285
  configModal.focus();
1048
1286
  }
1049
- else if (field.type === 'toggle') {
1287
+ else if (field.type === "toggle") {
1050
1288
  config[field.key] = !config[field.key];
1051
1289
  renderConfigScreen();
1052
1290
  }
1053
- else if (field.type === 'number') {
1291
+ else if (field.type === "number") {
1054
1292
  // Number input
1055
- const isCtxSize = field.key === 'ctxSize';
1293
+ const isCtxSize = field.key === "ctxSize";
1294
+ const inputOverlay = (0, overlay_utils_js_1.createOverlay)(screen);
1295
+ screen.append(inputOverlay);
1056
1296
  const inputModal = createModal(`Edit ${field.label}`, isCtxSize ? 11 : 10);
1057
1297
  const currentDisplay = isCtxSize
1058
1298
  ? formatContextSize(config[field.key])
@@ -1070,7 +1310,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1070
1310
  parent: inputModal,
1071
1311
  top: 2,
1072
1312
  left: 2,
1073
- content: '{gray-fg}Accepts: 4096, 4k, 8k, 16k, 32k, 64k, 128k{/gray-fg}',
1313
+ content: "{gray-fg}Accepts: 4096, 4k, 8k, 16k, 32k, 64k, 128k{/gray-fg}",
1074
1314
  tags: true,
1075
1315
  });
1076
1316
  }
@@ -1081,17 +1321,17 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1081
1321
  right: 2,
1082
1322
  height: 3,
1083
1323
  inputOnFocus: true,
1084
- border: { type: 'line' },
1324
+ border: { type: "line" },
1085
1325
  style: {
1086
- border: { fg: 'white' },
1087
- focus: { border: { fg: 'green' } },
1326
+ border: { fg: "white" },
1327
+ focus: { border: { fg: "green" } },
1088
1328
  },
1089
1329
  });
1090
1330
  blessed_1.default.text({
1091
1331
  parent: inputModal,
1092
1332
  bottom: 1,
1093
1333
  left: 2,
1094
- content: '{gray-fg}[Enter] Confirm [ESC] Cancel{/gray-fg}',
1334
+ content: "{gray-fg}[Enter] Confirm [ESC] Cancel{/gray-fg}",
1095
1335
  tags: true,
1096
1336
  });
1097
1337
  // Pre-fill with k notation for context size
@@ -1102,7 +1342,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1102
1342
  screen.render();
1103
1343
  inputBox.focus();
1104
1344
  await new Promise((resolveInput) => {
1105
- inputBox.on('submit', (value) => {
1345
+ inputBox.on("submit", (value) => {
1106
1346
  let numValue;
1107
1347
  if (isCtxSize) {
1108
1348
  numValue = parseContextSize(value);
@@ -1116,14 +1356,17 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1116
1356
  config[field.key] = numValue;
1117
1357
  }
1118
1358
  screen.remove(inputModal);
1359
+ screen.remove(inputOverlay);
1119
1360
  resolveInput();
1120
1361
  });
1121
- inputBox.on('cancel', () => {
1362
+ inputBox.on("cancel", () => {
1122
1363
  screen.remove(inputModal);
1364
+ screen.remove(inputOverlay);
1123
1365
  resolveInput();
1124
1366
  });
1125
- inputBox.key(['escape'], () => {
1367
+ inputBox.key(["escape"], () => {
1126
1368
  screen.remove(inputModal);
1369
+ screen.remove(inputOverlay);
1127
1370
  resolveInput();
1128
1371
  });
1129
1372
  });
@@ -1138,94 +1381,36 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1138
1381
  const content = renderListView(lastSystemMetrics);
1139
1382
  contentBox.setContent(content);
1140
1383
  screen.render();
1141
- registerHandlers();
1384
+ keyboardManager.popContext(); // Pop the blocking context
1142
1385
  startPolling();
1143
1386
  return;
1144
1387
  }
1145
1388
  // Step 3: Create the server
1146
- const progressModal = showProgressModal('Creating server...');
1389
+ const progressModal = showProgressModal("Creating server...");
1147
1390
  try {
1148
- // Generate full server config
1149
- const serverOptions = {
1391
+ // Create server using centralized service
1392
+ const result = await server_lifecycle_service_js_1.serverLifecycleService.createServer(model.baseModelName || model.filename, {
1150
1393
  port: config.port,
1151
1394
  host: config.host,
1152
1395
  threads: config.threads,
1153
1396
  ctxSize: config.ctxSize,
1154
1397
  gpuLayers: config.gpuLayers,
1155
1398
  verbose: config.verbose,
1156
- };
1157
- progressModal.setContent('\n {cyan-fg}Generating configuration...{/cyan-fg}');
1158
- screen.render();
1159
- const serverConfig = await config_generator_js_1.configGenerator.generateConfig(model.path, model.filename, model.size, config.port, serverOptions);
1160
- // Ensure log directory exists
1161
- await (0, file_utils_js_1.ensureDir)(path.dirname(serverConfig.stdoutPath));
1162
- // Create plist
1163
- progressModal.setContent('\n {cyan-fg}Creating launchctl service...{/cyan-fg}');
1164
- screen.render();
1165
- await launchctl_manager_js_1.launchctlManager.createPlist(serverConfig);
1166
- // Load service
1167
- try {
1168
- await launchctl_manager_js_1.launchctlManager.loadService(serverConfig.plistPath);
1169
- }
1170
- catch (error) {
1171
- await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
1172
- throw new Error(`Failed to load service: ${error.message}`);
1173
- }
1174
- // Start service
1175
- progressModal.setContent('\n {cyan-fg}Starting server...{/cyan-fg}');
1176
- screen.render();
1177
- try {
1178
- await launchctl_manager_js_1.launchctlManager.startService(serverConfig.label);
1179
- }
1180
- catch (error) {
1181
- await launchctl_manager_js_1.launchctlManager.unloadService(serverConfig.plistPath);
1182
- await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
1183
- throw new Error(`Failed to start service: ${error.message}`);
1184
- }
1185
- // Wait for startup
1186
- progressModal.setContent('\n {cyan-fg}Waiting for server to start...{/cyan-fg}');
1187
- screen.render();
1188
- const started = await launchctl_manager_js_1.launchctlManager.waitForServiceStart(serverConfig.label, 5000);
1189
- if (!started) {
1190
- await launchctl_manager_js_1.launchctlManager.unloadService(serverConfig.plistPath);
1191
- await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
1192
- throw new Error('Server failed to start. Check logs.');
1193
- }
1194
- // Wait for port to be ready (server may take a moment to bind)
1195
- progressModal.setContent('\n {cyan-fg}Waiting for server to be ready...{/cyan-fg}');
1196
- screen.render();
1197
- const portTimeout = 10000; // 10 seconds
1198
- const portStartTime = Date.now();
1199
- let portReady = false;
1200
- while (Date.now() - portStartTime < portTimeout) {
1201
- if (await (0, process_utils_js_1.isPortInUse)(serverConfig.port)) {
1202
- portReady = true;
1203
- break;
1204
- }
1205
- await new Promise(resolve => setTimeout(resolve, 500));
1206
- }
1207
- if (!portReady) {
1208
- await launchctl_manager_js_1.launchctlManager.unloadService(serverConfig.plistPath);
1209
- await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
1210
- throw new Error('Server started but port not responding. Check logs.');
1211
- }
1212
- // Update config with running status
1213
- let updatedConfig = await status_checker_js_1.statusChecker.updateServerStatus(serverConfig);
1214
- // Parse Metal memory allocation (wait a bit for model to load)
1215
- progressModal.setContent('\n {cyan-fg}Detecting GPU memory allocation...{/cyan-fg}');
1216
- screen.render();
1217
- await new Promise(resolve => setTimeout(resolve, 3000));
1218
- const metalMemoryMB = await (0, file_utils_js_1.parseMetalMemoryFromLog)(updatedConfig.stderrPath);
1219
- if (metalMemoryMB) {
1220
- updatedConfig = { ...updatedConfig, metalMemoryMB };
1399
+ metalDetectionDelayMs: 3000,
1400
+ onProgress: (message) => {
1401
+ progressModal.setContent(`\n {cyan-fg}${message}{/cyan-fg}`);
1402
+ screen.render();
1403
+ },
1404
+ });
1405
+ if (!result.success) {
1406
+ throw new Error(result.error);
1221
1407
  }
1222
- // Save server config
1223
- await state_manager_js_1.stateManager.saveServerConfig(updatedConfig);
1408
+ const updatedConfig = result.server;
1224
1409
  // Show success message briefly
1225
- progressModal.setContent('\n {green-fg}✓ Server created successfully!{/green-fg}');
1410
+ progressModal.setContent("\n {green-fg}✓ Server created successfully!{/green-fg}");
1226
1411
  screen.render();
1227
- await new Promise(resolve => setTimeout(resolve, 1000));
1228
- screen.remove(progressModal);
1412
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1413
+ modalController.closeProgress(progressModal);
1229
1414
  // Add to our arrays
1230
1415
  servers.push(updatedConfig);
1231
1416
  aggregators.set(updatedConfig.id, new metrics_aggregator_js_1.MetricsAggregator(updatedConfig));
@@ -1238,24 +1423,84 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1238
1423
  // Select the new server in list view
1239
1424
  selectedRowIndex = servers.length - 1;
1240
1425
  selectedServerIndex = selectedRowIndex;
1241
- registerHandlers();
1426
+ keyboardManager.popContext(); // Pop the blocking context
1242
1427
  startPolling();
1243
1428
  }
1244
1429
  catch (err) {
1245
- screen.remove(progressModal);
1246
- await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
1430
+ modalController.closeProgress(progressModal);
1431
+ await showErrorModal(err instanceof Error ? err.message : "Unknown error");
1247
1432
  // Immediately render with cached data for instant feedback
1248
1433
  const content = renderListView(lastSystemMetrics);
1249
1434
  contentBox.setContent(content);
1250
1435
  screen.render();
1251
- registerHandlers();
1436
+ keyboardManager.popContext(); // Pop the blocking context
1252
1437
  startPolling();
1253
1438
  }
1254
1439
  }
1255
1440
  // Store key handler references for cleanup when switching views
1441
+ /**
1442
+ * Helper function to update keyboard context based on current view state.
1443
+ * Call this whenever viewMode or detailSubView changes.
1444
+ */
1445
+ function updateKeyboardContext() {
1446
+ keyboardManager.updateCurrentContext(getHandlersForCurrentState());
1447
+ }
1448
+ /**
1449
+ * Get keyboard handlers for the current view state.
1450
+ * This function returns the appropriate handlers based on viewMode and detailSubView.
1451
+ */
1452
+ function getHandlersForCurrentState() {
1453
+ const handlers = {};
1454
+ // Always available keys
1455
+ handlers['escape'] = keyHandlers.escape;
1456
+ handlers['q'] = keyHandlers.quit;
1457
+ handlers['Q'] = keyHandlers.quit;
1458
+ handlers['C-c'] = keyHandlers.quit;
1459
+ if (viewMode === 'list') {
1460
+ // List view keys
1461
+ handlers['up'] = keyHandlers.up;
1462
+ handlers['k'] = keyHandlers.up;
1463
+ handlers['down'] = keyHandlers.down;
1464
+ handlers['j'] = keyHandlers.down;
1465
+ handlers['enter'] = keyHandlers.enter;
1466
+ handlers['m'] = keyHandlers.models;
1467
+ handlers['M'] = keyHandlers.models;
1468
+ handlers['r'] = keyHandlers.router;
1469
+ handlers['R'] = keyHandlers.router;
1470
+ handlers['h'] = keyHandlers.history;
1471
+ handlers['H'] = keyHandlers.history;
1472
+ handlers['n'] = keyHandlers.create;
1473
+ handlers['N'] = keyHandlers.create;
1474
+ }
1475
+ else if (viewMode === 'detail') {
1476
+ if (detailSubView === 'status') {
1477
+ // Detail status view keys
1478
+ handlers['h'] = keyHandlers.history;
1479
+ handlers['H'] = keyHandlers.history;
1480
+ handlers['c'] = keyHandlers.config;
1481
+ handlers['C'] = keyHandlers.config;
1482
+ handlers['r'] = keyHandlers.remove;
1483
+ handlers['R'] = keyHandlers.remove;
1484
+ handlers['s'] = keyHandlers.startStop;
1485
+ handlers['S'] = keyHandlers.startStop;
1486
+ handlers['l'] = keyHandlers.logs;
1487
+ handlers['L'] = keyHandlers.logs;
1488
+ }
1489
+ else if (detailSubView === 'logs') {
1490
+ // Logs view keys
1491
+ handlers['t'] = keyHandlers.toggleLogType;
1492
+ handlers['T'] = keyHandlers.toggleLogType;
1493
+ handlers['r'] = keyHandlers.refreshLogs;
1494
+ handlers['R'] = keyHandlers.refreshLogs;
1495
+ handlers['f'] = keyHandlers.toggleLogsRefresh;
1496
+ handlers['F'] = keyHandlers.toggleLogsRefresh;
1497
+ }
1498
+ }
1499
+ return handlers;
1500
+ }
1256
1501
  const keyHandlers = {
1257
1502
  up: () => {
1258
- if (viewMode === 'list') {
1503
+ if (viewMode === "list") {
1259
1504
  selectedRowIndex = Math.max(0, selectedRowIndex - 1);
1260
1505
  // Re-render immediately for responsive feel
1261
1506
  const content = renderListView(lastSystemMetrics);
@@ -1264,7 +1509,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1264
1509
  }
1265
1510
  },
1266
1511
  down: () => {
1267
- if (viewMode === 'list') {
1512
+ if (viewMode === "list") {
1268
1513
  selectedRowIndex = Math.min(servers.length - 1, selectedRowIndex + 1);
1269
1514
  // Re-render immediately for responsive feel
1270
1515
  const content = renderListView(lastSystemMetrics);
@@ -1273,43 +1518,83 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1273
1518
  }
1274
1519
  },
1275
1520
  enter: () => {
1276
- if (viewMode === 'list') {
1521
+ if (viewMode === "list") {
1277
1522
  showLoading();
1278
1523
  selectedServerIndex = selectedRowIndex;
1279
- viewMode = 'detail';
1524
+ viewMode = "detail";
1525
+ updateKeyboardContext(); // Update handlers for new view
1280
1526
  fetchData();
1281
1527
  }
1282
1528
  },
1283
1529
  escape: () => {
1284
- // Don't handle ESC if we're in historical view - let historical view handle it
1530
+ // Don't handle ESC if we're in historical view
1531
+ // Note: No need to check modalController.isModalOpen() - KeyboardManager handles this!
1285
1532
  if (inHistoricalView)
1286
1533
  return;
1287
- if (viewMode === 'detail') {
1534
+ if (viewMode === "detail" && detailSubView === "logs") {
1535
+ // Return from logs to detail status view
1536
+ detailSubView = "status";
1537
+ stopLogsAutoRefresh();
1538
+ updateKeyboardContext(); // Update handlers for new sub-view
1539
+ render();
1540
+ }
1541
+ else if (viewMode === "detail") {
1288
1542
  showLoading();
1289
- viewMode = 'list';
1543
+ viewMode = "list";
1544
+ detailSubView = "status"; // Reset to status when going back to list
1290
1545
  cameFromDirectJump = false; // Clear direct jump flag when returning to list
1546
+ updateKeyboardContext(); // Update handlers for new view
1291
1547
  fetchData();
1292
1548
  }
1293
- else if (viewMode === 'list') {
1294
- // ESC in list view - exit
1295
- showLoading();
1296
- if (intervalId)
1297
- clearInterval(intervalId);
1298
- if (spinnerIntervalId)
1299
- clearInterval(spinnerIntervalId);
1300
- setTimeout(() => {
1301
- screen.destroy();
1302
- process.exit(0);
1303
- }, 100);
1549
+ // ESC in list view does nothing - use 'q' or Ctrl-C to quit
1550
+ },
1551
+ logs: () => {
1552
+ // Only available from detail view and not in historical view
1553
+ if (viewMode !== "detail" || inHistoricalView)
1554
+ return;
1555
+ if (detailSubView === "status") {
1556
+ detailSubView = "logs";
1557
+ logType = 'stdout'; // Reset to activity logs when entering logs view
1558
+ updateKeyboardContext(); // Update handlers for new sub-view
1559
+ render();
1560
+ }
1561
+ },
1562
+ refreshLogs: () => {
1563
+ if (viewMode === "detail" && detailSubView === "logs") {
1564
+ render();
1565
+ }
1566
+ },
1567
+ toggleLogsRefresh: () => {
1568
+ if (viewMode === "detail" && detailSubView === "logs") {
1569
+ if (logsRefreshInterval) {
1570
+ stopLogsAutoRefresh();
1571
+ }
1572
+ else {
1573
+ startLogsAutoRefresh();
1574
+ }
1575
+ render();
1576
+ }
1577
+ },
1578
+ toggleLogType: () => {
1579
+ if (viewMode === "detail" && detailSubView === "logs") {
1580
+ logType = logType === 'stdout' ? 'stderr' : 'stdout';
1581
+ render();
1304
1582
  }
1305
1583
  },
1306
1584
  models: async () => {
1307
- if (onModels && viewMode === 'list' && !inHistoricalView) {
1585
+ if (onModels && viewMode === "list" && !inHistoricalView) {
1308
1586
  // Pause monitor (don't destroy - we'll resume when returning)
1309
1587
  controls.pause();
1310
1588
  await onModels(controls);
1311
1589
  }
1312
1590
  },
1591
+ router: async () => {
1592
+ if (onRouter && viewMode === "list" && !inHistoricalView) {
1593
+ // Pause monitor (don't destroy - we'll resume when returning)
1594
+ controls.pause();
1595
+ await onRouter(controls);
1596
+ }
1597
+ },
1313
1598
  history: async () => {
1314
1599
  // Prevent entering historical view if already there
1315
1600
  if (inHistoricalView)
@@ -1322,7 +1607,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1322
1607
  screen.remove(contentBox);
1323
1608
  // Mark that we're in historical view
1324
1609
  inHistoricalView = true;
1325
- if (viewMode === 'list') {
1610
+ if (viewMode === "list") {
1326
1611
  // Show multi-server historical view
1327
1612
  await (0, HistoricalMonitorApp_js_1.createMultiServerHistoricalUI)(screen, servers, selectedServerIndex, () => {
1328
1613
  // Mark that we've left historical view
@@ -1352,7 +1637,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1352
1637
  },
1353
1638
  config: async () => {
1354
1639
  // Only available from detail view and not in historical view
1355
- if (viewMode !== 'detail' || inHistoricalView)
1640
+ if (viewMode !== "detail" || inHistoricalView)
1356
1641
  return;
1357
1642
  // Pause monitor
1358
1643
  controls.pause();
@@ -1390,7 +1675,7 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1390
1675
  },
1391
1676
  remove: async () => {
1392
1677
  // Only available from detail view and not in historical view
1393
- if (viewMode !== 'detail' || inHistoricalView)
1678
+ if (viewMode !== "detail" || inHistoricalView)
1394
1679
  return;
1395
1680
  const selectedServer = servers[selectedServerIndex];
1396
1681
  // Show remove server dialog
@@ -1398,49 +1683,47 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1398
1683
  },
1399
1684
  startStop: async () => {
1400
1685
  // Only available from detail view and not in historical view
1401
- if (viewMode !== 'detail' || inHistoricalView)
1686
+ if (viewMode !== "detail" || inHistoricalView)
1402
1687
  return;
1403
1688
  const selectedServer = servers[selectedServerIndex];
1404
1689
  // If running, stop it. If stopped, start it.
1405
- if (selectedServer.status === 'running') {
1690
+ if (selectedServer.status === "running") {
1406
1691
  // Stop the server
1407
1692
  if (intervalId)
1408
1693
  clearInterval(intervalId);
1409
1694
  if (spinnerIntervalId)
1410
1695
  clearInterval(spinnerIntervalId);
1411
- unregisterHandlers();
1412
- const progressModal = showProgressModal('Stopping server...');
1696
+ const progressModal = showProgressModal("Stopping server...");
1413
1697
  try {
1414
- // Unload service (this stops and unregisters it)
1415
- progressModal.setContent('\n {cyan-fg}Stopping server...{/cyan-fg}');
1416
- screen.render();
1417
- await launchctl_manager_js_1.launchctlManager.unloadService(selectedServer.plistPath);
1418
- // Wait for shutdown
1419
- progressModal.setContent('\n {cyan-fg}Waiting for server to stop...{/cyan-fg}');
1420
- screen.render();
1421
- await launchctl_manager_js_1.launchctlManager.waitForServiceStop(selectedServer.label, 5000);
1422
- // Update server status
1423
- const updatedServer = await status_checker_js_1.statusChecker.updateServerStatus(selectedServer);
1424
- servers[selectedServerIndex] = updatedServer;
1425
- serverDataMap.set(updatedServer.id, {
1426
- server: updatedServer,
1698
+ // Use centralized lifecycle service
1699
+ const result = await server_lifecycle_service_js_1.serverLifecycleService.stopServer(selectedServer.id, {
1700
+ onProgress: (msg) => {
1701
+ progressModal.setContent(`\n {cyan-fg}${msg}{/cyan-fg}`);
1702
+ screen.render();
1703
+ },
1704
+ });
1705
+ if (!result.success) {
1706
+ throw new Error(result.error);
1707
+ }
1708
+ // Update local state
1709
+ servers[selectedServerIndex] = result.server;
1710
+ serverDataMap.set(result.server.id, {
1711
+ server: result.server,
1427
1712
  data: null,
1428
1713
  error: null,
1429
1714
  });
1430
- // Save updated config
1431
- await state_manager_js_1.stateManager.saveServerConfig(updatedServer);
1715
+ // Immediately update UI so it reflects the new state before modal closes
1716
+ await fetchData();
1432
1717
  // Show success briefly
1433
- progressModal.setContent('\n {green-fg}✓ Server stopped successfully!{/green-fg}');
1718
+ progressModal.setContent("\n {green-fg}✓ Server stopped successfully!{/green-fg}");
1434
1719
  screen.render();
1435
- await new Promise(resolve => setTimeout(resolve, 800));
1436
- screen.remove(progressModal);
1437
- registerHandlers();
1720
+ await new Promise((resolve) => setTimeout(resolve, 800));
1721
+ modalController.closeProgress(progressModal);
1438
1722
  startPolling();
1439
1723
  }
1440
1724
  catch (err) {
1441
- screen.remove(progressModal);
1442
- await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
1443
- registerHandlers();
1725
+ modalController.closeProgress(progressModal);
1726
+ await showErrorModal(err instanceof Error ? err.message : "Unknown error");
1444
1727
  startPolling();
1445
1728
  }
1446
1729
  return;
@@ -1451,80 +1734,43 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1451
1734
  clearInterval(intervalId);
1452
1735
  if (spinnerIntervalId)
1453
1736
  clearInterval(spinnerIntervalId);
1454
- unregisterHandlers();
1455
- const progressModal = showProgressModal('Starting server...');
1737
+ const progressModal = showProgressModal("Starting server...");
1456
1738
  try {
1457
- // Recreate plist if needed
1458
- const plistExists = await fs.access(selectedServer.plistPath).then(() => true).catch(() => false);
1459
- if (!plistExists) {
1460
- progressModal.setContent('\n {cyan-fg}Recreating plist...{/cyan-fg}');
1461
- screen.render();
1462
- await launchctl_manager_js_1.launchctlManager.createPlist(selectedServer);
1463
- }
1464
- // Load service
1465
- progressModal.setContent('\n {cyan-fg}Loading service...{/cyan-fg}');
1466
- screen.render();
1467
- try {
1468
- await launchctl_manager_js_1.launchctlManager.loadService(selectedServer.plistPath);
1469
- }
1470
- catch (err) {
1471
- // May already be loaded, continue
1472
- }
1473
- // Start service
1474
- progressModal.setContent('\n {cyan-fg}Starting server...{/cyan-fg}');
1475
- screen.render();
1476
- await launchctl_manager_js_1.launchctlManager.startService(selectedServer.label);
1477
- // Wait for startup
1478
- progressModal.setContent('\n {cyan-fg}Waiting for server to start...{/cyan-fg}');
1479
- screen.render();
1480
- const started = await launchctl_manager_js_1.launchctlManager.waitForServiceStart(selectedServer.label, 5000);
1481
- if (!started) {
1482
- throw new Error('Server failed to start. Check logs.');
1483
- }
1484
- // Wait for port to be ready
1485
- progressModal.setContent('\n {cyan-fg}Waiting for server to be ready...{/cyan-fg}');
1486
- screen.render();
1487
- const portTimeout = 10000;
1488
- const portStartTime = Date.now();
1489
- let portReady = false;
1490
- while (Date.now() - portStartTime < portTimeout) {
1491
- if (await (0, process_utils_js_1.isPortInUse)(selectedServer.port)) {
1492
- portReady = true;
1493
- break;
1494
- }
1495
- await new Promise(resolve => setTimeout(resolve, 500));
1496
- }
1497
- if (!portReady) {
1498
- throw new Error('Server started but port not responding. Check logs.');
1739
+ // Use centralized lifecycle service
1740
+ const result = await server_lifecycle_service_js_1.serverLifecycleService.startServer(selectedServer.id, {
1741
+ onProgress: (msg) => {
1742
+ progressModal.setContent(`\n {cyan-fg}${msg}{/cyan-fg}`);
1743
+ screen.render();
1744
+ },
1745
+ });
1746
+ if (!result.success) {
1747
+ throw new Error(result.error);
1499
1748
  }
1500
- // Update server status
1501
- const updatedServer = await status_checker_js_1.statusChecker.updateServerStatus(selectedServer);
1502
- servers[selectedServerIndex] = updatedServer;
1503
- serverDataMap.set(updatedServer.id, {
1504
- server: updatedServer,
1749
+ // Update local state
1750
+ servers[selectedServerIndex] = result.server;
1751
+ serverDataMap.set(result.server.id, {
1752
+ server: result.server,
1505
1753
  data: null,
1506
1754
  error: null,
1507
1755
  });
1508
- // Save updated config
1509
- await state_manager_js_1.stateManager.saveServerConfig(updatedServer);
1756
+ // Immediately update UI so it reflects the new state before modal closes
1757
+ await fetchData();
1510
1758
  // Show success briefly
1511
- progressModal.setContent('\n {green-fg}✓ Server started successfully!{/green-fg}');
1759
+ progressModal.setContent("\n {green-fg}✓ Server started successfully!{/green-fg}");
1512
1760
  screen.render();
1513
- await new Promise(resolve => setTimeout(resolve, 800));
1514
- screen.remove(progressModal);
1515
- registerHandlers();
1761
+ await new Promise((resolve) => setTimeout(resolve, 800));
1762
+ modalController.closeProgress(progressModal);
1516
1763
  startPolling();
1517
1764
  }
1518
1765
  catch (err) {
1519
- screen.remove(progressModal);
1520
- await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
1521
- registerHandlers();
1766
+ modalController.closeProgress(progressModal);
1767
+ await showErrorModal(err instanceof Error ? err.message : "Unknown error");
1522
1768
  startPolling();
1523
1769
  }
1524
1770
  },
1525
1771
  create: async () => {
1526
1772
  // Only available from list view and not in historical view
1527
- if (viewMode !== 'list' || inHistoricalView)
1773
+ if (viewMode !== "list" || inHistoricalView)
1528
1774
  return;
1529
1775
  // Show create server flow
1530
1776
  await showCreateServerFlow();
@@ -1542,60 +1788,28 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1542
1788
  }, 100);
1543
1789
  },
1544
1790
  };
1545
- // Unregister all keyboard handlers
1546
- function unregisterHandlers() {
1547
- screen.unkey('up', keyHandlers.up);
1548
- screen.unkey('k', keyHandlers.up);
1549
- screen.unkey('down', keyHandlers.down);
1550
- screen.unkey('j', keyHandlers.down);
1551
- screen.unkey('enter', keyHandlers.enter);
1552
- screen.unkey('escape', keyHandlers.escape);
1553
- screen.unkey('m', keyHandlers.models);
1554
- screen.unkey('M', keyHandlers.models);
1555
- screen.unkey('h', keyHandlers.history);
1556
- screen.unkey('H', keyHandlers.history);
1557
- screen.unkey('c', keyHandlers.config);
1558
- screen.unkey('C', keyHandlers.config);
1559
- screen.unkey('r', keyHandlers.remove);
1560
- screen.unkey('R', keyHandlers.remove);
1561
- screen.unkey('s', keyHandlers.startStop);
1562
- screen.unkey('S', keyHandlers.startStop);
1563
- screen.unkey('n', keyHandlers.create);
1564
- screen.unkey('N', keyHandlers.create);
1565
- screen.unkey('q', keyHandlers.quit);
1566
- screen.unkey('Q', keyHandlers.quit);
1567
- screen.unkey('C-c', keyHandlers.quit);
1568
- }
1569
- // Register keyboard handlers
1570
- function registerHandlers() {
1571
- screen.key(['up', 'k'], keyHandlers.up);
1572
- screen.key(['down', 'j'], keyHandlers.down);
1573
- screen.key(['enter'], keyHandlers.enter);
1574
- screen.key(['escape'], keyHandlers.escape);
1575
- screen.key(['m', 'M'], keyHandlers.models);
1576
- screen.key(['h', 'H'], keyHandlers.history);
1577
- screen.key(['c', 'C'], keyHandlers.config);
1578
- screen.key(['r', 'R'], keyHandlers.remove);
1579
- screen.key(['s', 'S'], keyHandlers.startStop);
1580
- screen.key(['n', 'N'], keyHandlers.create);
1581
- screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
1582
- }
1791
+ // NOTE: Old unregisterHandlers() and registerHandlers() functions removed.
1792
+ // Keyboard handling now managed by KeyboardManager with context stack.
1793
+ // See getHandlersForCurrentState() and updateKeyboardContext() above.
1583
1794
  // Controls object for pause/resume from other views
1584
1795
  const controls = {
1585
1796
  pause: () => {
1586
- unregisterHandlers();
1797
+ keyboardManager.popContext(); // Pop our context when pausing
1587
1798
  if (intervalId)
1588
1799
  clearInterval(intervalId);
1589
1800
  if (spinnerIntervalId)
1590
1801
  clearInterval(spinnerIntervalId);
1802
+ stopLogsAutoRefresh();
1591
1803
  screen.remove(contentBox);
1592
1804
  },
1593
1805
  resume: () => {
1594
1806
  screen.append(contentBox);
1595
- registerHandlers();
1807
+ // Reset to status view when resuming
1808
+ detailSubView = "status";
1809
+ keyboardManager.pushContext('multi-server-monitor', getHandlersForCurrentState(), false);
1596
1810
  // Re-render with last known data (instant, no loading)
1597
- let content = '';
1598
- if (viewMode === 'list') {
1811
+ let content = "";
1812
+ if (viewMode === "list") {
1599
1813
  content = renderListView(lastSystemMetrics);
1600
1814
  }
1601
1815
  else {
@@ -1608,18 +1822,20 @@ async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage
1608
1822
  },
1609
1823
  getServers: () => servers,
1610
1824
  };
1611
- // Initial registration
1612
- registerHandlers();
1825
+ // Initial keyboard context setup (non-blocking so other components can add contexts on top)
1826
+ keyboardManager.pushContext('multi-server-monitor', getHandlersForCurrentState(), false);
1613
1827
  // Initial display - skip "Connecting" message when returning from another view
1614
1828
  if (!skipConnectingMessage) {
1615
- contentBox.setContent('{cyan-fg}⏳ Connecting to servers...{/cyan-fg}');
1829
+ contentBox.setContent("{cyan-fg}⏳ Connecting to servers...{/cyan-fg}");
1616
1830
  screen.render();
1617
1831
  }
1618
1832
  startPolling();
1619
1833
  // Cleanup
1620
- screen.on('destroy', () => {
1834
+ screen.on("destroy", () => {
1621
1835
  if (intervalId)
1622
1836
  clearInterval(intervalId);
1837
+ stopLogsAutoRefresh();
1838
+ keyboardManager.clearAll(); // Clear all keyboard contexts
1623
1839
  // Note: macmon child processes will automatically die when parent exits
1624
1840
  // since they're spawned with detached: false
1625
1841
  });