@appkit/llamacpp-cli 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +249 -40
- package/dist/cli.js +154 -10
- package/dist/cli.js.map +1 -1
- package/dist/commands/completion.d.ts +9 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +83 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/monitor.js +1 -1
- package/dist/commands/monitor.js.map +1 -1
- package/dist/commands/ps.d.ts +1 -3
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +36 -115
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/router/config.d.ts +11 -0
- package/dist/commands/router/config.d.ts.map +1 -0
- package/dist/commands/router/config.js +100 -0
- package/dist/commands/router/config.js.map +1 -0
- package/dist/commands/router/logs.d.ts +12 -0
- package/dist/commands/router/logs.d.ts.map +1 -0
- package/dist/commands/router/logs.js +238 -0
- package/dist/commands/router/logs.js.map +1 -0
- package/dist/commands/router/restart.d.ts +2 -0
- package/dist/commands/router/restart.d.ts.map +1 -0
- package/dist/commands/router/restart.js +39 -0
- package/dist/commands/router/restart.js.map +1 -0
- package/dist/commands/router/start.d.ts +2 -0
- package/dist/commands/router/start.d.ts.map +1 -0
- package/dist/commands/router/start.js +60 -0
- package/dist/commands/router/start.js.map +1 -0
- package/dist/commands/router/status.d.ts +2 -0
- package/dist/commands/router/status.d.ts.map +1 -0
- package/dist/commands/router/status.js +116 -0
- package/dist/commands/router/status.js.map +1 -0
- package/dist/commands/router/stop.d.ts +2 -0
- package/dist/commands/router/stop.d.ts.map +1 -0
- package/dist/commands/router/stop.js +36 -0
- package/dist/commands/router/stop.js.map +1 -0
- package/dist/commands/tui.d.ts +2 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +27 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/lib/completion.d.ts +5 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +195 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +5 -1
- package/dist/lib/model-downloader.d.ts.map +1 -1
- package/dist/lib/model-downloader.js +53 -20
- package/dist/lib/model-downloader.js.map +1 -1
- package/dist/lib/router-logger.d.ts +61 -0
- package/dist/lib/router-logger.d.ts.map +1 -0
- package/dist/lib/router-logger.js +200 -0
- package/dist/lib/router-logger.js.map +1 -0
- package/dist/lib/router-manager.d.ts +103 -0
- package/dist/lib/router-manager.d.ts.map +1 -0
- package/dist/lib/router-manager.js +394 -0
- package/dist/lib/router-manager.js.map +1 -0
- package/dist/lib/router-server.d.ts +61 -0
- package/dist/lib/router-server.d.ts.map +1 -0
- package/dist/lib/router-server.js +485 -0
- package/dist/lib/router-server.js.map +1 -0
- package/dist/tui/ConfigApp.d.ts +7 -0
- package/dist/tui/ConfigApp.d.ts.map +1 -0
- package/dist/tui/ConfigApp.js +1002 -0
- package/dist/tui/ConfigApp.js.map +1 -0
- package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
- package/dist/tui/HistoricalMonitorApp.js +85 -49
- package/dist/tui/HistoricalMonitorApp.js.map +1 -1
- package/dist/tui/ModelsApp.d.ts +7 -0
- package/dist/tui/ModelsApp.d.ts.map +1 -0
- package/dist/tui/ModelsApp.js +362 -0
- package/dist/tui/ModelsApp.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
- package/dist/tui/MultiServerMonitorApp.js +1038 -122
- package/dist/tui/MultiServerMonitorApp.js.map +1 -1
- package/dist/tui/RootNavigator.d.ts +7 -0
- package/dist/tui/RootNavigator.d.ts.map +1 -0
- package/dist/tui/RootNavigator.js +55 -0
- package/dist/tui/RootNavigator.js.map +1 -0
- package/dist/tui/SearchApp.d.ts +6 -0
- package/dist/tui/SearchApp.d.ts.map +1 -0
- package/dist/tui/SearchApp.js +451 -0
- package/dist/tui/SearchApp.js.map +1 -0
- package/dist/tui/SplashScreen.d.ts +16 -0
- package/dist/tui/SplashScreen.d.ts.map +1 -0
- package/dist/tui/SplashScreen.js +129 -0
- package/dist/tui/SplashScreen.js.map +1 -0
- package/dist/types/router-config.d.ts +19 -0
- package/dist/types/router-config.d.ts.map +1 -0
- package/dist/types/router-config.js +3 -0
- package/dist/types/router-config.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +121 -10
- package/src/commands/monitor.ts +1 -1
- package/src/commands/ps.ts +44 -133
- package/src/commands/router/config.ts +116 -0
- package/src/commands/router/logs.ts +256 -0
- package/src/commands/router/restart.ts +36 -0
- package/src/commands/router/start.ts +60 -0
- package/src/commands/router/status.ts +119 -0
- package/src/commands/router/stop.ts +33 -0
- package/src/commands/tui.ts +25 -0
- package/src/lib/model-downloader.ts +57 -20
- package/src/lib/router-logger.ts +201 -0
- package/src/lib/router-manager.ts +414 -0
- package/src/lib/router-server.ts +538 -0
- package/src/tui/ConfigApp.ts +1085 -0
- package/src/tui/HistoricalMonitorApp.ts +88 -49
- package/src/tui/ModelsApp.ts +368 -0
- package/src/tui/MultiServerMonitorApp.ts +1163 -122
- package/src/tui/RootNavigator.ts +74 -0
- package/src/tui/SearchApp.ts +511 -0
- package/src/tui/SplashScreen.ts +149 -0
- package/src/types/router-config.ts +25 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { ServerConfig } from '../types/server-config.js';
|
|
3
|
+
import { createMultiServerMonitorUI, MonitorUIControls } from './MultiServerMonitorApp.js';
|
|
4
|
+
import { createModelsUI } from './ModelsApp.js';
|
|
5
|
+
import { createSplashScreen } from './SplashScreen.js';
|
|
6
|
+
|
|
7
|
+
type RootView = 'monitor' | 'models';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Root navigator that manages switching between Monitor and Models views
|
|
11
|
+
*/
|
|
12
|
+
export async function createRootNavigator(
|
|
13
|
+
screen: blessed.Widgets.Screen,
|
|
14
|
+
servers: ServerConfig[],
|
|
15
|
+
directJumpToServer?: number
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
let currentView: RootView = 'monitor';
|
|
18
|
+
let isExiting = false;
|
|
19
|
+
|
|
20
|
+
// Show splash screen with loading sequence
|
|
21
|
+
const cleanupSplash = await createSplashScreen(screen, {
|
|
22
|
+
onLoadConfigs: async () => {
|
|
23
|
+
// Configs are already loaded (passed in), but simulate brief work
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
25
|
+
},
|
|
26
|
+
onCheckServices: async () => {
|
|
27
|
+
// Services already checked by ps command, but simulate brief work
|
|
28
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
29
|
+
},
|
|
30
|
+
onInitMetrics: async () => {
|
|
31
|
+
// Metrics collectors initialize on first poll, but simulate brief work
|
|
32
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Callback to switch to Models view (receives controls from Monitor)
|
|
37
|
+
const switchToModels = async (monitorControls: MonitorUIControls) => {
|
|
38
|
+
if (isExiting) return;
|
|
39
|
+
currentView = 'models';
|
|
40
|
+
|
|
41
|
+
// Create Models view (Monitor is paused, not destroyed)
|
|
42
|
+
await createModelsUI(
|
|
43
|
+
screen,
|
|
44
|
+
async () => {
|
|
45
|
+
// onBack callback - return to Monitor view
|
|
46
|
+
if (isExiting) return;
|
|
47
|
+
currentView = 'monitor';
|
|
48
|
+
|
|
49
|
+
// Resume Monitor view instantly (no reload, just re-attach and resume polling)
|
|
50
|
+
monitorControls.resume();
|
|
51
|
+
},
|
|
52
|
+
async () => {
|
|
53
|
+
// onSearch callback - handled by ModelsApp (opens SearchApp)
|
|
54
|
+
// This is a placeholder, actual implementation in ModelsApp
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Start with Monitor view (skip connecting message since splash already showed loading)
|
|
60
|
+
// Pass cleanupSplash as onFirstRender callback - splash stays until monitor data is ready
|
|
61
|
+
await createMultiServerMonitorUI(
|
|
62
|
+
screen,
|
|
63
|
+
servers,
|
|
64
|
+
true, // Skip "Connecting to servers..." since splash screen showed loading
|
|
65
|
+
directJumpToServer,
|
|
66
|
+
switchToModels,
|
|
67
|
+
cleanupSplash
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Handle cleanup on exit
|
|
71
|
+
screen.on('destroy', () => {
|
|
72
|
+
isExiting = true;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { modelSearch, HFModelResult } from '../lib/model-search.js';
|
|
3
|
+
import { modelDownloader, DownloadProgress } from '../lib/model-downloader.js';
|
|
4
|
+
import { stateManager } from '../lib/state-manager.js';
|
|
5
|
+
import { formatBytes } from '../utils/format-utils.js';
|
|
6
|
+
|
|
7
|
+
interface SearchState {
|
|
8
|
+
query: string;
|
|
9
|
+
results: HFModelResult[];
|
|
10
|
+
selectedIndex: number;
|
|
11
|
+
isSearching: boolean;
|
|
12
|
+
expandedModelIndex: number | null;
|
|
13
|
+
modelFiles: string[];
|
|
14
|
+
selectedFileIndex: number;
|
|
15
|
+
isLoadingFiles: boolean;
|
|
16
|
+
error: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Search HuggingFace and download models TUI
|
|
21
|
+
*/
|
|
22
|
+
export async function createSearchUI(
|
|
23
|
+
screen: blessed.Widgets.Screen,
|
|
24
|
+
onBack: () => void
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const state: SearchState = {
|
|
27
|
+
query: '',
|
|
28
|
+
results: [],
|
|
29
|
+
selectedIndex: 0,
|
|
30
|
+
isSearching: false,
|
|
31
|
+
expandedModelIndex: null,
|
|
32
|
+
modelFiles: [],
|
|
33
|
+
selectedFileIndex: 0,
|
|
34
|
+
isLoadingFiles: false,
|
|
35
|
+
error: null,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Create content box for results
|
|
39
|
+
const contentBox = blessed.box({
|
|
40
|
+
parent: screen,
|
|
41
|
+
top: 0,
|
|
42
|
+
left: 0,
|
|
43
|
+
right: 0,
|
|
44
|
+
bottom: 0,
|
|
45
|
+
tags: true,
|
|
46
|
+
scrollable: true,
|
|
47
|
+
alwaysScroll: true,
|
|
48
|
+
keys: true,
|
|
49
|
+
vi: true,
|
|
50
|
+
mouse: true,
|
|
51
|
+
scrollbar: {
|
|
52
|
+
ch: '█',
|
|
53
|
+
style: {
|
|
54
|
+
fg: 'blue',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Render content
|
|
60
|
+
function render() {
|
|
61
|
+
const termWidth = (screen.width as number) || 80;
|
|
62
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
63
|
+
let content = '';
|
|
64
|
+
|
|
65
|
+
// Header
|
|
66
|
+
content += '{bold}{blue-fg}═══ Search Models{/blue-fg}{/bold}\n';
|
|
67
|
+
content += '{gray-fg}Press [/] to search HuggingFace{/gray-fg}\n';
|
|
68
|
+
content += divider + '\n\n';
|
|
69
|
+
|
|
70
|
+
// Show searching state
|
|
71
|
+
if (state.isSearching) {
|
|
72
|
+
content += '{cyan-fg}⏳ Searching...{/cyan-fg}\n';
|
|
73
|
+
contentBox.setContent(content);
|
|
74
|
+
screen.render();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Show error
|
|
79
|
+
if (state.error) {
|
|
80
|
+
content += `{red-fg}❌ Error: ${state.error}{/red-fg}\n\n`;
|
|
81
|
+
content += '{gray-fg}Press [/] to search again{/gray-fg}\n';
|
|
82
|
+
contentBox.setContent(content);
|
|
83
|
+
screen.render();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Show results or empty state
|
|
88
|
+
if (state.results.length === 0) {
|
|
89
|
+
if (state.query) {
|
|
90
|
+
content += `{yellow-fg}No results found for: ${state.query}{/yellow-fg}\n`;
|
|
91
|
+
} else {
|
|
92
|
+
content += '{gray-fg}Press [/] to search HuggingFace for models{/gray-fg}\n';
|
|
93
|
+
}
|
|
94
|
+
content += '\n' + divider + '\n';
|
|
95
|
+
content += '{gray-fg}[/] Search [ESC] Back [Q]uit{/gray-fg}';
|
|
96
|
+
contentBox.setContent(content);
|
|
97
|
+
screen.render();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Show expanded model files
|
|
102
|
+
if (state.expandedModelIndex !== null) {
|
|
103
|
+
const model = state.results[state.expandedModelIndex];
|
|
104
|
+
content += `{bold}Model: ${model.modelId}{/bold}\n`;
|
|
105
|
+
content += `Downloads: ${model.downloads.toLocaleString()} | Likes: ${model.likes}\n`;
|
|
106
|
+
content += divider + '\n\n';
|
|
107
|
+
|
|
108
|
+
if (state.isLoadingFiles) {
|
|
109
|
+
content += '{cyan-fg}⏳ Loading GGUF files...{/cyan-fg}\n';
|
|
110
|
+
} else if (state.modelFiles.length === 0) {
|
|
111
|
+
content += '{yellow-fg}No GGUF files found for this model{/yellow-fg}\n';
|
|
112
|
+
} else {
|
|
113
|
+
content += '{bold}GGUF Files:{/bold}\n\n';
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < state.modelFiles.length; i++) {
|
|
116
|
+
const file = state.modelFiles[i];
|
|
117
|
+
const isSelected = i === state.selectedFileIndex;
|
|
118
|
+
const indicator = isSelected ? '►' : ' ';
|
|
119
|
+
|
|
120
|
+
let rowContent = '';
|
|
121
|
+
if (isSelected) {
|
|
122
|
+
rowContent = `{cyan-bg}{15-fg}${indicator} ${file}{/15-fg}{/cyan-bg}`;
|
|
123
|
+
} else {
|
|
124
|
+
rowContent = `${indicator} ${file}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
content += rowContent + '\n';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
content += '\n{gray-fg}Press Enter to download selected file{/gray-fg}\n';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
content += '\n' + divider + '\n';
|
|
134
|
+
content += '{gray-fg}[↑/↓] Navigate [Enter] Download [ESC] Back{/gray-fg}';
|
|
135
|
+
contentBox.setContent(content);
|
|
136
|
+
screen.render();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Show results table
|
|
141
|
+
content += `{bold}Results for: "${state.query}"{/bold}\n`;
|
|
142
|
+
content += divider + '\n\n';
|
|
143
|
+
|
|
144
|
+
// Table header
|
|
145
|
+
content += '{bold} │ Model │ Downloads │ Likes{/bold}\n';
|
|
146
|
+
content += divider + '\n';
|
|
147
|
+
|
|
148
|
+
// Result rows
|
|
149
|
+
for (let i = 0; i < state.results.length; i++) {
|
|
150
|
+
const result = state.results[i];
|
|
151
|
+
const isSelected = i === state.selectedIndex;
|
|
152
|
+
const indicator = isSelected ? '►' : ' ';
|
|
153
|
+
|
|
154
|
+
// Model ID (truncate if too long)
|
|
155
|
+
const maxModelLen = 51;
|
|
156
|
+
let modelId = result.modelId;
|
|
157
|
+
if (modelId.length > maxModelLen) {
|
|
158
|
+
modelId = modelId.substring(0, maxModelLen - 3) + '...';
|
|
159
|
+
}
|
|
160
|
+
modelId = modelId.padEnd(maxModelLen);
|
|
161
|
+
|
|
162
|
+
// Downloads
|
|
163
|
+
const downloads = result.downloads.toLocaleString().padStart(10);
|
|
164
|
+
|
|
165
|
+
// Likes
|
|
166
|
+
const likes = result.likes.toString().padStart(6);
|
|
167
|
+
|
|
168
|
+
// Build row content
|
|
169
|
+
let rowContent = '';
|
|
170
|
+
if (isSelected) {
|
|
171
|
+
rowContent = `{cyan-bg}{15-fg}${indicator} │ ${modelId} │ ${downloads} │ ${likes}{/15-fg}{/cyan-bg}`;
|
|
172
|
+
} else {
|
|
173
|
+
rowContent = `${indicator} │ ${modelId} │ ${downloads} │ ${likes}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
content += rowContent + '\n';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Footer
|
|
180
|
+
content += '\n' + divider + '\n';
|
|
181
|
+
content += '{gray-fg}[↑/↓] Navigate [Enter] View files [/] New search [ESC] Back [Q]uit{/gray-fg}';
|
|
182
|
+
|
|
183
|
+
contentBox.setContent(content);
|
|
184
|
+
screen.render();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Show search popup modal
|
|
188
|
+
function showSearchPopup() {
|
|
189
|
+
const searchBox = blessed.box({
|
|
190
|
+
parent: screen,
|
|
191
|
+
top: 'center',
|
|
192
|
+
left: 'center',
|
|
193
|
+
width: '60%',
|
|
194
|
+
height: 7,
|
|
195
|
+
border: { type: 'line' },
|
|
196
|
+
style: {
|
|
197
|
+
border: { fg: 'cyan' },
|
|
198
|
+
},
|
|
199
|
+
tags: true,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
searchBox.setContent('{bold}Search HuggingFace{/bold}\n\nType your query and press Enter:');
|
|
203
|
+
|
|
204
|
+
const searchInput = blessed.textbox({
|
|
205
|
+
parent: searchBox,
|
|
206
|
+
bottom: 0,
|
|
207
|
+
left: 1,
|
|
208
|
+
right: 1,
|
|
209
|
+
height: 1,
|
|
210
|
+
inputOnFocus: true,
|
|
211
|
+
style: {
|
|
212
|
+
fg: 'white',
|
|
213
|
+
bg: 'black',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Handle submit
|
|
218
|
+
searchInput.on('submit', async (value: string) => {
|
|
219
|
+
screen.remove(searchBox);
|
|
220
|
+
screen.render();
|
|
221
|
+
|
|
222
|
+
if (value && value.trim()) {
|
|
223
|
+
await executeSearch(value.trim());
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Handle cancel
|
|
228
|
+
searchInput.on('cancel', () => {
|
|
229
|
+
screen.remove(searchBox);
|
|
230
|
+
render();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
searchInput.key(['escape'], () => {
|
|
234
|
+
screen.remove(searchBox);
|
|
235
|
+
render();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
screen.append(searchBox);
|
|
239
|
+
searchInput.focus();
|
|
240
|
+
screen.render();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Execute search
|
|
244
|
+
async function executeSearch(query: string) {
|
|
245
|
+
state.query = query;
|
|
246
|
+
state.isSearching = true;
|
|
247
|
+
state.error = null;
|
|
248
|
+
state.results = [];
|
|
249
|
+
state.selectedIndex = 0;
|
|
250
|
+
render();
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const results = await modelSearch.searchModels(query, 20);
|
|
254
|
+
state.results = results;
|
|
255
|
+
state.isSearching = false;
|
|
256
|
+
render();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
state.error = error instanceof Error ? error.message : 'Unknown error';
|
|
259
|
+
state.isSearching = false;
|
|
260
|
+
render();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Load model files
|
|
265
|
+
async function loadModelFiles(modelIndex: number) {
|
|
266
|
+
const model = state.results[modelIndex];
|
|
267
|
+
state.expandedModelIndex = modelIndex;
|
|
268
|
+
state.isLoadingFiles = true;
|
|
269
|
+
state.modelFiles = [];
|
|
270
|
+
state.selectedFileIndex = 0;
|
|
271
|
+
render();
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const files = await modelSearch.getModelFiles(model.modelId);
|
|
275
|
+
state.modelFiles = files;
|
|
276
|
+
state.isLoadingFiles = false;
|
|
277
|
+
render();
|
|
278
|
+
} catch (error) {
|
|
279
|
+
state.error = error instanceof Error ? error.message : 'Unknown error';
|
|
280
|
+
state.expandedModelIndex = null;
|
|
281
|
+
state.isLoadingFiles = false;
|
|
282
|
+
render();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Download selected file
|
|
287
|
+
async function downloadFile() {
|
|
288
|
+
if (state.expandedModelIndex === null || state.modelFiles.length === 0) return;
|
|
289
|
+
|
|
290
|
+
const model = state.results[state.expandedModelIndex];
|
|
291
|
+
const filename = state.modelFiles[state.selectedFileIndex];
|
|
292
|
+
|
|
293
|
+
// Get models directory
|
|
294
|
+
const modelsDir = await stateManager.getModelsDirectory();
|
|
295
|
+
|
|
296
|
+
// Create progress modal
|
|
297
|
+
const progressBox = blessed.box({
|
|
298
|
+
parent: screen,
|
|
299
|
+
top: 'center',
|
|
300
|
+
left: 'center',
|
|
301
|
+
width: '70%',
|
|
302
|
+
height: 12,
|
|
303
|
+
border: { type: 'line' },
|
|
304
|
+
style: {
|
|
305
|
+
border: { fg: 'cyan' },
|
|
306
|
+
},
|
|
307
|
+
tags: true,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Create abort controller for cancellation
|
|
311
|
+
const abortController = new AbortController();
|
|
312
|
+
let downloadCancelled = false;
|
|
313
|
+
|
|
314
|
+
// Update progress display
|
|
315
|
+
function updateProgress(progress: DownloadProgress) {
|
|
316
|
+
if (downloadCancelled) return;
|
|
317
|
+
const percentage = progress.percentage.toFixed(1);
|
|
318
|
+
const barLength = 40;
|
|
319
|
+
const filled = Math.round((progress.percentage / 100) * barLength);
|
|
320
|
+
const empty = barLength - filled;
|
|
321
|
+
const bar = '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty));
|
|
322
|
+
|
|
323
|
+
let content = `{bold}Downloading: ${progress.filename}{/bold}\n\n`;
|
|
324
|
+
content += `[${bar}] ${percentage}%\n\n`;
|
|
325
|
+
content += `Downloaded: ${formatBytes(progress.downloaded)} / ${formatBytes(progress.total)}\n`;
|
|
326
|
+
content += `Speed: ${progress.speed}\n\n`;
|
|
327
|
+
content += '{gray-fg}Press ESC or Ctrl+C to cancel{/gray-fg}';
|
|
328
|
+
|
|
329
|
+
progressBox.setContent(content);
|
|
330
|
+
screen.render();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Temporarily unregister main handlers during download
|
|
334
|
+
screen.unkey('escape', keyHandlers.escape);
|
|
335
|
+
screen.unkey('C-c', keyHandlers.quit);
|
|
336
|
+
|
|
337
|
+
// Handle cancel
|
|
338
|
+
const cancelHandler = () => {
|
|
339
|
+
downloadCancelled = true;
|
|
340
|
+
abortController.abort(); // Actually abort the download
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Cleanup function to restore handlers
|
|
344
|
+
const cleanup = () => {
|
|
345
|
+
screen.unkey('escape', cancelHandler);
|
|
346
|
+
screen.unkey('C-c', cancelHandler);
|
|
347
|
+
screen.remove(progressBox);
|
|
348
|
+
// Re-register main handlers
|
|
349
|
+
screen.key(['escape'], keyHandlers.escape);
|
|
350
|
+
screen.key(['C-c'], keyHandlers.quit);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
screen.append(progressBox);
|
|
354
|
+
progressBox.focus();
|
|
355
|
+
screen.key(['escape', 'C-c'], cancelHandler);
|
|
356
|
+
screen.render();
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
// Start download with silent mode and abort signal
|
|
360
|
+
await modelDownloader.downloadModel(
|
|
361
|
+
model.modelId,
|
|
362
|
+
filename,
|
|
363
|
+
(progress) => {
|
|
364
|
+
if (!downloadCancelled) {
|
|
365
|
+
updateProgress(progress);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
modelsDir,
|
|
369
|
+
{ silent: true, signal: abortController.signal }
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
if (!downloadCancelled) {
|
|
373
|
+
// Show success message
|
|
374
|
+
cleanup();
|
|
375
|
+
|
|
376
|
+
const successBox = blessed.message({
|
|
377
|
+
parent: screen,
|
|
378
|
+
top: 'center',
|
|
379
|
+
left: 'center',
|
|
380
|
+
width: '60%',
|
|
381
|
+
height: 'shrink',
|
|
382
|
+
border: { type: 'line' },
|
|
383
|
+
style: {
|
|
384
|
+
border: { fg: 'green' },
|
|
385
|
+
fg: 'green',
|
|
386
|
+
},
|
|
387
|
+
tags: true,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
successBox.display(
|
|
391
|
+
`{bold}Download complete!{/bold}\n\nFile: ${filename}\nLocation: ${modelsDir}\n\nPress any key to continue`,
|
|
392
|
+
() => {
|
|
393
|
+
screen.remove(successBox);
|
|
394
|
+
render();
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
cleanup();
|
|
400
|
+
|
|
401
|
+
if (!downloadCancelled) {
|
|
402
|
+
// Show error message (not cancelled by user)
|
|
403
|
+
const errorBox = blessed.message({
|
|
404
|
+
parent: screen,
|
|
405
|
+
top: 'center',
|
|
406
|
+
left: 'center',
|
|
407
|
+
width: '60%',
|
|
408
|
+
height: 'shrink',
|
|
409
|
+
border: { type: 'line' },
|
|
410
|
+
style: {
|
|
411
|
+
border: { fg: 'red' },
|
|
412
|
+
fg: 'red',
|
|
413
|
+
},
|
|
414
|
+
tags: true,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
418
|
+
errorBox.display(`{bold}Download failed{/bold}\n\n${errorMsg}\n\nPress any key to continue`, () => {
|
|
419
|
+
screen.remove(errorBox);
|
|
420
|
+
render();
|
|
421
|
+
});
|
|
422
|
+
} else {
|
|
423
|
+
// Cancelled by user - just render
|
|
424
|
+
render();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Store key handler references for cleanup
|
|
430
|
+
const keyHandlers = {
|
|
431
|
+
up: () => {
|
|
432
|
+
if (state.expandedModelIndex !== null) {
|
|
433
|
+
// Navigating files
|
|
434
|
+
state.selectedFileIndex = Math.max(0, state.selectedFileIndex - 1);
|
|
435
|
+
} else {
|
|
436
|
+
// Navigating results
|
|
437
|
+
state.selectedIndex = Math.max(0, state.selectedIndex - 1);
|
|
438
|
+
}
|
|
439
|
+
render();
|
|
440
|
+
},
|
|
441
|
+
down: () => {
|
|
442
|
+
if (state.expandedModelIndex !== null) {
|
|
443
|
+
// Navigating files
|
|
444
|
+
state.selectedFileIndex = Math.min(state.modelFiles.length - 1, state.selectedFileIndex + 1);
|
|
445
|
+
} else {
|
|
446
|
+
// Navigating results
|
|
447
|
+
state.selectedIndex = Math.min(state.results.length - 1, state.selectedIndex + 1);
|
|
448
|
+
}
|
|
449
|
+
render();
|
|
450
|
+
},
|
|
451
|
+
enter: () => {
|
|
452
|
+
if (state.expandedModelIndex !== null) {
|
|
453
|
+
// Download selected file
|
|
454
|
+
downloadFile();
|
|
455
|
+
} else if (state.results.length > 0) {
|
|
456
|
+
// Expand selected model to show files
|
|
457
|
+
loadModelFiles(state.selectedIndex);
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
search: () => {
|
|
461
|
+
if (state.expandedModelIndex !== null) return; // Don't allow search when viewing files
|
|
462
|
+
showSearchPopup();
|
|
463
|
+
},
|
|
464
|
+
escape: () => {
|
|
465
|
+
if (state.expandedModelIndex !== null) {
|
|
466
|
+
// Go back to results list
|
|
467
|
+
state.expandedModelIndex = null;
|
|
468
|
+
state.modelFiles = [];
|
|
469
|
+
state.selectedFileIndex = 0;
|
|
470
|
+
render();
|
|
471
|
+
} else {
|
|
472
|
+
// Go back to models view
|
|
473
|
+
unregisterHandlers();
|
|
474
|
+
screen.remove(contentBox);
|
|
475
|
+
onBack();
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
quit: () => {
|
|
479
|
+
screen.destroy();
|
|
480
|
+
process.exit(0);
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// Unregister all keyboard handlers
|
|
485
|
+
function unregisterHandlers() {
|
|
486
|
+
screen.unkey('up', keyHandlers.up);
|
|
487
|
+
screen.unkey('k', keyHandlers.up);
|
|
488
|
+
screen.unkey('down', keyHandlers.down);
|
|
489
|
+
screen.unkey('j', keyHandlers.down);
|
|
490
|
+
screen.unkey('enter', keyHandlers.enter);
|
|
491
|
+
screen.unkey('/', keyHandlers.search);
|
|
492
|
+
screen.unkey('escape', keyHandlers.escape);
|
|
493
|
+
screen.unkey('q', keyHandlers.quit);
|
|
494
|
+
screen.unkey('Q', keyHandlers.quit);
|
|
495
|
+
screen.unkey('C-c', keyHandlers.quit);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Register key handlers
|
|
499
|
+
screen.key(['up', 'k'], keyHandlers.up);
|
|
500
|
+
screen.key(['down', 'j'], keyHandlers.down);
|
|
501
|
+
screen.key(['enter'], keyHandlers.enter);
|
|
502
|
+
screen.key(['/'], keyHandlers.search);
|
|
503
|
+
screen.key(['escape'], keyHandlers.escape);
|
|
504
|
+
screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
|
|
505
|
+
|
|
506
|
+
// Initial render
|
|
507
|
+
render();
|
|
508
|
+
|
|
509
|
+
// Auto-open search popup on load
|
|
510
|
+
showSearchPopup();
|
|
511
|
+
}
|