@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.
- package/README.md +271 -277
- package/dist/cli.js +133 -23
- package/dist/cli.js.map +1 -1
- package/dist/commands/admin/config.d.ts +1 -1
- package/dist/commands/admin/config.js +5 -5
- package/dist/commands/admin/config.js.map +1 -1
- package/dist/commands/admin/log-config.d.ts +11 -0
- package/dist/commands/admin/log-config.d.ts.map +1 -0
- package/dist/commands/admin/log-config.js +159 -0
- package/dist/commands/admin/log-config.js.map +1 -0
- package/dist/commands/admin/logs.d.ts +2 -3
- package/dist/commands/admin/logs.d.ts.map +1 -1
- package/dist/commands/admin/logs.js +6 -48
- package/dist/commands/admin/logs.js.map +1 -1
- package/dist/commands/admin/status.d.ts.map +1 -1
- package/dist/commands/admin/status.js +1 -0
- package/dist/commands/admin/status.js.map +1 -1
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +63 -196
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/create.d.ts +3 -2
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +24 -97
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +7 -24
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/internal/server-wrapper.d.ts +15 -0
- package/dist/commands/internal/server-wrapper.d.ts.map +1 -0
- package/dist/commands/internal/server-wrapper.js +126 -0
- package/dist/commands/internal/server-wrapper.js.map +1 -0
- package/dist/commands/logs-all.d.ts +0 -2
- package/dist/commands/logs-all.d.ts.map +1 -1
- package/dist/commands/logs-all.js +1 -61
- package/dist/commands/logs-all.js.map +1 -1
- package/dist/commands/logs.d.ts +2 -5
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +104 -120
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/migrate-labels.d.ts +12 -0
- package/dist/commands/migrate-labels.d.ts.map +1 -0
- package/dist/commands/migrate-labels.js +160 -0
- package/dist/commands/migrate-labels.js.map +1 -0
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +2 -1
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +22 -48
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/router/config.d.ts +1 -1
- package/dist/commands/router/config.js +6 -6
- package/dist/commands/router/config.js.map +1 -1
- package/dist/commands/router/logs.d.ts +2 -4
- package/dist/commands/router/logs.d.ts.map +1 -1
- package/dist/commands/router/logs.js +34 -189
- package/dist/commands/router/logs.js.map +1 -1
- package/dist/commands/router/status.d.ts.map +1 -1
- package/dist/commands/router/status.js +1 -0
- package/dist/commands/router/status.js.map +1 -1
- package/dist/commands/server-show.d.ts.map +1 -1
- package/dist/commands/server-show.js +3 -0
- package/dist/commands/server-show.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +21 -72
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +10 -26
- package/dist/commands/stop.js.map +1 -1
- package/dist/launchers/llamacpp-admin +8 -0
- package/dist/launchers/llamacpp-router +8 -0
- package/dist/launchers/llamacpp-server +8 -0
- package/dist/lib/admin-manager.d.ts +4 -0
- package/dist/lib/admin-manager.d.ts.map +1 -1
- package/dist/lib/admin-manager.js +42 -18
- package/dist/lib/admin-manager.js.map +1 -1
- package/dist/lib/admin-server.d.ts +48 -1
- package/dist/lib/admin-server.d.ts.map +1 -1
- package/dist/lib/admin-server.js +632 -238
- package/dist/lib/admin-server.js.map +1 -1
- package/dist/lib/config-generator.d.ts +1 -0
- package/dist/lib/config-generator.d.ts.map +1 -1
- package/dist/lib/config-generator.js +12 -5
- package/dist/lib/config-generator.js.map +1 -1
- package/dist/lib/keyboard-manager.d.ts +162 -0
- package/dist/lib/keyboard-manager.d.ts.map +1 -0
- package/dist/lib/keyboard-manager.js +247 -0
- package/dist/lib/keyboard-manager.js.map +1 -0
- package/dist/lib/label-migration.d.ts +65 -0
- package/dist/lib/label-migration.d.ts.map +1 -0
- package/dist/lib/label-migration.js +458 -0
- package/dist/lib/label-migration.js.map +1 -0
- package/dist/lib/launchctl-manager.d.ts +9 -0
- package/dist/lib/launchctl-manager.d.ts.map +1 -1
- package/dist/lib/launchctl-manager.js +65 -19
- package/dist/lib/launchctl-manager.js.map +1 -1
- package/dist/lib/log-management-service.d.ts +51 -0
- package/dist/lib/log-management-service.d.ts.map +1 -0
- package/dist/lib/log-management-service.js +124 -0
- package/dist/lib/log-management-service.js.map +1 -0
- package/dist/lib/log-workers.d.ts +70 -0
- package/dist/lib/log-workers.d.ts.map +1 -0
- package/dist/lib/log-workers.js +217 -0
- package/dist/lib/log-workers.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +9 -1
- package/dist/lib/model-downloader.d.ts.map +1 -1
- package/dist/lib/model-downloader.js +98 -1
- package/dist/lib/model-downloader.js.map +1 -1
- package/dist/lib/model-management-service.d.ts +60 -0
- package/dist/lib/model-management-service.d.ts.map +1 -0
- package/dist/lib/model-management-service.js +246 -0
- package/dist/lib/model-management-service.js.map +1 -0
- package/dist/lib/model-management-service.test.d.ts +2 -0
- package/dist/lib/model-management-service.test.d.ts.map +1 -0
- package/dist/lib/model-management-service.test.js.map +1 -0
- package/dist/lib/model-scanner.d.ts +15 -3
- package/dist/lib/model-scanner.d.ts.map +1 -1
- package/dist/lib/model-scanner.js +174 -17
- package/dist/lib/model-scanner.js.map +1 -1
- package/dist/lib/openapi-spec.d.ts +1335 -0
- package/dist/lib/openapi-spec.d.ts.map +1 -0
- package/dist/lib/openapi-spec.js +1017 -0
- package/dist/lib/openapi-spec.js.map +1 -0
- package/dist/lib/router-logger.d.ts +1 -1
- package/dist/lib/router-logger.d.ts.map +1 -1
- package/dist/lib/router-logger.js +13 -11
- package/dist/lib/router-logger.js.map +1 -1
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +30 -18
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +22 -12
- package/dist/lib/router-server.js.map +1 -1
- package/dist/lib/server-config-service.d.ts +51 -0
- package/dist/lib/server-config-service.d.ts.map +1 -0
- package/dist/lib/server-config-service.js +310 -0
- package/dist/lib/server-config-service.js.map +1 -0
- package/dist/lib/server-config-service.test.d.ts +2 -0
- package/dist/lib/server-config-service.test.d.ts.map +1 -0
- package/dist/lib/server-config-service.test.js.map +1 -0
- package/dist/lib/server-lifecycle-service.d.ts +172 -0
- package/dist/lib/server-lifecycle-service.d.ts.map +1 -0
- package/dist/lib/server-lifecycle-service.js +619 -0
- package/dist/lib/server-lifecycle-service.js.map +1 -0
- package/dist/lib/state-manager.d.ts +18 -1
- package/dist/lib/state-manager.d.ts.map +1 -1
- package/dist/lib/state-manager.js +51 -2
- package/dist/lib/state-manager.js.map +1 -1
- package/dist/lib/status-checker.d.ts +11 -4
- package/dist/lib/status-checker.d.ts.map +1 -1
- package/dist/lib/status-checker.js +34 -1
- package/dist/lib/status-checker.js.map +1 -1
- package/dist/lib/validation-service.d.ts +43 -0
- package/dist/lib/validation-service.d.ts.map +1 -0
- package/dist/lib/validation-service.js +112 -0
- package/dist/lib/validation-service.js.map +1 -0
- package/dist/lib/validation-service.test.d.ts +2 -0
- package/dist/lib/validation-service.test.d.ts.map +1 -0
- package/dist/lib/validation-service.test.js.map +1 -0
- package/dist/scripts/http-log-filter.sh +8 -0
- package/dist/tui/ConfigApp.d.ts.map +1 -1
- package/dist/tui/ConfigApp.js +222 -184
- package/dist/tui/ConfigApp.js.map +1 -1
- package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
- package/dist/tui/HistoricalMonitorApp.js +12 -0
- package/dist/tui/HistoricalMonitorApp.js.map +1 -1
- package/dist/tui/ModelsApp.d.ts.map +1 -1
- package/dist/tui/ModelsApp.js +93 -17
- package/dist/tui/ModelsApp.js.map +1 -1
- package/dist/tui/MonitorApp.d.ts.map +1 -1
- package/dist/tui/MonitorApp.js +1 -3
- package/dist/tui/MonitorApp.js.map +1 -1
- package/dist/tui/MultiServerMonitorApp.d.ts +3 -3
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
- package/dist/tui/MultiServerMonitorApp.js +724 -508
- package/dist/tui/MultiServerMonitorApp.js.map +1 -1
- package/dist/tui/RootNavigator.d.ts.map +1 -1
- package/dist/tui/RootNavigator.js +17 -1
- package/dist/tui/RootNavigator.js.map +1 -1
- package/dist/tui/RouterApp.d.ts +6 -0
- package/dist/tui/RouterApp.d.ts.map +1 -0
- package/dist/tui/RouterApp.js +928 -0
- package/dist/tui/RouterApp.js.map +1 -0
- package/dist/tui/SearchApp.d.ts.map +1 -1
- package/dist/tui/SearchApp.js +27 -6
- package/dist/tui/SearchApp.js.map +1 -1
- package/dist/tui/shared/modal-controller.d.ts +65 -0
- package/dist/tui/shared/modal-controller.d.ts.map +1 -0
- package/dist/tui/shared/modal-controller.js +625 -0
- package/dist/tui/shared/modal-controller.js.map +1 -0
- package/dist/tui/shared/overlay-utils.d.ts +7 -0
- package/dist/tui/shared/overlay-utils.d.ts.map +1 -0
- package/dist/tui/shared/overlay-utils.js +54 -0
- package/dist/tui/shared/overlay-utils.js.map +1 -0
- package/dist/types/admin-config.d.ts +15 -2
- package/dist/types/admin-config.d.ts.map +1 -1
- package/dist/types/model-info.d.ts +5 -0
- package/dist/types/model-info.d.ts.map +1 -1
- package/dist/types/router-config.d.ts +2 -2
- package/dist/types/router-config.d.ts.map +1 -1
- package/dist/types/server-config.d.ts +8 -0
- package/dist/types/server-config.d.ts.map +1 -1
- package/dist/types/server-config.js +25 -0
- package/dist/types/server-config.js.map +1 -1
- package/dist/utils/http-log-filter.d.ts +10 -0
- package/dist/utils/http-log-filter.d.ts.map +1 -0
- package/dist/utils/http-log-filter.js +84 -0
- package/dist/utils/http-log-filter.js.map +1 -0
- package/dist/utils/log-parser.d.ts.map +1 -1
- package/dist/utils/log-parser.js +7 -4
- package/dist/utils/log-parser.js.map +1 -1
- package/dist/utils/log-utils.d.ts +59 -4
- package/dist/utils/log-utils.d.ts.map +1 -1
- package/dist/utils/log-utils.js +150 -11
- package/dist/utils/log-utils.js.map +1 -1
- package/dist/utils/shard-utils.d.ts +72 -0
- package/dist/utils/shard-utils.d.ts.map +1 -0
- package/dist/utils/shard-utils.js +168 -0
- package/dist/utils/shard-utils.js.map +1 -0
- package/package.json +18 -4
- package/src/launchers/llamacpp-admin +8 -0
- package/src/launchers/llamacpp-router +8 -0
- package/src/launchers/llamacpp-server +8 -0
- package/web/dist/assets/index-Byhoy86V.css +1 -0
- package/web/dist/assets/index-HSrgvray.js +50 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Bin89Lwr.css +0 -1
- 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
|
|
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
|
|
56
|
-
|
|
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 ?
|
|
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:
|
|
90
|
-
height:
|
|
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:
|
|
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
|
|
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 +=
|
|
124
|
+
let content = "";
|
|
125
|
+
content += "{bold}System Resources{/bold}\n";
|
|
115
126
|
const termWidth = screen.width || 80;
|
|
116
|
-
const divider =
|
|
117
|
-
content += divider +
|
|
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 +=
|
|
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 /
|
|
137
|
-
const memoryTotalGB = systemMetrics.memoryTotal /
|
|
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(
|
|
155
|
+
content += `\n{yellow-fg}⚠ ${systemMetrics.warnings.join(", ")}{/yellow-fg}\n`;
|
|
145
156
|
}
|
|
146
157
|
}
|
|
147
158
|
else {
|
|
148
|
-
content +=
|
|
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 +=
|
|
165
|
+
let content = "";
|
|
166
|
+
content += "{bold}Server Resources{/bold}\n";
|
|
156
167
|
const termWidth = screen.width || 80;
|
|
157
|
-
const divider =
|
|
158
|
-
content += divider +
|
|
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 ===
|
|
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 +=
|
|
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 ?
|
|
195
|
+
content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? "server" : "servers"}){/gray-fg}\n`;
|
|
183
196
|
// Memory: Sum of all process memory
|
|
184
|
-
const totalMemoryGB = totalMemoryBytes /
|
|
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 ?
|
|
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 +=
|
|
207
|
+
let content = "";
|
|
208
|
+
content += "{bold}Server Resources{/bold}\n";
|
|
196
209
|
const termWidth = screen.width || 80;
|
|
197
|
-
const divider =
|
|
198
|
-
content += divider +
|
|
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 +=
|
|
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 /
|
|
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 &&
|
|
222
|
-
|
|
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 ===
|
|
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 ===
|
|
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 =
|
|
269
|
-
let content =
|
|
283
|
+
const divider = "─".repeat(termWidth - 2);
|
|
284
|
+
let content = "";
|
|
270
285
|
// Header
|
|
271
|
-
content +=
|
|
286
|
+
content += "{bold}{blue-fg}═══ LLAMACPP{/blue-fg}{/bold}\n\n";
|
|
272
287
|
// System resources
|
|
273
288
|
content += renderSystemResources(systemMetrics);
|
|
274
|
-
content +=
|
|
289
|
+
content += "\n";
|
|
275
290
|
// Aggregate model resources (CPU + memory for all running servers)
|
|
276
291
|
content += renderAggregateModelResources();
|
|
277
|
-
content +=
|
|
292
|
+
content += "\n";
|
|
278
293
|
// Server list header
|
|
279
|
-
const runningCount = servers.filter(s => s.status ===
|
|
280
|
-
const stoppedCount = servers.filter(s => s.status !==
|
|
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 +=
|
|
283
|
-
|
|
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 =
|
|
309
|
+
const serverIdHeader = "Server ID".padEnd(serverIdWidth);
|
|
294
310
|
content += `{bold} │ ${serverIdHeader}│ Port │ Status │ Slots │ tok/s │ Memory{/bold}\n`;
|
|
295
|
-
content += divider +
|
|
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
|
-
|
|
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 !==
|
|
331
|
+
let status = "";
|
|
332
|
+
let statusPlain = "";
|
|
333
|
+
if (server.status !== "running") {
|
|
312
334
|
// Server is stopped according to config
|
|
313
|
-
status =
|
|
314
|
-
statusPlain =
|
|
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 =
|
|
320
|
-
statusPlain =
|
|
341
|
+
status = "{green-fg}● RUN{/green-fg} ";
|
|
342
|
+
statusPlain = "● RUN ";
|
|
321
343
|
}
|
|
322
344
|
else {
|
|
323
|
-
status =
|
|
324
|
-
statusPlain =
|
|
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 =
|
|
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)
|
|
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 +
|
|
394
|
+
content += rowContent + "\n";
|
|
371
395
|
});
|
|
372
396
|
// Footer
|
|
373
|
-
content +=
|
|
374
|
-
content += `{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 =
|
|
383
|
-
let content =
|
|
406
|
+
const divider = "─".repeat(termWidth - 2);
|
|
407
|
+
let content = "";
|
|
384
408
|
// Header
|
|
385
|
-
|
|
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 !==
|
|
414
|
+
if (server.status !== "running") {
|
|
388
415
|
// Show minimal stopped server info
|
|
389
|
-
content +=
|
|
390
|
-
content += divider +
|
|
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 ||
|
|
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 +=
|
|
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 +=
|
|
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 +=
|
|
434
|
+
content += "\n";
|
|
408
435
|
// Server Information
|
|
409
|
-
content +=
|
|
410
|
-
content += divider +
|
|
411
|
-
const statusIcon = data.server.healthy
|
|
412
|
-
|
|
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 +=
|
|
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 +=
|
|
451
|
+
content += "\n";
|
|
423
452
|
// Handle null host (legacy configs) by defaulting to 127.0.0.1
|
|
424
|
-
const displayHost = server.host ||
|
|
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 +=
|
|
456
|
+
content += "\n";
|
|
428
457
|
// Request Metrics
|
|
429
458
|
if (data.server.totalSlots > 0) {
|
|
430
|
-
content +=
|
|
431
|
-
content += divider +
|
|
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 &&
|
|
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 &&
|
|
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 +=
|
|
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 ===
|
|
475
|
+
const activeSlots = data.server.slots.filter((s) => s.state === "processing");
|
|
445
476
|
if (activeSlots.length > 0) {
|
|
446
|
-
content +=
|
|
447
|
-
content += divider +
|
|
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 +=
|
|
489
|
+
content += " tokens";
|
|
459
490
|
}
|
|
460
|
-
content +=
|
|
491
|
+
content += "\n";
|
|
461
492
|
});
|
|
462
|
-
content +=
|
|
493
|
+
content += "\n";
|
|
463
494
|
}
|
|
464
495
|
}
|
|
465
496
|
// Footer - show [S]top for running servers
|
|
466
|
-
content += divider +
|
|
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(
|
|
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([
|
|
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 ===
|
|
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 :
|
|
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 !==
|
|
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 &&
|
|
796
|
+
if (serverData.data &&
|
|
797
|
+
!serverData.data.server.stale &&
|
|
798
|
+
serverData.data.server.healthy) {
|
|
545
799
|
const manager = historyManagers.get(serverId);
|
|
546
|
-
manager
|
|
547
|
-
.
|
|
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
|
-
|
|
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 :
|
|
573
|
-
contentBox.setContent(
|
|
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
|
-
|
|
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] ===
|
|
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 =
|
|
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:
|
|
612
|
-
left:
|
|
613
|
-
width:
|
|
858
|
+
top: "center",
|
|
859
|
+
left: "center",
|
|
860
|
+
width: "70%",
|
|
614
861
|
height,
|
|
615
|
-
border: { type:
|
|
862
|
+
border: { type: "line" },
|
|
616
863
|
style: {
|
|
617
864
|
border: { fg: borderColor },
|
|
618
|
-
fg:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 ===
|
|
666
|
-
if (server.status ===
|
|
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([
|
|
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([
|
|
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([
|
|
948
|
+
modal.key(["space"], () => {
|
|
710
949
|
if (showDeleteModelOption && selectedOption === 0) {
|
|
711
950
|
deleteModelOption = !deleteModelOption;
|
|
712
951
|
renderDialog();
|
|
713
952
|
}
|
|
714
953
|
});
|
|
715
|
-
modal.key([
|
|
954
|
+
modal.key(["escape"], () => {
|
|
716
955
|
screen.remove(modal);
|
|
717
|
-
|
|
956
|
+
screen.remove(overlay);
|
|
957
|
+
keyboardManager.popContext(); // Pop the blocking context
|
|
718
958
|
startPolling();
|
|
719
959
|
resolve();
|
|
720
960
|
});
|
|
721
|
-
modal.key([
|
|
961
|
+
modal.key(["enter"], async () => {
|
|
722
962
|
screen.remove(modal);
|
|
963
|
+
screen.remove(overlay);
|
|
723
964
|
// Show progress
|
|
724
|
-
const progressModal = showProgressModal(
|
|
965
|
+
const progressModal = showProgressModal("Removing server...");
|
|
725
966
|
try {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
991
|
+
viewMode = "list";
|
|
770
992
|
selectedRowIndex = Math.min(selectedRowIndex, Math.max(0, servers.length - 1));
|
|
771
993
|
selectedServerIndex = selectedRowIndex;
|
|
772
|
-
|
|
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
|
-
|
|
778
|
-
await showErrorModal(err instanceof Error ? err.message :
|
|
779
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
822
|
-
content +=
|
|
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
|
|
839
|
-
|
|
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 +=
|
|
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([
|
|
1088
|
+
modelModal.key(["up", "k"], () => {
|
|
860
1089
|
selectedModelIndex = Math.max(0, selectedModelIndex - 1);
|
|
861
1090
|
renderModelPicker();
|
|
862
1091
|
});
|
|
863
|
-
modelModal.key([
|
|
1092
|
+
modelModal.key(["down", "j"], () => {
|
|
864
1093
|
selectedModelIndex = Math.min(models.length - 1, selectedModelIndex + 1);
|
|
865
1094
|
renderModelPicker();
|
|
866
1095
|
});
|
|
867
|
-
modelModal.key([
|
|
1096
|
+
modelModal.key(["escape"], () => {
|
|
868
1097
|
screen.remove(modelModal);
|
|
1098
|
+
screen.remove(modelOverlay);
|
|
869
1099
|
resolve(null);
|
|
870
1100
|
});
|
|
871
|
-
modelModal.key([
|
|
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
|
-
|
|
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) {
|
|
1123
|
+
if (modelSize < 1024 * 1024 * 1024) {
|
|
1124
|
+
// < 1GB
|
|
905
1125
|
defaultCtxSize = 2048;
|
|
906
1126
|
}
|
|
907
|
-
else if (modelSize < 3 * 1024 * 1024 * 1024) {
|
|
1127
|
+
else if (modelSize < 3 * 1024 * 1024 * 1024) {
|
|
1128
|
+
// < 3GB
|
|
908
1129
|
defaultCtxSize = 4096;
|
|
909
1130
|
}
|
|
910
|
-
else if (modelSize < 6 * 1024 * 1024 * 1024) {
|
|
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(
|
|
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:
|
|
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
|
-
{
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
|
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 ===
|
|
939
|
-
return value ?
|
|
940
|
-
if (key ===
|
|
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 =
|
|
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 +=
|
|
949
|
-
content +=
|
|
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 +=
|
|
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 +=
|
|
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([
|
|
1208
|
+
configModal.key(["up", "k"], () => {
|
|
979
1209
|
selectedFieldIndex = Math.max(0, selectedFieldIndex - 1);
|
|
980
1210
|
renderConfigScreen();
|
|
981
1211
|
});
|
|
982
|
-
configModal.key([
|
|
1212
|
+
configModal.key(["down", "j"], () => {
|
|
983
1213
|
selectedFieldIndex = Math.min(fields.length, selectedFieldIndex + 1);
|
|
984
1214
|
renderConfigScreen();
|
|
985
1215
|
});
|
|
986
|
-
configModal.key([
|
|
1216
|
+
configModal.key(["escape"], () => {
|
|
987
1217
|
screen.remove(configModal);
|
|
1218
|
+
screen.remove(configOverlay);
|
|
988
1219
|
resolve(false);
|
|
989
1220
|
});
|
|
990
|
-
configModal.key([
|
|
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 ===
|
|
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 =
|
|
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 ===
|
|
1019
|
-
content +=
|
|
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 +=
|
|
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([
|
|
1264
|
+
selectModal.key(["up", "k"], () => {
|
|
1029
1265
|
optionIndex = Math.max(0, optionIndex - 1);
|
|
1030
1266
|
renderSelectOptions();
|
|
1031
1267
|
});
|
|
1032
|
-
selectModal.key([
|
|
1268
|
+
selectModal.key(["down", "j"], () => {
|
|
1033
1269
|
optionIndex = Math.min(options.length - 1, optionIndex + 1);
|
|
1034
1270
|
renderSelectOptions();
|
|
1035
1271
|
});
|
|
1036
|
-
selectModal.key([
|
|
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([
|
|
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 ===
|
|
1287
|
+
else if (field.type === "toggle") {
|
|
1050
1288
|
config[field.key] = !config[field.key];
|
|
1051
1289
|
renderConfigScreen();
|
|
1052
1290
|
}
|
|
1053
|
-
else if (field.type ===
|
|
1291
|
+
else if (field.type === "number") {
|
|
1054
1292
|
// Number input
|
|
1055
|
-
const isCtxSize = field.key ===
|
|
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:
|
|
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:
|
|
1324
|
+
border: { type: "line" },
|
|
1085
1325
|
style: {
|
|
1086
|
-
border: { fg:
|
|
1087
|
-
focus: { border: { fg:
|
|
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:
|
|
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(
|
|
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(
|
|
1362
|
+
inputBox.on("cancel", () => {
|
|
1122
1363
|
screen.remove(inputModal);
|
|
1364
|
+
screen.remove(inputOverlay);
|
|
1123
1365
|
resolveInput();
|
|
1124
1366
|
});
|
|
1125
|
-
inputBox.key([
|
|
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
|
-
|
|
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(
|
|
1389
|
+
const progressModal = showProgressModal("Creating server...");
|
|
1147
1390
|
try {
|
|
1148
|
-
//
|
|
1149
|
-
const
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
1223
|
-
await state_manager_js_1.stateManager.saveServerConfig(updatedConfig);
|
|
1408
|
+
const updatedConfig = result.server;
|
|
1224
1409
|
// Show success message briefly
|
|
1225
|
-
progressModal.setContent(
|
|
1410
|
+
progressModal.setContent("\n {green-fg}✓ Server created successfully!{/green-fg}");
|
|
1226
1411
|
screen.render();
|
|
1227
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1228
|
-
|
|
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
|
-
|
|
1426
|
+
keyboardManager.popContext(); // Pop the blocking context
|
|
1242
1427
|
startPolling();
|
|
1243
1428
|
}
|
|
1244
1429
|
catch (err) {
|
|
1245
|
-
|
|
1246
|
-
await showErrorModal(err instanceof Error ? err.message :
|
|
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
1521
|
+
if (viewMode === "list") {
|
|
1277
1522
|
showLoading();
|
|
1278
1523
|
selectedServerIndex = selectedRowIndex;
|
|
1279
|
-
viewMode =
|
|
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
|
|
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 ===
|
|
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 =
|
|
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
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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 ===
|
|
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 ===
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
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
|
-
|
|
1412
|
-
const progressModal = showProgressModal('Stopping server...');
|
|
1696
|
+
const progressModal = showProgressModal("Stopping server...");
|
|
1413
1697
|
try {
|
|
1414
|
-
//
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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
|
-
//
|
|
1431
|
-
await
|
|
1715
|
+
// Immediately update UI so it reflects the new state before modal closes
|
|
1716
|
+
await fetchData();
|
|
1432
1717
|
// Show success briefly
|
|
1433
|
-
progressModal.setContent(
|
|
1718
|
+
progressModal.setContent("\n {green-fg}✓ Server stopped successfully!{/green-fg}");
|
|
1434
1719
|
screen.render();
|
|
1435
|
-
await new Promise(resolve => setTimeout(resolve, 800));
|
|
1436
|
-
|
|
1437
|
-
registerHandlers();
|
|
1720
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1721
|
+
modalController.closeProgress(progressModal);
|
|
1438
1722
|
startPolling();
|
|
1439
1723
|
}
|
|
1440
1724
|
catch (err) {
|
|
1441
|
-
|
|
1442
|
-
await showErrorModal(err instanceof Error ? err.message :
|
|
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
|
-
|
|
1455
|
-
const progressModal = showProgressModal('Starting server...');
|
|
1737
|
+
const progressModal = showProgressModal("Starting server...");
|
|
1456
1738
|
try {
|
|
1457
|
-
//
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
-
//
|
|
1509
|
-
await
|
|
1756
|
+
// Immediately update UI so it reflects the new state before modal closes
|
|
1757
|
+
await fetchData();
|
|
1510
1758
|
// Show success briefly
|
|
1511
|
-
progressModal.setContent(
|
|
1759
|
+
progressModal.setContent("\n {green-fg}✓ Server started successfully!{/green-fg}");
|
|
1512
1760
|
screen.render();
|
|
1513
|
-
await new Promise(resolve => setTimeout(resolve, 800));
|
|
1514
|
-
|
|
1515
|
-
registerHandlers();
|
|
1761
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1762
|
+
modalController.closeProgress(progressModal);
|
|
1516
1763
|
startPolling();
|
|
1517
1764
|
}
|
|
1518
1765
|
catch (err) {
|
|
1519
|
-
|
|
1520
|
-
await showErrorModal(err instanceof Error ? err.message :
|
|
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 !==
|
|
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
|
-
//
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
1612
|
-
|
|
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(
|
|
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(
|
|
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
|
});
|