openhorse 0.1.19 → 0.1.21

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 (84) hide show
  1. package/dist/cli.js +446 -33
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/index.d.ts.map +1 -1
  4. package/dist/commands/index.js +99 -40
  5. package/dist/commands/index.js.map +1 -1
  6. package/dist/commands/types.d.ts +14 -0
  7. package/dist/commands/types.d.ts.map +1 -1
  8. package/dist/commands/types.js.map +1 -1
  9. package/dist/framework/query.d.ts.map +1 -1
  10. package/dist/framework/query.js +25 -7
  11. package/dist/framework/query.js.map +1 -1
  12. package/dist/runtime/turn-controller.d.ts +37 -0
  13. package/dist/runtime/turn-controller.d.ts.map +1 -0
  14. package/dist/runtime/turn-controller.js +84 -0
  15. package/dist/runtime/turn-controller.js.map +1 -0
  16. package/dist/services/config.d.ts +4 -2
  17. package/dist/services/config.d.ts.map +1 -1
  18. package/dist/services/config.js +30 -0
  19. package/dist/services/config.js.map +1 -1
  20. package/dist/services/global-config.d.ts +13 -0
  21. package/dist/services/global-config.d.ts.map +1 -1
  22. package/dist/services/global-config.js +4 -0
  23. package/dist/services/global-config.js.map +1 -1
  24. package/dist/services/llm.d.ts +4 -1
  25. package/dist/services/llm.d.ts.map +1 -1
  26. package/dist/services/llm.js +15 -2
  27. package/dist/services/llm.js.map +1 -1
  28. package/dist/ui/command-panel.d.ts +17 -0
  29. package/dist/ui/command-panel.d.ts.map +1 -1
  30. package/dist/ui/command-panel.js +234 -93
  31. package/dist/ui/command-panel.js.map +1 -1
  32. package/dist/ui/file-completion.d.ts +3 -0
  33. package/dist/ui/file-completion.d.ts.map +1 -1
  34. package/dist/ui/file-completion.js +7 -2
  35. package/dist/ui/file-completion.js.map +1 -1
  36. package/dist/ui/multiline-input.d.ts +4 -0
  37. package/dist/ui/multiline-input.d.ts.map +1 -1
  38. package/dist/ui/multiline-input.js +7 -0
  39. package/dist/ui/multiline-input.js.map +1 -1
  40. package/dist/ui/user-input.d.ts +1 -0
  41. package/dist/ui/user-input.d.ts.map +1 -1
  42. package/dist/ui/user-input.js +8 -4
  43. package/dist/ui/user-input.js.map +1 -1
  44. package/dist/ui-v2/components/command-palette.d.ts +6 -0
  45. package/dist/ui-v2/components/command-palette.d.ts.map +1 -0
  46. package/dist/ui-v2/components/command-palette.js +57 -0
  47. package/dist/ui-v2/components/command-palette.js.map +1 -0
  48. package/dist/ui-v2/components/session-picker.d.ts +17 -0
  49. package/dist/ui-v2/components/session-picker.d.ts.map +1 -0
  50. package/dist/ui-v2/components/session-picker.js +23 -0
  51. package/dist/ui-v2/components/session-picker.js.map +1 -0
  52. package/dist/ui-v2/components/shell.d.ts +49 -0
  53. package/dist/ui-v2/components/shell.d.ts.map +1 -0
  54. package/dist/ui-v2/components/shell.js +190 -0
  55. package/dist/ui-v2/components/shell.js.map +1 -0
  56. package/dist/ui-v2/index.d.ts +10 -0
  57. package/dist/ui-v2/index.d.ts.map +1 -0
  58. package/dist/ui-v2/index.js +26 -0
  59. package/dist/ui-v2/index.js.map +1 -0
  60. package/dist/ui-v2/runtime/text.d.ts +8 -0
  61. package/dist/ui-v2/runtime/text.d.ts.map +1 -0
  62. package/dist/ui-v2/runtime/text.js +58 -0
  63. package/dist/ui-v2/runtime/text.js.map +1 -0
  64. package/dist/ui-v2/state/input-reducer.d.ts +39 -0
  65. package/dist/ui-v2/state/input-reducer.d.ts.map +1 -0
  66. package/dist/ui-v2/state/input-reducer.js +82 -0
  67. package/dist/ui-v2/state/input-reducer.js.map +1 -0
  68. package/dist/ui-v2/state/picker.d.ts +16 -0
  69. package/dist/ui-v2/state/picker.d.ts.map +1 -0
  70. package/dist/ui-v2/state/picker.js +52 -0
  71. package/dist/ui-v2/state/picker.js.map +1 -0
  72. package/dist/ui-v2/state/sessions.d.ts +14 -0
  73. package/dist/ui-v2/state/sessions.d.ts.map +1 -0
  74. package/dist/ui-v2/state/sessions.js +65 -0
  75. package/dist/ui-v2/state/sessions.js.map +1 -0
  76. package/dist/ui-v2/state/suggestions.d.ts +22 -0
  77. package/dist/ui-v2/state/suggestions.d.ts.map +1 -0
  78. package/dist/ui-v2/state/suggestions.js +55 -0
  79. package/dist/ui-v2/state/suggestions.js.map +1 -0
  80. package/dist/ui-v2/types.d.ts +33 -0
  81. package/dist/ui-v2/types.d.ts.map +1 -0
  82. package/dist/ui-v2/types.js +9 -0
  83. package/dist/ui-v2/types.js.map +1 -0
  84. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -34,6 +34,8 @@ const multiline_input_1 = require("./ui/multiline-input");
34
34
  const file_completion_1 = require("./ui/file-completion");
35
35
  const status_bar_1 = require("./ui/status-bar");
36
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");
37
39
  // Get version from package.json
38
40
  const VERSION = (() => {
39
41
  try {
@@ -66,10 +68,12 @@ function showCliHelp() {
66
68
  console.log(' openhorse Start interactive REPL');
67
69
  console.log(' openhorse --help Show this help message');
68
70
  console.log(' openhorse --version Show version');
71
+ console.log(' openhorse --ui v2 Enable UI v2 preview components');
69
72
  console.log();
70
73
  console.log(ACCENT('Options:'));
71
74
  console.log(' -h, --help Show help');
72
75
  console.log(' -v, --version Show version');
76
+ console.log(' --ui <mode> UI renderer: legacy | v2');
73
77
  console.log();
74
78
  console.log(ACCENT('Interactive Commands:'));
75
79
  console.log(' /help Show available slash commands');
@@ -81,6 +85,24 @@ function showCliHelp() {
81
85
  console.log(DIM('Type /help in REPL for full command list.'));
82
86
  console.log();
83
87
  }
88
+ function parseCliUIRenderer(args) {
89
+ for (let i = 0; i < args.length; i++) {
90
+ const arg = args[i];
91
+ const value = arg === '--ui'
92
+ ? args[i + 1]
93
+ : arg.startsWith('--ui=')
94
+ ? arg.slice('--ui='.length)
95
+ : undefined;
96
+ if (value === undefined)
97
+ continue;
98
+ if (value === 'legacy' || value === 'v2')
99
+ return value;
100
+ console.error(ERROR(`Invalid --ui value: ${value}`));
101
+ console.error(DIM('Expected one of: legacy, v2'));
102
+ process.exit(1);
103
+ }
104
+ return undefined;
105
+ }
84
106
  // ============================================================================
85
107
  // 全局状态
86
108
  // ============================================================================
@@ -88,29 +110,108 @@ let llm = null;
88
110
  let store;
89
111
  let currentSession = null;
90
112
  let runtime;
113
+ const turnController = new turn_controller_1.TurnController();
114
+ let isShuttingDown = false;
91
115
  // 输入状态
92
116
  let currentInput = '';
93
117
  let inputHistory = [];
94
118
  let historyIndex = -1;
95
119
  let historyMode = 'none';
96
120
  let searchQuery = '';
121
+ let resumePickerState = null;
122
+ let resumePickerLines = [];
123
+ let resumePickerReservedHeight = 0;
124
+ function ensureCurrentSession() {
125
+ if (!currentSession) {
126
+ currentSession = (0, session_storage_1.createSession)(process.cwd(), store.getSnapshot().currentModel || store.getSnapshot().config.model);
127
+ (0, global_config_1.incrementSessionCount)();
128
+ }
129
+ return currentSession;
130
+ }
131
+ function setCurrentSession(session) {
132
+ currentSession = session;
133
+ }
134
+ function getCurrentSession() {
135
+ return currentSession;
136
+ }
97
137
  function echoSubmittedInput(input) {
98
- process.stdout.write('\x1b[2K\r');
99
- console.log((0, user_input_1.renderUserInputEcho)(input));
100
- (0, command_panel_1.resetRenderLength)();
138
+ (0, command_panel_1.clearRenderedInput)();
139
+ const rendered = isV2UI()
140
+ ? (0, user_input_1.renderUserInputEchoFrame)(input)
141
+ : (0, user_input_1.renderUserInputEcho)(input);
142
+ (0, command_panel_1.writeLinePreservingInput)(rendered);
143
+ if (isV2UI()) {
144
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
145
+ }
146
+ }
147
+ function isExitCommandInput(input) {
148
+ const parsed = (0, parser_1.parseInput)(input.trim());
149
+ if (!parsed.isCommand)
150
+ return false;
151
+ const cmd = (0, commands_1.findCommand)(parsed.name);
152
+ return cmd?.name === 'exit';
101
153
  }
102
154
  function submitInput(input) {
103
155
  const submittedInput = input;
104
156
  currentInput = '';
105
157
  echoSubmittedInput(submittedInput);
106
- handleInput(submittedInput).catch(err => {
107
- console.log(ERROR(`Input error: ${err.message || String(err)}`));
108
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
109
- });
110
158
  (0, global_config_1.addToInputHistory)(submittedInput);
111
159
  inputHistory = (0, global_config_1.getInputHistory)();
160
+ if (turnController.hasActiveTurn()) {
161
+ const text = submittedInput.trim();
162
+ const parsed = (0, parser_1.parseInput)(text);
163
+ if (isExitCommandInput(text)) {
164
+ void shutdownCli();
165
+ return;
166
+ }
167
+ if (parsed.isCommand) {
168
+ console.log(DIM('Command ignored while agent is running. Press Ctrl+C to interrupt first.'));
169
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
170
+ return;
171
+ }
172
+ if (text) {
173
+ turnController.clearExitIntent();
174
+ turnController.requestRevision(submittedInput);
175
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
176
+ }
177
+ return;
178
+ }
179
+ void runInputTurn(submittedInput);
180
+ }
181
+ async function runInputTurn(input) {
182
+ let nextInput = input;
183
+ while (nextInput) {
184
+ const turn = turnController.beginTurn(nextInput);
185
+ store.setProcessing(true);
186
+ try {
187
+ await handleInput(nextInput, turn.abortSignal, { redrawPrompt: false, updateStatus: false });
188
+ }
189
+ catch (err) {
190
+ console.log(ERROR(`Input error: ${err.message || String(err)}`));
191
+ }
192
+ finally {
193
+ const revision = turnController.finishTurn(turn.id);
194
+ store.setProcessing(false);
195
+ if (revision && revision.trim()) {
196
+ (0, command_panel_1.writeLinePreservingInput)(DIM('Interrupted. Restarting with latest instruction...'));
197
+ nextInput = revision;
198
+ }
199
+ else {
200
+ nextInput = undefined;
201
+ }
202
+ }
203
+ }
204
+ updateStatusBar();
205
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
112
206
  }
113
207
  function parseKey(char, key) {
208
+ const sequence = key?.sequence || char || '';
209
+ if (sequence === '\x1b[13;2u') {
210
+ return { name: 'enter', ctrl: false, shift: true, meta: false, sequence };
211
+ }
212
+ if (sequence === '\x1b[13;3u') {
213
+ return { name: 'enter', ctrl: false, shift: false, meta: true, sequence };
214
+ }
114
215
  if (!key) {
115
216
  // 手动解析
116
217
  if (char === '\r' || char === '\n')
@@ -127,6 +228,9 @@ function parseKey(char, key) {
127
228
  if (key.name === 'return') {
128
229
  key.name = 'enter';
129
230
  }
231
+ if (key.name === 'enter' && (key.shift || key.meta)) {
232
+ return key;
233
+ }
130
234
  // 如果 key.name 为空,使用 char 作为 name
131
235
  if (!key.name && char) {
132
236
  key.name = char;
@@ -135,6 +239,14 @@ function parseKey(char, key) {
135
239
  }
136
240
  function handleKeypress(char, key) {
137
241
  const k = parseKey(char, key);
242
+ if (k.ctrl && k.name === 'l') {
243
+ clearTerminalView();
244
+ return;
245
+ }
246
+ if (k.ctrl && k.name === 'c') {
247
+ void handleCtrlC();
248
+ return;
249
+ }
138
250
  // 命令面板模式
139
251
  if ((0, command_panel_1.isPanelVisible)()) {
140
252
  handlePanelKeypress(k, char);
@@ -145,6 +257,11 @@ function handleKeypress(char, key) {
145
257
  handleFileCompletionKeypress(k, char);
146
258
  return;
147
259
  }
260
+ // Resume session picker mode
261
+ if (resumePickerState?.visible) {
262
+ handleResumePickerKeypress(k, char);
263
+ return;
264
+ }
148
265
  // 历史导航模式
149
266
  if (historyMode !== 'none') {
150
267
  handleHistoryKeypress(k, char);
@@ -161,6 +278,13 @@ function handlePanelKeypress(k, char) {
161
278
  case 'down':
162
279
  (0, command_panel_1.navigatePanel)('down');
163
280
  break;
281
+ case 'tab':
282
+ const completedCmd = (0, command_panel_1.completeSelectedCommand)();
283
+ if (completedCmd) {
284
+ currentInput = completedCmd;
285
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
286
+ }
287
+ break;
164
288
  case 'enter':
165
289
  const cmd = (0, command_panel_1.selectCommand)();
166
290
  if (cmd) {
@@ -310,16 +434,146 @@ function handleFileCompletionKeypress(k, char) {
310
434
  }
311
435
  }
312
436
  }
437
+ function handleResumePickerKeypress(k, char) {
438
+ if (!resumePickerState?.visible)
439
+ return;
440
+ switch (k.name) {
441
+ case 'up':
442
+ resumePickerState.selectedIndex = Math.max(0, resumePickerState.selectedIndex - 1);
443
+ renderResumePicker();
444
+ break;
445
+ case 'down':
446
+ resumePickerState.selectedIndex = Math.min(resumePickerState.sessions.length - 1, resumePickerState.selectedIndex + 1);
447
+ renderResumePicker();
448
+ break;
449
+ case 'enter':
450
+ restoreSelectedResumeSession();
451
+ break;
452
+ case 'escape':
453
+ hideResumePicker();
454
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
455
+ break;
456
+ default:
457
+ if (char && /^[1-9]$/.test(char)) {
458
+ const index = Number(char) - 1;
459
+ if (index >= 0 && index < resumePickerState.sessions.length) {
460
+ resumePickerState.selectedIndex = index;
461
+ restoreSelectedResumeSession();
462
+ }
463
+ }
464
+ }
465
+ }
466
+ function showResumePicker(options) {
467
+ resumePickerState = {
468
+ visible: true,
469
+ sessions: options.sessions,
470
+ selectedIndex: 0,
471
+ title: options.title,
472
+ showProject: options.showProject,
473
+ moreCount: options.moreCount,
474
+ allProjects: options.allProjects,
475
+ };
476
+ renderResumePicker();
477
+ }
478
+ function renderResumePicker() {
479
+ if (!resumePickerState?.visible)
480
+ return;
481
+ clearResumePicker({ release: false });
482
+ const lines = (0, ui_v2_1.renderSessionPicker)({
483
+ title: resumePickerState.title,
484
+ sessions: resumePickerState.sessions,
485
+ selectedIndex: resumePickerState.selectedIndex,
486
+ width: process.stdout.columns || 80,
487
+ showProject: resumePickerState.showProject,
488
+ moreCount: resumePickerState.moreCount,
489
+ footer: ' ↑↓ Select Enter Resume 1-9 Quick Esc Cancel',
490
+ theme: {
491
+ accent: ACCENT,
492
+ dim: DIM,
493
+ selected: text => chalk_1.default.bgHex('#1E293B').hex('#E2E8F0')(text),
494
+ },
495
+ });
496
+ const offset = getResumePickerOffsetRows();
497
+ reserveResumePickerSpace(lines.length + offset);
498
+ resumePickerLines = lines;
499
+ process.stdout.write('\x1b7');
500
+ if (offset > 0) {
501
+ process.stdout.write(`\x1b[${offset}B\r`);
502
+ }
503
+ process.stdout.write('\x1b[J');
504
+ for (let index = 0; index < lines.length; index++) {
505
+ if (index > 0 || offset === 0) {
506
+ process.stdout.write('\n');
507
+ }
508
+ process.stdout.write('\r' + lines[index]);
509
+ }
510
+ process.stdout.write('\x1b8');
511
+ }
512
+ function hideResumePicker() {
513
+ clearResumePicker({ release: true });
514
+ resumePickerState = null;
515
+ }
516
+ function clearResumePicker(options = {}) {
517
+ const height = Math.max(resumePickerReservedHeight, resumePickerLines.length);
518
+ if (height <= 0)
519
+ return;
520
+ process.stdout.write('\x1b7');
521
+ const offset = getResumePickerOffsetRows();
522
+ if (offset > 0) {
523
+ process.stdout.write(`\x1b[${offset}B\r`);
524
+ }
525
+ process.stdout.write('\x1b[J');
526
+ process.stdout.write('\x1b8');
527
+ resumePickerLines = [];
528
+ if (options.release) {
529
+ resumePickerReservedHeight = 0;
530
+ }
531
+ }
532
+ function reserveResumePickerSpace(requiredHeight) {
533
+ if (requiredHeight <= resumePickerReservedHeight)
534
+ return;
535
+ const extraLines = requiredHeight - resumePickerReservedHeight;
536
+ process.stdout.write('\n'.repeat(extraLines));
537
+ process.stdout.write(`\x1b[${extraLines}A`);
538
+ resumePickerReservedHeight = requiredHeight;
539
+ }
540
+ function getResumePickerOffsetRows() {
541
+ return isV2UI() ? 2 : 1;
542
+ }
543
+ function restoreSelectedResumeSession() {
544
+ if (!resumePickerState?.visible)
545
+ return;
546
+ const session = resumePickerState.sessions[resumePickerState.selectedIndex];
547
+ const suffix = resumePickerState.allProjects ? ' --all' : '';
548
+ hideResumePicker();
549
+ currentInput = '';
550
+ (0, command_panel_1.clearRenderedInput)();
551
+ runInputTurn(`/resume ${session.id}${suffix}`).catch(err => {
552
+ console.log(ERROR(`Resume error: ${err.message || String(err)}`));
553
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
554
+ });
555
+ }
313
556
  function handleNormalKeypress(k, char) {
314
557
  switch (k.name) {
315
558
  case 'enter':
559
+ if (k.shift || k.meta) {
560
+ if (isV2UI()) {
561
+ insertInputNewline();
562
+ }
563
+ return;
564
+ }
316
565
  // 多行模式:添加行
317
566
  if ((0, multiline_input_1.isMultilineActive)()) {
318
567
  if ((0, multiline_input_1.shouldEnterMultiline)(currentInput)) {
319
- (0, multiline_input_1.addMultilineLine)(currentInput.slice(0, -1));
568
+ (0, multiline_input_1.addMultilineLine)(currentInput);
320
569
  currentInput = '';
321
- process.stdout.write('\r\x1b[2K');
322
- process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
570
+ if (isV2UI()) {
571
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
572
+ }
573
+ else {
574
+ process.stdout.write('\r\x1b[2K');
575
+ process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
576
+ }
323
577
  }
324
578
  else {
325
579
  // 结束多行,发送完整输入
@@ -337,8 +591,13 @@ function handleNormalKeypress(k, char) {
337
591
  if ((0, multiline_input_1.shouldEnterMultiline)(currentInput)) {
338
592
  (0, multiline_input_1.enterMultiline)(currentInput);
339
593
  currentInput = '';
340
- process.stdout.write('\r\x1b[2K');
341
- process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
594
+ if (isV2UI()) {
595
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
596
+ }
597
+ else {
598
+ process.stdout.write('\r\x1b[2K');
599
+ process.stdout.write((0, multiline_input_1.renderContinuationPrompt)());
600
+ }
342
601
  return;
343
602
  }
344
603
  // 正常发送输入
@@ -392,7 +651,7 @@ function handleNormalKeypress(k, char) {
392
651
  case 'd':
393
652
  if (k.ctrl && currentInput === '') {
394
653
  // Ctrl+D:退出(空输入时)
395
- handleCtrlC();
654
+ void shutdownCli();
396
655
  }
397
656
  else if (char) {
398
657
  currentInput += char;
@@ -402,9 +661,8 @@ function handleNormalKeypress(k, char) {
402
661
  case '/':
403
662
  // Issue #30 fix: 只在输入为空时触发命令面板(即 `/` 是第一个字符)
404
663
  // 避免在 URL(http://)、路径(src/)、正则等场景误触发
405
- if (currentInput === '') {
664
+ if (currentInput === '' && !turnController.hasActiveTurn()) {
406
665
  currentInput = '/';
407
- (0, command_panel_1.resetRenderLength)();
408
666
  (0, command_panel_1.redrawInputWithPrompt)(currentInput);
409
667
  (0, command_panel_1.showCommandPanel)('');
410
668
  }
@@ -416,10 +674,25 @@ function handleNormalKeypress(k, char) {
416
674
  break;
417
675
  case '@':
418
676
  // 显示文件补全
419
- const baseInput = currentInput;
420
- currentInput += '@';
421
- (0, file_completion_1.showFileCompletion)('', baseInput);
422
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
677
+ if (turnController.hasActiveTurn()) {
678
+ currentInput += '@';
679
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
680
+ }
681
+ else {
682
+ const baseInput = currentInput;
683
+ currentInput += '@';
684
+ (0, file_completion_1.showFileCompletion)('', baseInput);
685
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
686
+ }
687
+ break;
688
+ case '?':
689
+ if (currentInput === '' && store.getSnapshot().config.ui?.renderer === 'v2' && !turnController.hasActiveTurn()) {
690
+ showV2Shortcuts();
691
+ }
692
+ else {
693
+ currentInput += '?';
694
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
695
+ }
423
696
  break;
424
697
  default:
425
698
  // 普通字符
@@ -429,6 +702,10 @@ function handleNormalKeypress(k, char) {
429
702
  }
430
703
  }
431
704
  }
705
+ function insertInputNewline() {
706
+ currentInput += '\n';
707
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
708
+ }
432
709
  function updateHistorySearch() {
433
710
  if (searchQuery) {
434
711
  const allHistory = (0, global_config_1.getInputHistory)();
@@ -444,10 +721,72 @@ function updateHistorySearch() {
444
721
  (0, command_panel_1.redrawInputWithPrompt)(currentInput, `[Search: ${searchQuery}]`);
445
722
  }
446
723
  function showHistorySearchPrompt() {
447
- process.stdout.write('\r\x1b[2K');
448
- process.stdout.write(ACCENT('') + DIM('[Search: ]'));
724
+ (0, command_panel_1.clearRenderedInput)();
725
+ (0, command_panel_1.redrawInputWithPrompt)('', '[Search: ]');
449
726
  }
450
- async function handleCtrlC() {
727
+ function isV2UI() {
728
+ return store.getSnapshot().config.ui?.renderer === 'v2';
729
+ }
730
+ function showV2Shortcuts() {
731
+ console.log();
732
+ console.log((0, ui_v2_1.renderV2Shortcuts)(process.stdout.columns || 80));
733
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
734
+ }
735
+ function clearTerminalView() {
736
+ (0, command_panel_1.hideCommandPanel)();
737
+ (0, file_completion_1.hideFileCompletion)();
738
+ (0, command_panel_1.resetRenderLength)();
739
+ process.stdout.write('\x1Bc');
740
+ showBanner();
741
+ if (isV2UI()) {
742
+ console.log(DIM(' View cleared. Conversation context is preserved.'));
743
+ console.log((0, ui_v2_1.renderV2FooterHint)(process.stdout.columns || 80));
744
+ }
745
+ else {
746
+ console.log(DIM(' View cleared. Conversation context is preserved.'));
747
+ }
748
+ console.log();
749
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
750
+ }
751
+ function cancelActiveInputMode() {
752
+ if ((0, multiline_input_1.isMultilineActive)()) {
753
+ (0, multiline_input_1.resetMultiline)();
754
+ currentInput = '';
755
+ console.log(DIM('(cancelled)'));
756
+ (0, command_panel_1.redrawInputWithPrompt)('');
757
+ return true;
758
+ }
759
+ if ((0, command_panel_1.isPanelVisible)()) {
760
+ (0, command_panel_1.hideCommandPanel)();
761
+ (0, command_panel_1.clearPendingCommand)();
762
+ currentInput = '';
763
+ (0, command_panel_1.redrawInputWithPrompt)('');
764
+ return true;
765
+ }
766
+ if ((0, file_completion_1.isFileCompletionVisible)()) {
767
+ (0, file_completion_1.hideFileCompletion)();
768
+ currentInput = (0, file_completion_1.getBaseInput)() + '@' + (0, file_completion_1.getFileQuery)();
769
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
770
+ return true;
771
+ }
772
+ if (resumePickerState?.visible) {
773
+ hideResumePicker();
774
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
775
+ return true;
776
+ }
777
+ if (historyMode !== 'none') {
778
+ historyMode = 'none';
779
+ searchQuery = '';
780
+ historyIndex = -1;
781
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
782
+ return true;
783
+ }
784
+ return false;
785
+ }
786
+ async function shutdownCli() {
787
+ if (isShuttingDown)
788
+ return;
789
+ isShuttingDown = true;
451
790
  // 保存会话摘要后退出
452
791
  if (currentSession) {
453
792
  const messages = (0, session_storage_1.readSessionMessages)(currentSession.id);
@@ -468,18 +807,54 @@ async function handleCtrlC() {
468
807
  console.log(SUCCESS('Goodbye! 🐴'));
469
808
  process.exit(0);
470
809
  }
810
+ async function handleCtrlC() {
811
+ if (cancelActiveInputMode()) {
812
+ turnController.clearExitIntent();
813
+ return;
814
+ }
815
+ if (turnController.hasActiveTurn()) {
816
+ const shouldExit = turnController.registerExitIntent();
817
+ turnController.interruptActiveTurn();
818
+ if (shouldExit) {
819
+ await shutdownCli();
820
+ return;
821
+ }
822
+ (0, command_panel_1.writeLinePreservingInput)(DIM('Interrupted. Press Ctrl+C again to exit.'));
823
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
824
+ return;
825
+ }
826
+ if (turnController.registerExitIntent()) {
827
+ await shutdownCli();
828
+ return;
829
+ }
830
+ (0, command_panel_1.writeLinePreservingInput)(DIM('Press Ctrl+C again to exit.'));
831
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
832
+ }
471
833
  // ============================================================================
472
834
  // Banner
473
835
  // ============================================================================
474
836
  function showBanner() {
475
837
  const config = store.getSnapshot().config;
476
838
  const baseUrl = config.apiBaseUrl || '';
839
+ const provider = baseUrl.includes('anthropic') ? 'Anthropic'
840
+ : baseUrl.includes('openai') ? 'OpenAI'
841
+ : baseUrl.includes('dashscope') ? 'Alibaba Cloud'
842
+ : 'Custom';
843
+ if (config.ui?.renderer === 'v2') {
844
+ console.log();
845
+ console.log((0, ui_v2_1.renderV2ShellHeader)({
846
+ provider,
847
+ model: config.model,
848
+ projectPath: process.cwd(),
849
+ status: llm ? 'ready' : 'loading',
850
+ statusText: llm ? 'ready' : 'Set OPENHORSE_API_KEY',
851
+ version: VERSION,
852
+ }));
853
+ return;
854
+ }
477
855
  console.log();
478
856
  console.log((0, box_1.renderHeaderBox)({
479
- provider: baseUrl.includes('anthropic') ? 'Anthropic'
480
- : baseUrl.includes('openai') ? 'OpenAI'
481
- : baseUrl.includes('dashscope') ? 'Alibaba Cloud'
482
- : 'Custom',
857
+ provider,
483
858
  model: config.model,
484
859
  endpoint: baseUrl,
485
860
  status: llm ? 'ready' : 'loading',
@@ -496,7 +871,9 @@ function getPrompt() {
496
871
  const modeIndicator = modeText ? `[${modeText}] ` : '';
497
872
  return ACCENT('❯ ') + DIM(modeIndicator);
498
873
  }
499
- async function handleInput(input) {
874
+ async function handleInput(input, abortSignal, options = {}) {
875
+ const redrawPrompt = options.redrawPrompt !== false;
876
+ const shouldUpdateStatus = options.updateStatus !== false;
500
877
  const text = input.trim();
501
878
  if (!text)
502
879
  return;
@@ -509,13 +886,21 @@ async function handleInput(input) {
509
886
  llm,
510
887
  runtime,
511
888
  sessionId: currentSession?.id,
889
+ ensureSession: ensureCurrentSession,
890
+ setSession: setCurrentSession,
891
+ getSession: getCurrentSession,
892
+ abortSignal,
893
+ writeOutput: command_panel_1.writeOutputPreservingInput,
894
+ writeLine: command_panel_1.writeLinePreservingInput,
512
895
  };
896
+ let pendingSessionPicker;
513
897
  try {
514
898
  const parsed = (0, parser_1.parseInput)(text);
515
899
  if (parsed.isCommand) {
516
900
  const cmd = (0, commands_1.findCommand)(parsed.name);
517
901
  if (cmd) {
518
902
  const result = await cmd.execute(ctx, parsed.args);
903
+ pendingSessionPicker = result.sessionPicker;
519
904
  // executeChat 会被 cmd.execute 调用,如果需要
520
905
  if (!result.continueAsChat) {
521
906
  // 命令完成后的输出已经在 cmd.execute 中处理
@@ -540,9 +925,16 @@ async function handleInput(input) {
540
925
  console.log(ERROR(`Error: ${err.message || String(err)}`));
541
926
  }
542
927
  // 显示状态栏
543
- updateStatusBar();
928
+ if (shouldUpdateStatus) {
929
+ updateStatusBar();
930
+ }
544
931
  // 重新显示 prompt
545
- (0, command_panel_1.redrawInputWithPrompt)(currentInput);
932
+ if (redrawPrompt) {
933
+ (0, command_panel_1.redrawInputWithPrompt)(currentInput);
934
+ }
935
+ if (pendingSessionPicker) {
936
+ showResumePicker(pendingSessionPicker);
937
+ }
546
938
  }
547
939
  /**
548
940
  * 更新状态栏显示
@@ -563,7 +955,17 @@ function updateStatusBar() {
563
955
  mcpTotal: mcpStatus.length,
564
956
  };
565
957
  // 在 prompt 上一行显示状态栏
566
- console.log((0, status_bar_1.renderStatusBar)(stats));
958
+ if (snapshot.config.ui?.renderer === 'v2') {
959
+ (0, command_panel_1.setInputStatusText)((0, ui_v2_1.renderV2StatusBadge)({
960
+ ...stats,
961
+ sessionId: currentSession?.id,
962
+ modeText: (0, types_1.getModeDisplayText)(snapshot.permissionMode) || undefined,
963
+ width: process.stdout.columns || 80,
964
+ }));
965
+ }
966
+ else {
967
+ console.log((0, status_bar_1.renderStatusBar)(stats));
968
+ }
567
969
  }
568
970
  // 接口变量(用于兼容性,主要逻辑通过 keypress 处理)
569
971
  let rl = null;
@@ -584,7 +986,13 @@ async function main() {
584
986
  (0, config_dir_1.ensureConfigDir)();
585
987
  (0, global_config_1.recordFirstStartTime)();
586
988
  const projectPath = process.cwd();
587
- const cliConfig = (0, config_1.loadConfig)();
989
+ const uiRenderer = parseCliUIRenderer(args);
990
+ const cliConfig = (0, config_1.loadConfig)(uiRenderer ? { ui: { renderer: uiRenderer } } : {});
991
+ (0, command_panel_1.setInputPromptRenderer)(cliConfig.ui?.renderer === 'v2' ? 'v2' : 'legacy');
992
+ (0, command_panel_1.setInputRenderContextProvider)(() => (cliConfig.ui?.renderer === 'v2' && (0, multiline_input_1.isMultilineActive)()
993
+ ? { prefixLines: (0, multiline_input_1.getMultilineLines)() }
994
+ : {}));
995
+ (0, file_completion_1.setFileCompletionPromptRenderer)(cliConfig.ui?.renderer === 'v2' ? 'v2' : 'legacy');
588
996
  // Load project memory
589
997
  const memories = (0, storage_1.loadAllMemories)(projectPath);
590
998
  const memoryContent = memories.length > 0
@@ -615,8 +1023,6 @@ async function main() {
615
1023
  currentPlan: s.currentPlan,
616
1024
  });
617
1025
  });
618
- currentSession = (0, session_storage_1.createSession)(projectPath, cliConfig.model);
619
- (0, global_config_1.incrementSessionCount)();
620
1026
  if ((0, config_1.isConfigured)(cliConfig)) {
621
1027
  try {
622
1028
  llm = new llm_1.LLMService({
@@ -656,6 +1062,10 @@ async function main() {
656
1062
  // 提示
657
1063
  console.log(SUCCESS('✔ System initialized'));
658
1064
  console.log(DIM(' Type /help for commands, /exit to quit'));
1065
+ if (cliConfig.ui?.renderer === 'v2') {
1066
+ console.log(DIM(' UI v2 preview enabled: command palette and session picker'));
1067
+ console.log((0, ui_v2_1.renderV2FooterHint)(process.stdout.columns || 80));
1068
+ }
659
1069
  if (!(0, config_1.isConfigured)(cliConfig)) {
660
1070
  console.log(WARN(' ⚠ LLM not configured — set OPENHORSE_API_KEY'));
661
1071
  }
@@ -687,6 +1097,9 @@ async function main() {
687
1097
  }
688
1098
  });
689
1099
  // 初始 prompt
1100
+ if (cliConfig.ui?.renderer === 'v2') {
1101
+ updateStatusBar();
1102
+ }
690
1103
  (0, command_panel_1.redrawInputWithPrompt)('');
691
1104
  }
692
1105
  main().catch(err => {