openhorse 0.1.23 → 0.2.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 (63) hide show
  1. package/dist/cli.d.ts +1 -3
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +125 -1136
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/index.js +1 -1
  6. package/dist/commands/index.js.map +1 -1
  7. package/dist/ink-ui/App.d.ts +6 -0
  8. package/dist/ink-ui/App.d.ts.map +1 -0
  9. package/dist/ink-ui/App.js +12 -0
  10. package/dist/ink-ui/App.js.map +1 -0
  11. package/dist/ink-ui/components/Markdown.d.ts +9 -0
  12. package/dist/ink-ui/components/Markdown.d.ts.map +1 -0
  13. package/dist/ink-ui/components/Markdown.js +188 -0
  14. package/dist/ink-ui/components/Markdown.js.map +1 -0
  15. package/dist/ink-ui/components/PixelHorseBanner.d.ts +8 -0
  16. package/dist/ink-ui/components/PixelHorseBanner.d.ts.map +1 -0
  17. package/dist/ink-ui/components/PixelHorseBanner.js +97 -0
  18. package/dist/ink-ui/components/PixelHorseBanner.js.map +1 -0
  19. package/dist/ink-ui/components/PromptInput.d.ts +9 -0
  20. package/dist/ink-ui/components/PromptInput.d.ts.map +1 -0
  21. package/dist/ink-ui/components/PromptInput.js +24 -0
  22. package/dist/ink-ui/components/PromptInput.js.map +1 -0
  23. package/dist/ink-ui/components/SelectList.d.ts +15 -0
  24. package/dist/ink-ui/components/SelectList.d.ts.map +1 -0
  25. package/dist/ink-ui/components/SelectList.js +39 -0
  26. package/dist/ink-ui/components/SelectList.js.map +1 -0
  27. package/dist/ink-ui/components/StatusLine.d.ts +9 -0
  28. package/dist/ink-ui/components/StatusLine.d.ts.map +1 -0
  29. package/dist/ink-ui/components/StatusLine.js +71 -0
  30. package/dist/ink-ui/components/StatusLine.js.map +1 -0
  31. package/dist/ink-ui/components/TerminalCursor.d.ts +13 -0
  32. package/dist/ink-ui/components/TerminalCursor.d.ts.map +1 -0
  33. package/dist/ink-ui/components/TerminalCursor.js +74 -0
  34. package/dist/ink-ui/components/TerminalCursor.js.map +1 -0
  35. package/dist/ink-ui/components/Transcript.d.ts +8 -0
  36. package/dist/ink-ui/components/Transcript.d.ts.map +1 -0
  37. package/dist/ink-ui/components/Transcript.js +29 -0
  38. package/dist/ink-ui/components/Transcript.js.map +1 -0
  39. package/dist/ink-ui/controllers/chat-controller.d.ts +15 -0
  40. package/dist/ink-ui/controllers/chat-controller.d.ts.map +1 -0
  41. package/dist/ink-ui/controllers/chat-controller.js +410 -0
  42. package/dist/ink-ui/controllers/chat-controller.js.map +1 -0
  43. package/dist/ink-ui/launch.d.ts +3 -0
  44. package/dist/ink-ui/launch.d.ts.map +1 -0
  45. package/dist/ink-ui/launch.js +14 -0
  46. package/dist/ink-ui/launch.js.map +1 -0
  47. package/dist/ink-ui/runtime/prompt-layout.d.ts +11 -0
  48. package/dist/ink-ui/runtime/prompt-layout.d.ts.map +1 -0
  49. package/dist/ink-ui/runtime/prompt-layout.js +51 -0
  50. package/dist/ink-ui/runtime/prompt-layout.js.map +1 -0
  51. package/dist/ink-ui/screens/ReplScreen.d.ts +14 -0
  52. package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -0
  53. package/dist/ink-ui/screens/ReplScreen.js +470 -0
  54. package/dist/ink-ui/screens/ReplScreen.js.map +1 -0
  55. package/dist/ink-ui/types.d.ts +43 -0
  56. package/dist/ink-ui/types.d.ts.map +1 -0
  57. package/dist/ink-ui/types.js +3 -0
  58. package/dist/ink-ui/types.js.map +1 -0
  59. package/dist/services/config.js +2 -2
  60. package/dist/services/config.js.map +1 -1
  61. package/dist/services/global-config.d.ts +1 -1
  62. package/dist/services/global-config.d.ts.map +1 -1
  63. package/package.json +5 -1
package/dist/cli.js CHANGED
@@ -1,8 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * openhorse - 命令行交互入口
4
- *
5
- * 简洁版 REPL,使用 readline + keypress 事件处理
3
+ * openhorse - Ink/React CLI entry
6
4
  */
7
5
  var __importDefault = (this && this.__importDefault) || function (mod) {
8
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -10,79 +8,53 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
8
  Object.defineProperty(exports, "__esModule", { value: true });
11
9
  require("dotenv/config");
12
10
  const chalk_1 = __importDefault(require("chalk"));
13
- const readline_1 = __importDefault(require("readline"));
14
11
  const fs_1 = require("fs");
15
12
  const path_1 = require("path");
16
13
  const init_1 = require("./init");
17
14
  const llm_1 = require("./services/llm");
18
- const tools_1 = require("./tools");
19
- const mcp_1 = require("./tools/mcp");
20
15
  const config_1 = require("./services/config");
21
16
  const config_dir_1 = require("./services/config-dir");
22
17
  const global_config_1 = require("./services/global-config");
23
- const model_context_1 = require("./services/model-context");
24
18
  const session_storage_1 = require("./services/session-storage");
25
19
  const storage_1 = require("./memory/storage");
26
20
  const skills_1 = require("./skills");
27
21
  const framework_1 = require("./framework");
28
- const commands_1 = require("./commands");
29
- const parser_1 = require("./commands/parser");
30
- const types_1 = require("./commands/types");
31
- const box_1 = require("./ui/box");
32
- const command_panel_1 = require("./ui/command-panel");
33
- const multiline_input_1 = require("./ui/multiline-input");
34
- const file_completion_1 = require("./ui/file-completion");
35
- const status_bar_1 = require("./ui/status-bar");
36
- const user_input_1 = require("./ui/user-input");
37
- const ui_v2_1 = require("./ui-v2");
38
- const turn_controller_1 = require("./runtime/turn-controller");
39
- // Get version from package.json
22
+ const tools_1 = require("./tools");
23
+ const mcp_1 = require("./tools/mcp");
24
+ const model_context_1 = require("./services/model-context");
25
+ const launch_1 = require("./ink-ui/launch");
26
+ const BRAND = chalk_1.default.hex('#FF6B35');
27
+ const ACCENT = chalk_1.default.hex('#00D4AA');
28
+ const DIM = chalk_1.default.dim;
29
+ const ERROR = chalk_1.default.red;
30
+ const WARN = chalk_1.default.yellow;
40
31
  const VERSION = (() => {
41
32
  try {
42
33
  const pkgPath = (0, path_1.join)(__dirname, '..', 'package.json');
43
34
  const pkg = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
44
- return pkg.version || '0.1.14';
35
+ return pkg.version || '0.2.0';
45
36
  }
46
37
  catch {
47
- return '0.1.14';
38
+ return '0.2.0';
48
39
  }
49
40
  })();
50
- // ============================================================================
51
- // 颜色常量
52
- // ============================================================================
53
- const BRAND = chalk_1.default.hex('#FF6B35');
54
- const ACCENT = chalk_1.default.hex('#00D4AA');
55
- const DIM = chalk_1.default.dim;
56
- const ERROR = chalk_1.default.red;
57
- const WARN = chalk_1.default.yellow;
58
- const SUCCESS = chalk_1.default.green;
59
- // ============================================================================
60
- // CLI Help
61
- // ============================================================================
62
41
  function showCliHelp() {
63
42
  console.log();
64
43
  console.log(BRAND('openhorse') + DIM(` v${VERSION}`));
65
44
  console.log(DIM(' Universal Agent Harness Framework'));
66
45
  console.log();
67
46
  console.log(ACCENT('Usage:'));
68
- console.log(' openhorse Start interactive REPL');
69
- console.log(' openhorse --help Show this help message');
70
- console.log(' openhorse --version Show version');
71
- console.log(' openhorse --ui legacy Use the legacy terminal renderer');
47
+ console.log(' openhorse Start the Ink/React interactive UI');
48
+ console.log(' openhorse --help Show this help message');
49
+ console.log(' openhorse --version Show version');
50
+ console.log(' openhorse --ui ink Start the Ink UI explicitly');
72
51
  console.log();
73
52
  console.log(ACCENT('Options:'));
74
53
  console.log(' -h, --help Show help');
75
54
  console.log(' -v, --version Show version');
76
- console.log(' --ui <mode> UI renderer: legacy | v2');
77
- console.log();
78
- console.log(ACCENT('Interactive Commands:'));
79
- console.log(' /help Show available slash commands');
80
- console.log(' /status Show system status');
81
- console.log(' /model [name] Show or change model');
82
- console.log(' /chat <msg> Send message to LLM');
83
- console.log(' /exit Exit the REPL');
55
+ console.log(' --ui <mode> UI renderer: ink');
84
56
  console.log();
85
- console.log(DIM('Type /help in REPL for full command list.'));
57
+ console.log(DIM('Legacy readline/v2 renderers were removed in v0.2.0.'));
86
58
  console.log();
87
59
  }
88
60
  function parseCliUIRenderer(args) {
@@ -95,1131 +67,148 @@ function parseCliUIRenderer(args) {
95
67
  : undefined;
96
68
  if (value === undefined)
97
69
  continue;
98
- if (value === 'legacy' || value === 'v2')
99
- return value;
70
+ if (value === 'ink')
71
+ return 'ink';
72
+ if (value === 'legacy' || value === 'v2') {
73
+ console.log(WARN(`Renderer "${value}" was removed in v0.2.0; starting Ink UI instead.`));
74
+ return 'ink';
75
+ }
100
76
  console.error(ERROR(`Invalid --ui value: ${value}`));
101
- console.error(DIM('Expected one of: legacy, v2'));
77
+ console.error(DIM('Expected: ink'));
102
78
  process.exit(1);
103
79
  }
104
- return undefined;
105
- }
106
- // ============================================================================
107
- // 全局状态
108
- // ============================================================================
109
- let llm = null;
110
- let store;
111
- let currentSession = null;
112
- let runtime;
113
- const turnController = new turn_controller_1.TurnController();
114
- let isShuttingDown = false;
115
- // 输入状态
116
- let currentInput = '';
117
- let inputHistory = [];
118
- let historyIndex = -1;
119
- let historyMode = 'none';
120
- let searchQuery = '';
121
- let resumePickerState = null;
122
- let resumePickerLines = [];
123
- let resumePickerReservedHeight = 0;
124
- const DEFAULT_RESUME_PICKER_VISIBLE_ITEMS = 10;
125
- let shortcutsOverlayVisible = false;
126
- let shortcutsOverlayLines = [];
127
- let shortcutsOverlayReservedHeight = 0;
128
- function ensureCurrentSession() {
129
- if (!currentSession) {
130
- currentSession = (0, session_storage_1.createSession)(process.cwd(), store.getSnapshot().currentModel || store.getSnapshot().config.model);
131
- (0, global_config_1.incrementSessionCount)();
132
- }
133
- return currentSession;
134
- }
135
- function setCurrentSession(session) {
136
- currentSession = session;
137
- }
138
- function getCurrentSession() {
139
- return currentSession;
140
- }
141
- function echoSubmittedInput(input) {
142
- (0, command_panel_1.clearRenderedInput)();
143
- const rendered = isV2UI()
144
- ? (0, user_input_1.renderUserInputEchoFrame)(input)
145
- : (0, user_input_1.renderUserInputEcho)(input);
146
- (0, command_panel_1.writeLinePreservingInput)(rendered);
147
- if (isV2UI()) {
148
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
149
- }
150
- }
151
- function isExitCommandInput(input) {
152
- const parsed = (0, parser_1.parseInput)(input.trim());
153
- if (!parsed.isCommand)
154
- return false;
155
- const cmd = (0, commands_1.findCommand)(parsed.name);
156
- return cmd?.name === 'exit';
157
- }
158
- function submitInput(input) {
159
- const submittedInput = input;
160
- currentInput = '';
161
- echoSubmittedInput(submittedInput);
162
- (0, global_config_1.addToInputHistory)(submittedInput);
163
- inputHistory = (0, global_config_1.getInputHistory)();
164
- if (turnController.hasActiveTurn()) {
165
- const text = submittedInput.trim();
166
- const parsed = (0, parser_1.parseInput)(text);
167
- if (isExitCommandInput(text)) {
168
- void shutdownCli();
169
- return;
170
- }
171
- if (parsed.isCommand) {
172
- console.log(DIM('Command ignored while agent is running. Press Ctrl+C to interrupt first.'));
173
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
174
- return;
175
- }
176
- if (text) {
177
- turnController.clearExitIntent();
178
- turnController.requestRevision(submittedInput);
179
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
180
- }
181
- return;
182
- }
183
- void runInputTurn(submittedInput);
184
- }
185
- async function runInputTurn(input) {
186
- let nextInput = input;
187
- while (nextInput) {
188
- const turn = turnController.beginTurn(nextInput);
189
- store.setProcessing(true);
190
- try {
191
- await handleInput(nextInput, turn.abortSignal, { redrawPrompt: false, updateStatus: false });
192
- }
193
- catch (err) {
194
- console.log(ERROR(`Input error: ${err.message || String(err)}`));
195
- }
196
- finally {
197
- const revision = turnController.finishTurn(turn.id);
198
- store.setProcessing(false);
199
- if (revision && revision.trim()) {
200
- (0, command_panel_1.writeLinePreservingInput)(DIM('Interrupted. Restarting with latest instruction...'));
201
- nextInput = revision;
202
- }
203
- else {
204
- nextInput = undefined;
205
- }
206
- }
207
- }
208
- updateStatusBar();
209
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
210
- }
211
- function parseKey(char, key) {
212
- const sequence = key?.sequence || char || '';
213
- if (sequence === '\x1b[13;2u') {
214
- return { name: 'enter', ctrl: false, shift: true, meta: false, sequence };
215
- }
216
- if (sequence === '\x1b[13;3u') {
217
- return { name: 'enter', ctrl: false, shift: false, meta: true, sequence };
218
- }
219
- if (!key) {
220
- // 手动解析
221
- if (char === '\r' || char === '\n')
222
- return { name: 'enter', ctrl: false, shift: false, meta: false, sequence: char || '' };
223
- if (char === '\x1b')
224
- return { name: 'escape', ctrl: false, shift: false, meta: false, sequence: char };
225
- if (char === '\t')
226
- return { name: 'tab', ctrl: false, shift: false, meta: false, sequence: char };
227
- if (char === '\x7f' || char === '\b')
228
- return { name: 'backspace', ctrl: false, shift: false, meta: false, sequence: char };
229
- return { name: char || '', ctrl: false, shift: false, meta: false, sequence: char || '' };
230
- }
231
- // 统一 "return" 和 "enter"
232
- if (key.name === 'return') {
233
- key.name = 'enter';
234
- }
235
- if (key.name === 'enter' && (key.shift || key.meta)) {
236
- return key;
237
- }
238
- // 如果 key.name 为空,使用 char 作为 name
239
- if (!key.name && char) {
240
- key.name = char;
241
- }
242
- return key;
243
- }
244
- function handleKeypress(char, key) {
245
- const k = parseKey(char, key);
246
- if (k.ctrl && k.name === 'l') {
247
- clearTerminalView();
248
- return;
249
- }
250
- if (k.ctrl && k.name === 'c') {
251
- void handleCtrlC();
252
- return;
253
- }
254
- // 命令面板模式
255
- if ((0, command_panel_1.isPanelVisible)()) {
256
- handlePanelKeypress(k, char);
257
- return;
258
- }
259
- // 文件补全模式
260
- if ((0, file_completion_1.isFileCompletionVisible)()) {
261
- handleFileCompletionKeypress(k, char);
262
- return;
263
- }
264
- // Resume session picker mode
265
- if (resumePickerState?.visible) {
266
- handleResumePickerKeypress(k, char);
267
- return;
268
- }
269
- if (shortcutsOverlayVisible) {
270
- handleShortcutsOverlayKeypress(k, char);
271
- return;
272
- }
273
- // 历史导航模式
274
- if (historyMode !== 'none') {
275
- handleHistoryKeypress(k, char);
276
- return;
277
- }
278
- // 正常输入模式
279
- handleNormalKeypress(k, char);
280
- }
281
- function handlePanelKeypress(k, char) {
282
- switch (k.name) {
283
- case 'up':
284
- (0, command_panel_1.navigatePanel)('up');
285
- break;
286
- case 'down':
287
- (0, command_panel_1.navigatePanel)('down');
288
- break;
289
- case 'tab':
290
- const completedCmd = (0, command_panel_1.completeSelectedCommand)();
291
- if (completedCmd) {
292
- currentInput = completedCmd;
293
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
294
- }
295
- break;
296
- case 'enter':
297
- const cmd = (0, command_panel_1.selectCommand)();
298
- if (cmd) {
299
- submitInput(cmd);
300
- (0, command_panel_1.clearPendingCommand)();
301
- }
302
- break;
303
- case 'escape':
304
- (0, command_panel_1.hideCommandPanel)();
305
- currentInput = '';
306
- (0, command_panel_1.redrawInputWithPrompt)('');
307
- break;
308
- case 'backspace':
309
- currentInput = currentInput.slice(0, -1);
310
- if (currentInput.length <= 1) {
311
- (0, command_panel_1.hideCommandPanel)();
312
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
313
- }
314
- else {
315
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
316
- (0, command_panel_1.updatePanelFilter)(currentInput.slice(1));
317
- }
318
- break;
319
- default:
320
- // 添加字符到过滤
321
- if (char && char.length === 1 && !k.ctrl) {
322
- currentInput += char;
323
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
324
- (0, command_panel_1.updatePanelFilter)(currentInput.slice(1));
325
- }
326
- }
327
- }
328
- function handleHistoryKeypress(k, char) {
329
- if (historyMode === 'search') {
330
- // 搜索模式:输入搜索词
331
- switch (k.name) {
332
- case 'enter':
333
- if (inputHistory.length > 0 && historyIndex >= 0) {
334
- currentInput = inputHistory[historyIndex]?.content || '';
335
- }
336
- historyMode = 'none';
337
- searchQuery = '';
338
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
339
- break;
340
- case 'escape':
341
- historyMode = 'none';
342
- searchQuery = '';
343
- currentInput = '';
344
- (0, command_panel_1.redrawInputWithPrompt)('');
345
- break;
346
- case 'backspace':
347
- searchQuery = searchQuery.slice(0, -1);
348
- updateHistorySearch();
349
- break;
350
- default:
351
- if (char && char.length === 1 && !k.ctrl) {
352
- searchQuery += char;
353
- updateHistorySearch();
354
- }
355
- }
356
- }
357
- else {
358
- // 导航模式
359
- switch (k.name) {
360
- case 'up':
361
- historyIndex = Math.min(inputHistory.length - 1, historyIndex + 1);
362
- currentInput = inputHistory[historyIndex]?.content || '';
363
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
364
- break;
365
- case 'down':
366
- historyIndex = Math.max(-1, historyIndex - 1);
367
- if (historyIndex < 0) {
368
- currentInput = '';
369
- historyMode = 'none';
370
- }
371
- else {
372
- currentInput = inputHistory[historyIndex]?.content || '';
373
- }
374
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
375
- break;
376
- case 'enter':
377
- historyMode = 'none';
378
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
379
- break;
380
- case 'escape':
381
- historyMode = 'none';
382
- currentInput = '';
383
- historyIndex = -1;
384
- (0, command_panel_1.redrawInputWithPrompt)('');
385
- break;
386
- }
387
- }
388
- }
389
- function handleFileCompletionKeypress(k, char) {
390
- switch (k.name) {
391
- case 'up':
392
- (0, file_completion_1.navigateFiles)('up');
393
- break;
394
- case 'down':
395
- (0, file_completion_1.navigateFiles)('down');
396
- break;
397
- case 'tab':
398
- const completedPath = (0, file_completion_1.completeFile)();
399
- if (completedPath) {
400
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + completedPath;
401
- (0, file_completion_1.hideFileCompletion)();
402
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
403
- }
404
- else {
405
- // 目录:继续显示面板,更新 query
406
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
407
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
408
- }
409
- break;
410
- case 'enter':
411
- const selectedPath = (0, file_completion_1.selectFile)();
412
- if (selectedPath) {
413
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + selectedPath;
414
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
415
- }
416
- break;
417
- case 'escape':
418
- (0, file_completion_1.hideFileCompletion)();
419
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
420
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
421
- break;
422
- case 'backspace':
423
- const query = (0, file_completion_1.getFileQuery)();
424
- if (query.length > 0) {
425
- (0, file_completion_1.updateFileQuery)(query.slice(0, -1));
426
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
427
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
428
- }
429
- else {
430
- (0, file_completion_1.hideFileCompletion)();
431
- currentInput = (0, file_completion_1.getBaseInput)();
432
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
433
- }
434
- break;
435
- default:
436
- // 添加字符到路径查询
437
- if (char && char.length === 1 && !k.ctrl) {
438
- const newQuery = (0, file_completion_1.getFileQuery)() + char;
439
- (0, file_completion_1.updateFileQuery)(newQuery);
440
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + newQuery;
441
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
442
- }
443
- }
444
- }
445
- function handleResumePickerKeypress(k, char) {
446
- if (!resumePickerState?.visible)
447
- return;
448
- const pageSize = resumePickerState.maxVisibleItems ?? DEFAULT_RESUME_PICKER_VISIBLE_ITEMS;
449
- switch (k.name) {
450
- case 'up':
451
- resumePickerState.selectedIndex = Math.max(0, resumePickerState.selectedIndex - 1);
452
- renderResumePicker();
453
- break;
454
- case 'down':
455
- resumePickerState.selectedIndex = Math.min(resumePickerState.sessions.length - 1, resumePickerState.selectedIndex + 1);
456
- renderResumePicker();
457
- break;
458
- case 'pageup':
459
- resumePickerState.selectedIndex = Math.max(0, resumePickerState.selectedIndex - pageSize);
460
- renderResumePicker();
461
- break;
462
- case 'pagedown':
463
- resumePickerState.selectedIndex = Math.min(resumePickerState.sessions.length - 1, resumePickerState.selectedIndex + pageSize);
464
- renderResumePicker();
465
- break;
466
- case 'home':
467
- resumePickerState.selectedIndex = 0;
468
- renderResumePicker();
469
- break;
470
- case 'end':
471
- resumePickerState.selectedIndex = Math.max(0, resumePickerState.sessions.length - 1);
472
- renderResumePicker();
473
- break;
474
- case 'enter':
475
- restoreSelectedResumeSession();
476
- break;
477
- case 'escape':
478
- hideResumePicker();
479
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
480
- break;
481
- default:
482
- if (char && /^[0-9]$/.test(char)) {
483
- const visibleOffset = char === '0' ? 9 : Number(char) - 1;
484
- const windowStart = (0, ui_v2_1.getSessionWindowStart)(resumePickerState.selectedIndex, resumePickerState.sessions.length, pageSize);
485
- const index = windowStart + visibleOffset;
486
- if (visibleOffset >= 0 && visibleOffset < pageSize && index < resumePickerState.sessions.length) {
487
- resumePickerState.selectedIndex = index;
488
- restoreSelectedResumeSession();
489
- }
490
- }
491
- }
492
- }
493
- function showResumePicker(options) {
494
- resumePickerState = {
495
- visible: true,
496
- sessions: options.sessions,
497
- selectedIndex: 0,
498
- title: options.title,
499
- showProject: options.showProject,
500
- moreCount: options.moreCount,
501
- allProjects: options.allProjects,
502
- maxVisibleItems: options.maxVisibleItems ?? DEFAULT_RESUME_PICKER_VISIBLE_ITEMS,
503
- };
504
- renderResumePicker();
505
- }
506
- function renderResumePicker() {
507
- if (!resumePickerState?.visible)
508
- return;
509
- clearResumePicker({ release: false });
510
- const lines = (0, ui_v2_1.renderSessionPicker)({
511
- title: resumePickerState.title,
512
- sessions: resumePickerState.sessions,
513
- selectedIndex: resumePickerState.selectedIndex,
514
- maxVisibleItems: resumePickerState.maxVisibleItems ?? DEFAULT_RESUME_PICKER_VISIBLE_ITEMS,
515
- width: process.stdout.columns || 80,
516
- showProject: resumePickerState.showProject,
517
- moreCount: resumePickerState.moreCount,
518
- footer: ' ↑↓ Scroll PgUp/PgDn Enter Resume 1-9/0 Quick Esc Cancel',
519
- theme: {
520
- accent: ACCENT,
521
- dim: DIM,
522
- selected: text => chalk_1.default.bgHex('#1E293B').hex('#E2E8F0')(text),
523
- },
524
- });
525
- const offset = getResumePickerOffsetRows();
526
- reserveResumePickerSpace(lines.length + offset);
527
- resumePickerLines = lines;
528
- process.stdout.write('\x1b7');
529
- if (offset > 0) {
530
- process.stdout.write(`\x1b[${offset}B\r`);
531
- }
532
- process.stdout.write('\x1b[J');
533
- for (let index = 0; index < lines.length; index++) {
534
- if (index > 0 || offset === 0) {
535
- process.stdout.write('\n');
536
- }
537
- process.stdout.write('\r' + lines[index]);
538
- }
539
- process.stdout.write('\x1b8');
540
- }
541
- function hideResumePicker() {
542
- clearResumePicker({ release: true });
543
- resumePickerState = null;
544
- }
545
- function clearResumePicker(options = {}) {
546
- const height = Math.max(resumePickerReservedHeight, resumePickerLines.length);
547
- if (height <= 0)
548
- return;
549
- process.stdout.write('\x1b7');
550
- const offset = getResumePickerOffsetRows();
551
- if (offset > 0) {
552
- process.stdout.write(`\x1b[${offset}B\r`);
553
- }
554
- process.stdout.write('\x1b[J');
555
- process.stdout.write('\x1b8');
556
- resumePickerLines = [];
557
- if (options.release) {
558
- resumePickerReservedHeight = 0;
559
- }
560
- }
561
- function reserveResumePickerSpace(requiredHeight) {
562
- if (requiredHeight <= resumePickerReservedHeight)
563
- return;
564
- const extraLines = requiredHeight - resumePickerReservedHeight;
565
- process.stdout.write('\n'.repeat(extraLines));
566
- process.stdout.write(`\x1b[${extraLines}A`);
567
- resumePickerReservedHeight = requiredHeight;
568
- }
569
- function getResumePickerOffsetRows() {
570
- return isV2UI() ? 2 : 1;
571
- }
572
- function restoreSelectedResumeSession() {
573
- if (!resumePickerState?.visible)
574
- return;
575
- const session = resumePickerState.sessions[resumePickerState.selectedIndex];
576
- const suffix = resumePickerState.allProjects ? ' --all' : '';
577
- hideResumePicker();
578
- currentInput = '';
579
- (0, command_panel_1.clearRenderedInput)();
580
- runInputTurn(`/resume ${session.id}${suffix}`).catch(err => {
581
- console.log(ERROR(`Resume error: ${err.message || String(err)}`));
582
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
583
- });
584
- }
585
- function handleShortcutsOverlayKeypress(k, char) {
586
- if (k.name === 'escape' || k.name === 'enter' || k.name === '?') {
587
- hideShortcutsOverlay();
588
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
589
- return;
590
- }
591
- if (char && char.length === 1 && !k.ctrl && !k.meta) {
592
- hideShortcutsOverlay();
593
- handleNormalKeypress(k, char);
594
- }
595
- }
596
- function toggleV2Shortcuts() {
597
- if (shortcutsOverlayVisible) {
598
- hideShortcutsOverlay();
599
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
600
- }
601
- else {
602
- showShortcutsOverlay();
603
- }
604
- }
605
- function showShortcutsOverlay() {
606
- if (!isV2UI())
607
- return;
608
- shortcutsOverlayVisible = true;
609
- renderShortcutsOverlay();
610
- }
611
- function renderShortcutsOverlay() {
612
- if (!shortcutsOverlayVisible)
613
- return;
614
- clearShortcutsOverlay({ release: false });
615
- const lines = (0, ui_v2_1.renderV2Shortcuts)(process.stdout.columns || 80).split('\n');
616
- const offset = getShortcutsOverlayOffsetRows();
617
- reserveShortcutsOverlaySpace(lines.length + offset);
618
- shortcutsOverlayLines = lines;
619
- process.stdout.write('\x1b7');
620
- if (offset > 0) {
621
- process.stdout.write(`\x1b[${offset}B\r`);
622
- }
623
- process.stdout.write('\x1b[J');
624
- for (let index = 0; index < lines.length; index++) {
625
- if (index > 0 || offset === 0) {
626
- process.stdout.write('\n');
627
- }
628
- process.stdout.write('\r' + lines[index]);
629
- }
630
- process.stdout.write('\x1b8');
631
- }
632
- function hideShortcutsOverlay() {
633
- clearShortcutsOverlay({ release: true });
634
- shortcutsOverlayVisible = false;
635
- }
636
- function clearShortcutsOverlay(options = {}) {
637
- const height = Math.max(shortcutsOverlayReservedHeight, shortcutsOverlayLines.length);
638
- if (height <= 0)
639
- return;
640
- process.stdout.write('\x1b7');
641
- const offset = getShortcutsOverlayOffsetRows();
642
- if (offset > 0) {
643
- process.stdout.write(`\x1b[${offset}B\r`);
644
- }
645
- process.stdout.write('\x1b[J');
646
- process.stdout.write('\x1b8');
647
- shortcutsOverlayLines = [];
648
- if (options.release) {
649
- shortcutsOverlayReservedHeight = 0;
650
- }
651
- }
652
- function reserveShortcutsOverlaySpace(requiredHeight) {
653
- if (requiredHeight <= shortcutsOverlayReservedHeight)
654
- return;
655
- const extraLines = requiredHeight - shortcutsOverlayReservedHeight;
656
- process.stdout.write('\n'.repeat(extraLines));
657
- process.stdout.write(`\x1b[${extraLines}A`);
658
- shortcutsOverlayReservedHeight = requiredHeight;
659
- }
660
- function getShortcutsOverlayOffsetRows() {
661
- return isV2UI() ? 2 : 1;
662
- }
663
- function handleNormalKeypress(k, char) {
664
- switch (k.name) {
665
- case 'enter':
666
- if (k.shift || k.meta) {
667
- if (isV2UI()) {
668
- insertInputNewline();
669
- }
670
- return;
671
- }
672
- // 多行模式:添加行
673
- if ((0, multiline_input_1.isMultilineActive)()) {
674
- if ((0, multiline_input_1.shouldEnterMultiline)(currentInput)) {
675
- (0, multiline_input_1.addMultilineLine)(currentInput);
676
- currentInput = '';
677
- if (isV2UI()) {
678
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
679
- }
680
- else {
681
- process.stdout.write('\r\x1b[2K');
682
- process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
683
- }
684
- }
685
- else {
686
- // 结束多行,发送完整输入
687
- (0, multiline_input_1.addMultilineLine)(currentInput);
688
- const fullInput = (0, multiline_input_1.getMultilineInput)();
689
- (0, multiline_input_1.resetMultiline)();
690
- if (fullInput.trim()) {
691
- submitInput(fullInput);
692
- }
693
- currentInput = '';
694
- }
695
- return;
696
- }
697
- // 检查是否进入多行模式
698
- if ((0, multiline_input_1.shouldEnterMultiline)(currentInput)) {
699
- (0, multiline_input_1.enterMultiline)(currentInput);
700
- currentInput = '';
701
- if (isV2UI()) {
702
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
703
- }
704
- else {
705
- process.stdout.write('\r\x1b[2K');
706
- process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
707
- }
708
- return;
709
- }
710
- // 正常发送输入
711
- if (currentInput.trim()) {
712
- submitInput(currentInput);
713
- }
714
- break;
715
- case 'backspace':
716
- currentInput = currentInput.slice(0, -1);
717
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
718
- break;
719
- case 'up':
720
- // 进入历史导航
721
- if (currentInput === '' && inputHistory.length > 0) {
722
- historyMode = 'navigate';
723
- historyIndex = 0;
724
- currentInput = inputHistory[0]?.content || '';
725
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
726
- }
727
- break;
728
- case 'r':
729
- if (k.ctrl) {
730
- // Ctrl+R:搜索历史
731
- historyMode = 'search';
732
- searchQuery = '';
733
- showHistorySearchPrompt();
734
- }
735
- else if (char) {
736
- currentInput += char;
737
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
738
- }
739
- break;
740
- case 'c':
741
- if (k.ctrl) {
742
- // Ctrl+C:取消多行或退出
743
- if ((0, multiline_input_1.isMultilineActive)()) {
744
- (0, multiline_input_1.resetMultiline)();
745
- currentInput = '';
746
- (0, command_panel_1.redrawInputWithPrompt)('');
747
- console.log(DIM('(cancelled)'));
748
- }
749
- else {
750
- handleCtrlC();
751
- }
752
- }
753
- else if (char) {
754
- currentInput += char;
755
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
756
- }
757
- break;
758
- case 'd':
759
- if (k.ctrl && currentInput === '') {
760
- // Ctrl+D:退出(空输入时)
761
- void shutdownCli();
762
- }
763
- else if (char) {
764
- currentInput += char;
765
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
766
- }
767
- break;
768
- case '/':
769
- // Issue #30 fix: 只在输入为空时触发命令面板(即 `/` 是第一个字符)
770
- // 避免在 URL(http://)、路径(src/)、正则等场景误触发
771
- if (currentInput === '' && !turnController.hasActiveTurn()) {
772
- currentInput = '/';
773
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
774
- (0, command_panel_1.showCommandPanel)('');
775
- }
776
- else {
777
- // 正常添加 `/` 到输入
778
- currentInput += '/';
779
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
780
- }
781
- break;
782
- case '@':
783
- // 显示文件补全
784
- if (turnController.hasActiveTurn()) {
785
- currentInput += '@';
786
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
787
- }
788
- else {
789
- const baseInput = currentInput;
790
- currentInput += '@';
791
- (0, file_completion_1.showFileCompletion)('', baseInput);
792
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
793
- }
794
- break;
795
- case '?':
796
- if (currentInput === '' && store.getSnapshot().config.ui?.renderer === 'v2' && !turnController.hasActiveTurn()) {
797
- toggleV2Shortcuts();
798
- }
799
- else {
800
- currentInput += '?';
801
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
802
- }
803
- break;
804
- default:
805
- // 普通字符
806
- if (char && char.length === 1 && !k.ctrl && !k.meta) {
807
- currentInput += char;
808
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
809
- }
810
- }
811
- }
812
- function insertInputNewline() {
813
- currentInput += '\n';
814
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
815
- }
816
- function updateHistorySearch() {
817
- if (searchQuery) {
818
- const allHistory = (0, global_config_1.getInputHistory)();
819
- const matches = allHistory.filter(h => h.content.toLowerCase().includes(searchQuery.toLowerCase()));
820
- historyIndex = 0;
821
- currentInput = matches[0]?.content || '';
822
- // Don't replace inputHistory — just display filtered results
823
- }
824
- else {
825
- historyIndex = -1;
826
- currentInput = '';
827
- }
828
- (0, command_panel_1.redrawInputWithPrompt)(currentInput, `[Search: ${searchQuery}]`);
829
- }
830
- function showHistorySearchPrompt() {
831
- (0, command_panel_1.clearRenderedInput)();
832
- (0, command_panel_1.redrawInputWithPrompt)('', '[Search: ]');
833
- }
834
- function isV2UI() {
835
- return store.getSnapshot().config.ui?.renderer === 'v2';
836
- }
837
- function clearTerminalView() {
838
- (0, command_panel_1.hideCommandPanel)();
839
- (0, file_completion_1.hideFileCompletion)();
840
- hideShortcutsOverlay();
841
- (0, command_panel_1.resetRenderLength)();
842
- process.stdout.write('\x1Bc');
843
- showBanner();
844
- if (isV2UI()) {
845
- console.log(DIM(' View cleared. Conversation context is preserved.'));
846
- console.log((0, ui_v2_1.renderV2FooterHint)(process.stdout.columns || 80));
847
- }
848
- else {
849
- console.log(DIM(' View cleared. Conversation context is preserved.'));
850
- }
851
- console.log();
852
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
853
- }
854
- function cancelActiveInputMode() {
855
- if ((0, multiline_input_1.isMultilineActive)()) {
856
- (0, multiline_input_1.resetMultiline)();
857
- currentInput = '';
858
- console.log(DIM('(cancelled)'));
859
- (0, command_panel_1.redrawInputWithPrompt)('');
860
- return true;
861
- }
862
- if ((0, command_panel_1.isPanelVisible)()) {
863
- (0, command_panel_1.hideCommandPanel)();
864
- (0, command_panel_1.clearPendingCommand)();
865
- currentInput = '';
866
- (0, command_panel_1.redrawInputWithPrompt)('');
867
- return true;
868
- }
869
- if ((0, file_completion_1.isFileCompletionVisible)()) {
870
- (0, file_completion_1.hideFileCompletion)();
871
- currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
872
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
873
- return true;
874
- }
875
- if (resumePickerState?.visible) {
876
- hideResumePicker();
877
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
878
- return true;
879
- }
880
- if (shortcutsOverlayVisible) {
881
- hideShortcutsOverlay();
882
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
883
- return true;
884
- }
885
- if (historyMode !== 'none') {
886
- historyMode = 'none';
887
- searchQuery = '';
888
- historyIndex = -1;
889
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
890
- return true;
891
- }
892
- return false;
893
- }
894
- async function shutdownCli() {
895
- if (isShuttingDown)
896
- return;
897
- isShuttingDown = true;
898
- // 保存会话摘要后退出
899
- if (currentSession) {
900
- const messages = (0, session_storage_1.readSessionMessages)(currentSession.id);
901
- if (messages.length > 0) {
902
- (0, session_storage_1.updateSessionSummary)(currentSession.id, messages);
903
- }
904
- (0, session_storage_1.endSession)(currentSession.id);
905
- }
906
- // 关闭 stdin raw mode 并退出
907
- if (process.stdin.isTTY) {
908
- process.stdin.setRawMode(false);
909
- }
910
- process.stdin.pause();
911
- console.log();
912
- console.log(DIM('Shutting down...'));
913
- mcp_1.mcpManager.disconnectAll();
914
- await runtime.shutdown();
915
- console.log(SUCCESS('Goodbye! 🐴'));
916
- process.exit(0);
917
- }
918
- async function handleCtrlC() {
919
- if (cancelActiveInputMode()) {
920
- turnController.clearExitIntent();
921
- return;
922
- }
923
- if (turnController.hasActiveTurn()) {
924
- const shouldExit = turnController.registerExitIntent();
925
- turnController.interruptActiveTurn();
926
- if (shouldExit) {
927
- await shutdownCli();
928
- return;
929
- }
930
- (0, command_panel_1.writeLinePreservingInput)(DIM('Interrupted. Press Ctrl+C again to exit.'));
931
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
932
- return;
933
- }
934
- if (turnController.registerExitIntent()) {
935
- await shutdownCli();
936
- return;
937
- }
938
- (0, command_panel_1.writeLinePreservingInput)(DIM('Press Ctrl+C again to exit.'));
939
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
940
- }
941
- // ============================================================================
942
- // Banner
943
- // ============================================================================
944
- function showBanner() {
945
- const config = store.getSnapshot().config;
946
- const baseUrl = config.apiBaseUrl || '';
947
- const provider = baseUrl.includes('anthropic') ? 'Anthropic'
948
- : baseUrl.includes('openai') ? 'OpenAI'
949
- : baseUrl.includes('dashscope') ? 'Alibaba Cloud'
950
- : 'Custom';
951
- if (config.ui?.renderer === 'v2') {
952
- console.log();
953
- console.log((0, ui_v2_1.renderV2ShellHeader)({
954
- provider,
955
- model: config.model,
956
- projectPath: process.cwd(),
957
- status: llm ? 'ready' : 'loading',
958
- statusText: llm ? 'ready' : 'Set OPENHORSE_API_KEY',
959
- version: VERSION,
960
- }));
961
- return;
962
- }
963
- console.log();
964
- console.log((0, box_1.renderHeaderBox)({
965
- provider,
966
- model: config.model,
967
- endpoint: baseUrl,
968
- status: llm ? 'ready' : 'loading',
969
- statusText: llm ? undefined : 'Set OPENHORSE_API_KEY in .env',
970
- version: VERSION,
971
- }));
80
+ return 'ink';
972
81
  }
973
- // ============================================================================
974
- // 输入处理
975
- // ============================================================================
976
- function getPrompt() {
977
- const mode = store.getSnapshot().permissionMode;
978
- const modeText = (0, types_1.getModeDisplayText)(mode);
979
- const modeIndicator = modeText ? `[${modeText}] ` : '';
980
- return ACCENT('❯ ') + DIM(modeIndicator);
981
- }
982
- async function handleInput(input, abortSignal, options = {}) {
983
- const redrawPrompt = options.redrawPrompt !== false;
984
- const shouldUpdateStatus = options.updateStatus !== false;
985
- const text = input.trim();
986
- if (!text)
987
- return;
988
- // 不在这里打印输入,readline 已经显示了
989
- // 直接交给 executeChat 处理,它有自己的 spinner 和流式输出
990
- const ctx = {
991
- cwd: process.cwd(),
992
- config: store.getSnapshot().config,
993
- store,
994
- llm,
995
- runtime,
996
- sessionId: currentSession?.id,
997
- ensureSession: ensureCurrentSession,
998
- setSession: setCurrentSession,
999
- getSession: getCurrentSession,
1000
- abortSignal,
1001
- writeOutput: command_panel_1.writeOutputPreservingInput,
1002
- writeLine: command_panel_1.writeLinePreservingInput,
1003
- };
1004
- let pendingSessionPicker;
1005
- try {
1006
- const parsed = (0, parser_1.parseInput)(text);
1007
- if (parsed.isCommand) {
1008
- const cmd = (0, commands_1.findCommand)(parsed.name);
1009
- if (cmd) {
1010
- const result = await cmd.execute(ctx, parsed.args);
1011
- pendingSessionPicker = result.sessionPicker;
1012
- if (result.continueAsChat) {
1013
- await (0, commands_1.executeChat)(ctx, result.chatInput ?? parsed.args);
1014
- }
1015
- }
1016
- else {
1017
- if ((0, skills_1.hasMatchingSkill)(text)) {
1018
- await (0, commands_1.executeChat)(ctx, text);
1019
- }
1020
- else {
1021
- console.log();
1022
- console.log(ERROR(`Unknown command: /${parsed.name}`));
1023
- const suggestions = (0, parser_1.buildCommandSuggestions)(parsed.name);
1024
- if (suggestions.length > 0) {
1025
- console.log(DIM(`Did you mean: ${suggestions.map(s => `/${s}`).join(', ')}?`));
1026
- }
1027
- console.log();
1028
- }
1029
- }
1030
- }
1031
- else {
1032
- // 直接 chat - executeChat 有自己的 spinner 和流式输出
1033
- await (0, commands_1.executeChat)(ctx, text);
1034
- }
1035
- }
1036
- catch (err) {
1037
- console.log(ERROR(`Error: ${err.message || String(err)}`));
1038
- }
1039
- // 显示状态栏
1040
- if (shouldUpdateStatus) {
1041
- updateStatusBar();
1042
- }
1043
- // 重新显示 prompt
1044
- if (redrawPrompt) {
1045
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
1046
- }
1047
- if (pendingSessionPicker) {
1048
- showResumePicker(pendingSessionPicker);
1049
- }
1050
- }
1051
- /**
1052
- * 更新状态栏显示
1053
- */
1054
- function updateStatusBar() {
1055
- const snapshot = store.getSnapshot();
1056
- const usage = snapshot.tokenUsage;
1057
- const costStats = snapshot.costTracker.getSessionStats();
1058
- const mcpStatus = mcp_1.mcpManager.getStatus();
1059
- const stats = {
1060
- model: snapshot.currentModel,
1061
- tokens: usage ? usage.promptTokens + usage.completionTokens : 0,
1062
- promptTokens: usage?.promptTokens || 0,
1063
- completionTokens: usage?.completionTokens || 0,
1064
- cost: costStats.totalCost,
1065
- ctxPercent: (0, model_context_1.calculateCtxPercent)(usage ? usage.promptTokens + usage.completionTokens : 0, snapshot.currentModel || 'gpt-4o'),
1066
- mcpConnected: mcpStatus.filter(s => s.connected).length,
1067
- mcpTotal: mcpStatus.length,
1068
- };
1069
- // 在 prompt 上一行显示状态栏
1070
- if (snapshot.config.ui?.renderer === 'v2') {
1071
- (0, command_panel_1.setInputStatusText)((0, ui_v2_1.renderV2StatusBadge)({
1072
- ...stats,
1073
- sessionId: currentSession?.id,
1074
- modeText: (0, types_1.getModeDisplayText)(snapshot.permissionMode) || undefined,
1075
- width: process.stdout.columns || 80,
1076
- }));
1077
- }
1078
- else {
1079
- console.log((0, status_bar_1.renderStatusBar)(stats));
1080
- }
1081
- }
1082
- // 接口变量(用于兼容性,主要逻辑通过 keypress 处理)
1083
- let rl = null;
1084
- // ============================================================================
1085
- // 主入口
1086
- // ============================================================================
1087
- async function main() {
1088
- // Parse command line arguments
1089
- const args = process.argv.slice(2);
1090
- if (args.includes('--help') || args.includes('-h')) {
1091
- showCliHelp();
1092
- process.exit(0);
1093
- }
1094
- if (args.includes('--version') || args.includes('-v')) {
1095
- console.log(`openhorse v${VERSION}`);
1096
- process.exit(0);
1097
- }
82
+ async function bootstrapRuntime(uiRenderer) {
1098
83
  (0, config_dir_1.ensureConfigDir)();
1099
84
  (0, global_config_1.recordFirstStartTime)();
1100
- const projectPath = process.cwd();
1101
- const uiRenderer = parseCliUIRenderer(args);
1102
- const cliConfig = (0, config_1.loadConfig)(uiRenderer ? { ui: { renderer: uiRenderer } } : {});
1103
- (0, command_panel_1.setInputPromptRenderer)(cliConfig.ui?.renderer === 'v2' ? 'v2' : 'legacy');
1104
- (0, command_panel_1.setInputRenderContextProvider)(() => (cliConfig.ui?.renderer === 'v2' && (0, multiline_input_1.isMultilineActive)()
1105
- ? { prefixLines: (0, multiline_input_1.getMultilineLines)() }
1106
- : {}));
1107
- (0, file_completion_1.setFileCompletionPromptRenderer)(cliConfig.ui?.renderer === 'v2' ? 'v2' : 'legacy');
1108
- // Load project memory
1109
- const memories = (0, storage_1.loadAllMemories)(projectPath);
85
+ const cwd = process.cwd();
86
+ const config = (0, config_1.loadConfig)({ ui: { renderer: uiRenderer } });
87
+ const memories = (0, storage_1.loadAllMemories)(cwd);
1110
88
  const memoryContent = memories.length > 0
1111
- ? memories.map(m => `## ${m.name} (${m.type})\n${m.content}`).join('\n\n')
89
+ ? memories.map(memory => `## ${memory.name} (${memory.type})\n${memory.content}`).join('\n\n')
1112
90
  : '';
1113
- // Load skills (builtin + user + project) and render the prompt section
1114
91
  let skillsContent = '';
1115
92
  try {
1116
93
  const registry = (0, skills_1.getSkillsRegistry)();
1117
94
  skillsContent = registry.generateSystemPromptInjection();
1118
95
  }
1119
- catch (err) {
1120
- console.error(WARN(`⚠ Skills load error: ${err.message}`));
96
+ catch {
97
+ skillsContent = '';
1121
98
  }
1122
- store = new framework_1.Store({
1123
- config: cliConfig,
99
+ const store = new framework_1.Store({
100
+ config,
1124
101
  tools: (0, tools_1.getRuntimeTools)(),
1125
- currentModel: cliConfig.model,
102
+ currentModel: config.model,
1126
103
  memoryContent,
1127
104
  skillsContent,
1128
105
  });
1129
- // Mirror tool-state (todos/plan) into Store so the UI can observe it
1130
106
  (0, framework_1.resetToolState)();
1131
- (0, framework_1.subscribeToolState)((s) => {
107
+ (0, framework_1.subscribeToolState)(state => {
1132
108
  store.setState({
1133
- todos: s.todos,
1134
- planMode: s.planMode,
1135
- currentPlan: s.currentPlan,
109
+ todos: state.todos,
110
+ planMode: state.planMode,
111
+ currentPlan: state.currentPlan,
1136
112
  });
1137
113
  });
1138
- if ((0, config_1.isConfigured)(cliConfig)) {
1139
- try {
1140
- llm = new llm_1.LLMService({
1141
- apiKey: cliConfig.apiKey,
1142
- baseUrl: cliConfig.apiBaseUrl,
1143
- model: cliConfig.model,
1144
- fallbackModel: cliConfig.fallbackModel,
1145
- });
1146
- // 动态发现模型上下文窗口(非阻塞)
1147
- if (cliConfig.apiBaseUrl) {
1148
- (0, model_context_1.discoverModelContexts)(cliConfig.apiBaseUrl, cliConfig.apiKey)
1149
- .then(models => {
1150
- if (models.length > 0) {
1151
- console.log(` Discovered ${models.length} models from API`);
1152
- }
1153
- })
1154
- .catch(() => { }); // 静默失败,回退到内置数据库
1155
- }
1156
- }
1157
- catch (err) {
1158
- console.log(WARN(`⚠ LLM initialization warning: ${err.message}`));
114
+ let llm = null;
115
+ if ((0, config_1.isConfigured)(config)) {
116
+ llm = new llm_1.LLMService({
117
+ apiKey: config.apiKey,
118
+ baseUrl: config.apiBaseUrl,
119
+ model: config.model,
120
+ fallbackModel: config.fallbackModel,
121
+ });
122
+ if (config.apiBaseUrl) {
123
+ (0, model_context_1.discoverModelContexts)(config.apiBaseUrl, config.apiKey).catch(() => undefined);
1159
124
  }
1160
125
  }
1161
- const config = store.getSnapshot().config;
1162
- runtime = await (0, init_1.init)({
126
+ const runtime = await (0, init_1.init)({
1163
127
  name: config.name,
1164
128
  mode: config.mode,
1165
129
  logLevel: config.logLevel,
1166
130
  });
1167
131
  await runtime.start();
1168
- // Auto-connect MCP servers from ~/.openhorse/mcp.json (non-blocking)
1169
- mcp_1.mcpManager.connectAll()
1170
- .then(() => {
1171
- store.setState({ tools: (0, tools_1.getRuntimeTools)() });
1172
- })
1173
- .catch(err => {
1174
- console.error(WARN(`⚠ MCP startup error: ${err.message}`));
1175
- });
1176
- // Banner
1177
- showBanner();
1178
- // 提示
1179
- console.log(SUCCESS('✔ System initialized'));
1180
- console.log(DIM(' Type /help for commands, /exit to quit'));
1181
- if (cliConfig.ui?.renderer === 'v2') {
1182
- console.log(DIM(' UI v2 enabled: command palette and session picker'));
1183
- console.log((0, ui_v2_1.renderV2FooterHint)(process.stdout.columns || 80));
1184
- }
1185
- if (!(0, config_1.isConfigured)(cliConfig)) {
1186
- console.log(WARN(' ⚠ LLM not configured — set OPENHORSE_API_KEY'));
1187
- }
1188
- console.log();
1189
- // 加载输入历史
1190
- inputHistory = (0, global_config_1.getInputHistory)();
1191
- // 启用 keypress 事件处理
1192
- // 使用 emitKeypressEvents + setRawMode 实现交互式功能
1193
- readline_1.default.emitKeypressEvents(process.stdin);
1194
- if (process.stdin.isTTY) {
132
+ void (async () => {
133
+ const originalLog = console.log;
134
+ const originalError = console.error;
1195
135
  try {
1196
- process.stdin.setRawMode(true);
136
+ console.log = () => undefined;
137
+ console.error = () => undefined;
138
+ await mcp_1.mcpManager.connectAll();
139
+ store.setState({ tools: (0, tools_1.getRuntimeTools)() });
1197
140
  }
1198
- catch (err) {
1199
- console.error(ERROR(`setRawMode failed: ${err.message}`));
141
+ catch {
142
+ // MCP failures are surfaced through /mcp and tool errors.
1200
143
  }
1201
- }
1202
- else {
1203
- console.log(WARN('⚠ stdin is not TTY - interactive features disabled'));
1204
- }
1205
- process.stdin.resume();
1206
- // 监听 keypress 事件
1207
- process.stdin.on('keypress', (char, key) => {
1208
- try {
1209
- handleKeypress(char, key);
1210
- }
1211
- catch (err) {
1212
- console.error(ERROR(`Keypress error: ${err.message}`));
144
+ finally {
145
+ console.log = originalLog;
146
+ console.error = originalError;
147
+ }
148
+ })();
149
+ let currentSession = null;
150
+ let shuttingDown = false;
151
+ const ensureSession = () => {
152
+ if (!currentSession) {
153
+ currentSession = (0, session_storage_1.createSession)(cwd, store.getSnapshot().currentModel || store.getSnapshot().config.model);
154
+ (0, global_config_1.incrementSessionCount)();
155
+ }
156
+ return currentSession;
157
+ };
158
+ const setSession = (session) => {
159
+ currentSession = session;
160
+ };
161
+ const getSession = () => currentSession;
162
+ const shutdown = async () => {
163
+ if (shuttingDown)
164
+ return;
165
+ shuttingDown = true;
166
+ if (currentSession) {
167
+ const messages = (0, session_storage_1.readSessionMessages)(currentSession.id);
168
+ if (messages.length > 0) {
169
+ (0, session_storage_1.updateSessionSummary)(currentSession.id, messages);
170
+ }
171
+ (0, session_storage_1.endSession)(currentSession.id);
1213
172
  }
1214
- });
1215
- // 初始 prompt
1216
- if (cliConfig.ui?.renderer === 'v2') {
1217
- updateStatusBar();
173
+ await mcp_1.mcpManager.disconnectAll();
174
+ await runtime.shutdown();
175
+ };
176
+ return {
177
+ cwd,
178
+ version: VERSION,
179
+ config,
180
+ store,
181
+ llm,
182
+ runtime,
183
+ isConfigured: (0, config_1.isConfigured)(config),
184
+ ensureSession,
185
+ setSession,
186
+ getSession,
187
+ shutdown,
188
+ };
189
+ }
190
+ async function main() {
191
+ const args = process.argv.slice(2);
192
+ if (args.includes('--help') || args.includes('-h')) {
193
+ showCliHelp();
194
+ process.exit(0);
195
+ }
196
+ if (args.includes('--version') || args.includes('-v')) {
197
+ console.log(`openhorse v${VERSION}`);
198
+ process.exit(0);
1218
199
  }
1219
- (0, command_panel_1.redrawInputWithPrompt)('');
200
+ const uiRenderer = parseCliUIRenderer(args);
201
+ const runtime = await bootstrapRuntime(uiRenderer);
202
+ await (0, launch_1.launchInkUI)(runtime);
1220
203
  }
1221
- main().catch(err => {
1222
- console.error(ERROR('[OpenHorse] Fatal error:'), err);
204
+ main().catch(async (error) => {
205
+ console.error(ERROR('[OpenHorse] Fatal error:'), error);
206
+ try {
207
+ await mcp_1.mcpManager.disconnectAll();
208
+ }
209
+ catch {
210
+ // ignore shutdown errors
211
+ }
1223
212
  process.exit(1);
1224
213
  });
1225
214
  //# sourceMappingURL=cli.js.map