@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.
Files changed (136) hide show
  1. package/README.md +294 -168
  2. package/dist/cli.js +35 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/launch/claude.d.ts +6 -0
  5. package/dist/commands/launch/claude.d.ts.map +1 -0
  6. package/dist/commands/launch/claude.js +277 -0
  7. package/dist/commands/launch/claude.js.map +1 -0
  8. package/dist/lib/integration-checker.d.ts +26 -0
  9. package/dist/lib/integration-checker.d.ts.map +1 -0
  10. package/dist/lib/integration-checker.js +77 -0
  11. package/dist/lib/integration-checker.js.map +1 -0
  12. package/dist/lib/router-manager.d.ts +4 -0
  13. package/dist/lib/router-manager.d.ts.map +1 -1
  14. package/dist/lib/router-manager.js +10 -0
  15. package/dist/lib/router-manager.js.map +1 -1
  16. package/dist/lib/router-server.d.ts +13 -0
  17. package/dist/lib/router-server.d.ts.map +1 -1
  18. package/dist/lib/router-server.js +267 -7
  19. package/dist/lib/router-server.js.map +1 -1
  20. package/dist/types/integration-config.d.ts +28 -0
  21. package/dist/types/integration-config.d.ts.map +1 -0
  22. package/dist/types/integration-config.js +3 -0
  23. package/dist/types/integration-config.js.map +1 -0
  24. package/package.json +10 -2
  25. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  26. package/web/dist/assets/index-CVmonw3T.js +17 -0
  27. package/web/{index.html → dist/index.html} +2 -1
  28. package/.versionrc.json +0 -16
  29. package/CHANGELOG.md +0 -213
  30. package/docs/images/.gitkeep +0 -1
  31. package/docs/images/web-ui-servers.png +0 -0
  32. package/src/cli.ts +0 -523
  33. package/src/commands/admin/config.ts +0 -121
  34. package/src/commands/admin/logs.ts +0 -91
  35. package/src/commands/admin/restart.ts +0 -26
  36. package/src/commands/admin/start.ts +0 -27
  37. package/src/commands/admin/status.ts +0 -84
  38. package/src/commands/admin/stop.ts +0 -16
  39. package/src/commands/config-global.ts +0 -38
  40. package/src/commands/config.ts +0 -323
  41. package/src/commands/create.ts +0 -183
  42. package/src/commands/delete.ts +0 -74
  43. package/src/commands/list.ts +0 -37
  44. package/src/commands/logs-all.ts +0 -251
  45. package/src/commands/logs.ts +0 -345
  46. package/src/commands/monitor.ts +0 -110
  47. package/src/commands/ps.ts +0 -84
  48. package/src/commands/pull.ts +0 -44
  49. package/src/commands/rm.ts +0 -107
  50. package/src/commands/router/config.ts +0 -116
  51. package/src/commands/router/logs.ts +0 -256
  52. package/src/commands/router/restart.ts +0 -36
  53. package/src/commands/router/start.ts +0 -60
  54. package/src/commands/router/status.ts +0 -119
  55. package/src/commands/router/stop.ts +0 -33
  56. package/src/commands/run.ts +0 -233
  57. package/src/commands/search.ts +0 -107
  58. package/src/commands/server-show.ts +0 -161
  59. package/src/commands/show.ts +0 -207
  60. package/src/commands/start.ts +0 -101
  61. package/src/commands/stop.ts +0 -39
  62. package/src/commands/tui.ts +0 -25
  63. package/src/lib/admin-manager.ts +0 -435
  64. package/src/lib/admin-server.ts +0 -1243
  65. package/src/lib/config-generator.ts +0 -130
  66. package/src/lib/download-job-manager.ts +0 -213
  67. package/src/lib/history-manager.ts +0 -172
  68. package/src/lib/launchctl-manager.ts +0 -225
  69. package/src/lib/metrics-aggregator.ts +0 -257
  70. package/src/lib/model-downloader.ts +0 -328
  71. package/src/lib/model-scanner.ts +0 -157
  72. package/src/lib/model-search.ts +0 -114
  73. package/src/lib/models-dir-setup.ts +0 -46
  74. package/src/lib/port-manager.ts +0 -80
  75. package/src/lib/router-logger.ts +0 -201
  76. package/src/lib/router-manager.ts +0 -414
  77. package/src/lib/router-server.ts +0 -538
  78. package/src/lib/state-manager.ts +0 -206
  79. package/src/lib/status-checker.ts +0 -113
  80. package/src/lib/system-collector.ts +0 -315
  81. package/src/tui/ConfigApp.ts +0 -1085
  82. package/src/tui/HistoricalMonitorApp.ts +0 -587
  83. package/src/tui/ModelsApp.ts +0 -368
  84. package/src/tui/MonitorApp.ts +0 -386
  85. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  86. package/src/tui/RootNavigator.ts +0 -74
  87. package/src/tui/SearchApp.ts +0 -511
  88. package/src/tui/SplashScreen.ts +0 -149
  89. package/src/types/admin-config.ts +0 -25
  90. package/src/types/global-config.ts +0 -26
  91. package/src/types/history-types.ts +0 -39
  92. package/src/types/model-info.ts +0 -8
  93. package/src/types/monitor-types.ts +0 -162
  94. package/src/types/router-config.ts +0 -25
  95. package/src/types/server-config.ts +0 -46
  96. package/src/utils/downsample-utils.ts +0 -128
  97. package/src/utils/file-utils.ts +0 -146
  98. package/src/utils/format-utils.ts +0 -98
  99. package/src/utils/log-parser.ts +0 -284
  100. package/src/utils/log-utils.ts +0 -178
  101. package/src/utils/process-utils.ts +0 -316
  102. package/src/utils/prompt-utils.ts +0 -47
  103. package/test-load.sh +0 -100
  104. package/tsconfig.json +0 -20
  105. package/web/eslint.config.js +0 -23
  106. package/web/llamacpp-web-dist.tar.gz +0 -0
  107. package/web/package-lock.json +0 -4017
  108. package/web/package.json +0 -38
  109. package/web/postcss.config.js +0 -6
  110. package/web/src/App.css +0 -42
  111. package/web/src/App.tsx +0 -86
  112. package/web/src/assets/react.svg +0 -1
  113. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  114. package/web/src/components/CreateServerModal.tsx +0 -372
  115. package/web/src/components/DownloadProgress.tsx +0 -123
  116. package/web/src/components/Nav.tsx +0 -89
  117. package/web/src/components/RouterConfigModal.tsx +0 -240
  118. package/web/src/components/SearchModal.tsx +0 -306
  119. package/web/src/components/ServerConfigModal.tsx +0 -291
  120. package/web/src/hooks/useApi.ts +0 -259
  121. package/web/src/index.css +0 -42
  122. package/web/src/lib/api.ts +0 -226
  123. package/web/src/main.tsx +0 -10
  124. package/web/src/pages/Dashboard.tsx +0 -103
  125. package/web/src/pages/Models.tsx +0 -258
  126. package/web/src/pages/Router.tsx +0 -270
  127. package/web/src/pages/RouterLogs.tsx +0 -201
  128. package/web/src/pages/ServerLogs.tsx +0 -553
  129. package/web/src/pages/Servers.tsx +0 -358
  130. package/web/src/types/api.ts +0 -140
  131. package/web/tailwind.config.js +0 -31
  132. package/web/tsconfig.app.json +0 -28
  133. package/web/tsconfig.json +0 -7
  134. package/web/tsconfig.node.json +0 -26
  135. package/web/vite.config.ts +0 -25
  136. /package/web/{public → dist}/vite.svg +0 -0
@@ -1,74 +0,0 @@
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
- }
@@ -1,511 +0,0 @@
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
- }