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