centaurus-cli 2.7.2 → 2.8.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 (86) hide show
  1. package/dist/cli-adapter.d.ts +8 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +104 -37
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/mcp-config-manager.d.ts +53 -0
  6. package/dist/config/mcp-config-manager.d.ts.map +1 -0
  7. package/dist/config/mcp-config-manager.js +210 -0
  8. package/dist/config/mcp-config-manager.js.map +1 -0
  9. package/dist/config/slash-commands.d.ts +14 -0
  10. package/dist/config/slash-commands.d.ts.map +1 -0
  11. package/dist/config/slash-commands.js +65 -0
  12. package/dist/config/slash-commands.js.map +1 -0
  13. package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
  14. package/dist/context/handlers/wsl-handler.js +2 -1
  15. package/dist/context/handlers/wsl-handler.js.map +1 -1
  16. package/dist/index.js +20 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/mcp/mcp-command-handler.d.ts +16 -0
  19. package/dist/mcp/mcp-command-handler.d.ts.map +1 -0
  20. package/dist/mcp/mcp-command-handler.js +196 -0
  21. package/dist/mcp/mcp-command-handler.js.map +1 -0
  22. package/dist/mcp/mcp-server-manager.d.ts +30 -0
  23. package/dist/mcp/mcp-server-manager.d.ts.map +1 -0
  24. package/dist/mcp/mcp-server-manager.js +165 -0
  25. package/dist/mcp/mcp-server-manager.js.map +1 -0
  26. package/dist/mcp/mcp-tool-wrapper.d.ts +12 -0
  27. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -0
  28. package/dist/mcp/mcp-tool-wrapper.js +57 -0
  29. package/dist/mcp/mcp-tool-wrapper.js.map +1 -0
  30. package/dist/services/ai-service-client.d.ts.map +1 -1
  31. package/dist/services/ai-service-client.js +20 -2
  32. package/dist/services/ai-service-client.js.map +1 -1
  33. package/dist/tools/command.js.map +1 -1
  34. package/dist/tools/file-ops.d.ts.map +1 -1
  35. package/dist/tools/file-ops.js +18 -10
  36. package/dist/tools/file-ops.js.map +1 -1
  37. package/dist/ui/components/App.d.ts +4 -0
  38. package/dist/ui/components/App.d.ts.map +1 -1
  39. package/dist/ui/components/App.js +235 -138
  40. package/dist/ui/components/App.js.map +1 -1
  41. package/dist/ui/components/FileCreationPreview.d.ts +8 -0
  42. package/dist/ui/components/FileCreationPreview.d.ts.map +1 -0
  43. package/dist/ui/components/FileCreationPreview.js +63 -0
  44. package/dist/ui/components/FileCreationPreview.js.map +1 -0
  45. package/dist/ui/components/InputBox.d.ts.map +1 -1
  46. package/dist/ui/components/InputBox.js +132 -4
  47. package/dist/ui/components/InputBox.js.map +1 -1
  48. package/dist/ui/components/InteractiveShell.d.ts +3 -1
  49. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  50. package/dist/ui/components/InteractiveShell.js +46 -89
  51. package/dist/ui/components/InteractiveShell.js.map +1 -1
  52. package/dist/ui/components/MarkdownRenderer.d.ts.map +1 -1
  53. package/dist/ui/components/MarkdownRenderer.js +16 -34
  54. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  55. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  56. package/dist/ui/components/MessageDisplay.js +10 -2
  57. package/dist/ui/components/MessageDisplay.js.map +1 -1
  58. package/dist/ui/components/SlashCommandAutocomplete.d.ts +11 -0
  59. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -0
  60. package/dist/ui/components/SlashCommandAutocomplete.js +15 -0
  61. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -0
  62. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  63. package/dist/ui/components/StreamingMessageDisplay.js +5 -5
  64. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  65. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  66. package/dist/ui/components/ToolExecutionMessage.js +96 -32
  67. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  68. package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
  69. package/dist/ui/components/ToolExecutionStatus.js +28 -1
  70. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  71. package/dist/utils/editor-utils.d.ts +50 -0
  72. package/dist/utils/editor-utils.d.ts.map +1 -0
  73. package/dist/utils/editor-utils.js +501 -0
  74. package/dist/utils/editor-utils.js.map +1 -0
  75. package/dist/utils/input-classifier.d.ts.map +1 -1
  76. package/dist/utils/input-classifier.js +4 -2
  77. package/dist/utils/input-classifier.js.map +1 -1
  78. package/dist/utils/shell.d.ts +34 -1
  79. package/dist/utils/shell.d.ts.map +1 -1
  80. package/dist/utils/shell.js +130 -123
  81. package/dist/utils/shell.js.map +1 -1
  82. package/dist/utils/syntax-checker.d.ts +24 -0
  83. package/dist/utils/syntax-checker.d.ts.map +1 -0
  84. package/dist/utils/syntax-checker.js +320 -0
  85. package/dist/utils/syntax-checker.js.map +1 -0
  86. package/package.json +5 -3
@@ -11,12 +11,13 @@ import { AgentTimer } from './AgentTimer.js';
11
11
  import { SelectPrompt } from './SelectPrompt.js';
12
12
  import { ConfirmPrompt } from './ConfirmPrompt.js';
13
13
  import { KeyboardHelp } from './KeyboardHelp.js';
14
- import { CodeBlock } from './CodeBlock.js';
14
+ import { FileCreationPreview } from './FileCreationPreview.js';
15
15
  import { DiffViewer } from './DiffViewer.js';
16
16
  import { PasswordPrompt } from './PasswordPrompt.js';
17
17
  import { VersionUpdatePrompt } from './VersionUpdatePrompt.js';
18
18
  import { InteractiveShell } from './InteractiveShell.js';
19
19
  import { checkForUpdates } from '../../utils/version-checker.js';
20
+ import { runInteractiveEditor, runWSLEditor, runDockerEditor, runSSHEditor } from '../../utils/editor-utils.js';
20
21
  // Banner item with stable timestamp - created once outside component
21
22
  const BANNER_ITEM = { id: '__banner__', role: '__banner__', content: '', timestamp: new Date(0) };
22
23
  const MessageList = React.memo(({ history, current, showBanner }) => {
@@ -31,13 +32,22 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
31
32
  }
32
33
  return React.createElement(MessageDisplay, { key: item.id, message: item });
33
34
  }),
34
- current && (current.role === 'assistant' && current.shouldStream !== false ? (React.createElement(StreamingMessageDisplay, { key: current.id, message: current })) : (React.createElement(MessageDisplay, { key: current.id, message: current })))));
35
+ current && !history.some(msg => msg.id === current.id) && (current.role === 'assistant' && current.shouldStream !== false ? (React.createElement(StreamingMessageDisplay, { key: current.id, message: current })) : (React.createElement(MessageDisplay, { key: current.id, message: current })))));
35
36
  }, (prevProps, nextProps) => {
36
37
  // Custom comparison to prevent unnecessary re-renders
37
38
  // Only re-render if history length changed, current message changed, or showBanner changed
39
+ // IMPORTANT: Also check thoughts for thinking updates
40
+ const prevThoughts = prevProps.current?.thoughts;
41
+ const nextThoughts = nextProps.current?.thoughts;
42
+ const thoughtsEqual = prevThoughts === nextThoughts ||
43
+ (prevThoughts && nextThoughts &&
44
+ prevThoughts.length === nextThoughts.length &&
45
+ prevThoughts.every((t, i) => t === nextThoughts[i]));
38
46
  return (prevProps.history.length === nextProps.history.length &&
39
47
  prevProps.current?.id === nextProps.current?.id &&
40
48
  prevProps.current?.content === nextProps.current?.content &&
49
+ prevProps.current?.thinkingDuration === nextProps.current?.thinkingDuration &&
50
+ thoughtsEqual &&
41
51
  prevProps.showBanner === nextProps.showBanner);
42
52
  });
43
53
  const ApprovalSection = React.memo(({ approvalRequest, onApprove }) => {
@@ -45,14 +55,14 @@ const ApprovalSection = React.memo(({ approvalRequest, onApprove }) => {
45
55
  const handleYes = React.useCallback(() => onApprove(true), [onApprove]);
46
56
  const handleNo = React.useCallback(() => onApprove(false), [onApprove]);
47
57
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
48
- approvalRequest.preview && approvalRequest.preview.type === 'code' && (React.createElement(CodeBlock, { code: approvalRequest.preview.content, language: approvalRequest.preview.language, title: "File Preview" })),
58
+ approvalRequest.preview && approvalRequest.preview.type === 'code' && (React.createElement(FileCreationPreview, { content: approvalRequest.preview.content, filePath: approvalRequest.operationDetails?.file_path || 'New File' })),
49
59
  approvalRequest.preview && approvalRequest.preview.type === 'diff' && (React.createElement(DiffViewer, { diff: approvalRequest.preview.content, filePath: 'Preview', fullDiff: approvalRequest.preview.fullDiff })),
50
60
  React.createElement(ConfirmPrompt, { message: approvalRequest.message, onYes: handleYes, onNo: handleNo })));
51
61
  }, (prevProps, nextProps) => {
52
62
  // Only re-render if the approval request message changes
53
63
  return prevProps.approvalRequest.message === nextProps.approvalRequest.message;
54
64
  });
55
- export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onResponseStream, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onCommandModeChange, onToggleCommandMode, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput }) => {
65
+ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onCommandModeChange, onToggleCommandMode, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode }) => {
56
66
  const { exit } = useApp();
57
67
  const autoAcceptRef = React.useRef(false);
58
68
  // Helper to clear screen
@@ -109,6 +119,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
109
119
  currentTokens: 0,
110
120
  maxTokens: getMaxTokensForModel(initialModel || 'gemini-2.5-flash'),
111
121
  shellState: undefined,
122
+ isInteractiveEditorMode: false,
112
123
  pickerOptions: undefined,
113
124
  approvalRequest: undefined,
114
125
  planApprovalRequest: undefined,
@@ -289,6 +300,32 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
289
300
  });
290
301
  });
291
302
  }, [onThoughtComplete]);
303
+ // Set up callback for direct messages (slash commands) - add directly to history
304
+ React.useEffect(() => {
305
+ onDirectMessage((message) => {
306
+ setState(prev => {
307
+ // Move current message to history if exists
308
+ const newHistory = prev.currentMessage
309
+ ? [...prev.messageHistory, prev.currentMessage]
310
+ : prev.messageHistory;
311
+ // Create system message for slash command response
312
+ const systemMessage = {
313
+ id: `system-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
314
+ role: 'assistant', // Display as assistant message
315
+ content: message,
316
+ timestamp: new Date(),
317
+ shouldStream: false // Don't stream system messages
318
+ };
319
+ return {
320
+ ...prev,
321
+ messageHistory: [...newHistory, systemMessage],
322
+ currentMessage: null,
323
+ isLoading: false,
324
+ isAiWorking: false
325
+ };
326
+ });
327
+ });
328
+ }, [onDirectMessage]);
292
329
  // Set up callback to receive complete responses (for slash commands and non-streaming responses)
293
330
  React.useEffect(() => {
294
331
  onResponseReceived((message) => {
@@ -380,6 +417,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
380
417
  });
381
418
  });
382
419
  }, [onResponseReceived]);
420
+ // Ref to track pending shell start (to avoid flickering for fast commands)
421
+ const pendingShellRef = React.useRef(null);
383
422
  // Set up callback to show pickers - only once on mount
384
423
  React.useEffect(() => {
385
424
  onPickerSetup((options) => {
@@ -395,38 +434,110 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
395
434
  // Set up callback for tool execution updates
396
435
  React.useEffect(() => {
397
436
  onToolExecutionUpdate((update) => {
398
- setState(prev => {
399
- // Don't add tool messages if we're not on chat screen
400
- if (prev.screen !== 'chat') {
401
- return prev;
437
+ // Special handling for execute_command
438
+ if (update.toolName === 'execute_command') {
439
+ try {
440
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] execute_command update: ${JSON.stringify(update)}\n`);
402
441
  }
403
- // Special handling for execute_command - use interactive shell
404
- if (update.toolName === 'execute_command') {
405
- try {
406
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] execute_command update: ${JSON.stringify(update)}\n`);
407
- }
408
- catch (e) { }
409
- if (update.status === 'executing') {
410
- const command = update.arguments?.command || update.arguments?.CommandLine || update.arguments?.commandLine || '';
411
- return {
412
- ...prev,
413
- shellState: {
414
- command,
415
- cwd: prev.currentWorkingDirectory,
416
- output: '',
417
- isRunning: true,
418
- exitCode: undefined,
419
- error: undefined,
420
- isFocused: false
421
- },
422
- isLoading: false,
423
- isAiWorking: true
424
- };
442
+ catch (e) { }
443
+ // STARTING EXECUTION
444
+ if (update.status === 'executing') {
445
+ const command = update.arguments?.command || update.arguments?.CommandLine || update.arguments?.commandLine || '';
446
+ // Start a pending shell with a short delay
447
+ // This prevents the UI from flickering for very fast commands (like ls)
448
+ // and prevents duplication by allowing us to skip the interactive shell entirely
449
+ const timeoutId = setTimeout(() => {
450
+ // If this timeout fires, it means the command is taking longer than 50ms
451
+ // So we should show the interactive shell
452
+ if (pendingShellRef.current) {
453
+ const bufferedOutput = pendingShellRef.current.output;
454
+ const cmd = pendingShellRef.current.command;
455
+ const cwd = pendingShellRef.current.cwd;
456
+ pendingShellRef.current = null; // Clear pending state
457
+ setState(prev => ({
458
+ ...prev,
459
+ shellState: {
460
+ command: cmd,
461
+ cwd: cwd,
462
+ output: bufferedOutput,
463
+ isRunning: true,
464
+ exitCode: undefined,
465
+ error: undefined,
466
+ isFocused: false,
467
+ isPty: update.arguments?.isPty || false
468
+ },
469
+ // IMPORTANT: Always preserve current message by moving it to history first
470
+ messageHistory: prev.currentMessage
471
+ ? [...prev.messageHistory, prev.currentMessage]
472
+ : prev.messageHistory,
473
+ currentMessage: {
474
+ id: `tool-execute_command-${Date.now()}`,
475
+ role: 'tool',
476
+ content: '',
477
+ timestamp: new Date(),
478
+ toolExecution: { ...update }
479
+ },
480
+ isLoading: false,
481
+ isAiWorking: true
482
+ }));
483
+ }
484
+ }, 50);
485
+ pendingShellRef.current = {
486
+ timeoutId,
487
+ command,
488
+ cwd: state.currentWorkingDirectory,
489
+ output: ''
490
+ };
491
+ return;
492
+ }
493
+ // COMPLETION / ERROR
494
+ if (update.status === 'completed' || update.status === 'error') {
495
+ // Check if we have a pending shell (fast command)
496
+ if (pendingShellRef.current) {
497
+ // Command finished VERY fast (within 50ms)
498
+ // Clear the timeout so we don't show the interactive shell
499
+ clearTimeout(pendingShellRef.current.timeoutId);
500
+ const bufferedOutput = pendingShellRef.current.output;
501
+ pendingShellRef.current = null;
502
+ // Add directly to history
503
+ setState(prev => {
504
+ const finalOutput = bufferedOutput + (update.result || '');
505
+ const errorMsg = update.error;
506
+ const shellMessage = {
507
+ id: `shell-${Date.now()}`,
508
+ role: 'tool',
509
+ content: '',
510
+ timestamp: new Date(),
511
+ toolExecution: {
512
+ toolName: 'execute_command',
513
+ status: update.status,
514
+ result: finalOutput,
515
+ error: errorMsg,
516
+ arguments: update.arguments
517
+ }
518
+ };
519
+ // Move current message to history if exists
520
+ const newHistory = prev.currentMessage
521
+ ? [...prev.messageHistory, prev.currentMessage]
522
+ : prev.messageHistory;
523
+ return {
524
+ ...prev,
525
+ shellState: undefined,
526
+ messageHistory: [...newHistory, shellMessage],
527
+ currentMessage: null,
528
+ isAiWorking: prev.isAiWorking // Preserve existing state
529
+ };
530
+ });
531
+ return;
425
532
  }
426
- else if (update.status === 'completed') {
427
- // Shell completed successfully - add to history as static message
533
+ // Normal case: Interactive shell was already shown, now finishing
534
+ setState(prev => {
535
+ // Don't add tool messages if we're not on chat screen
536
+ if (prev.screen !== 'chat') {
537
+ return prev;
538
+ }
428
539
  const finalOutput = prev.shellState?.output || update.result || '';
429
- // Create a message with the shell execution details
540
+ const errorMsg = update.error;
430
541
  const shellMessage = {
431
542
  id: `shell-${Date.now()}`,
432
543
  role: 'tool',
@@ -434,31 +545,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
434
545
  timestamp: new Date(),
435
546
  toolExecution: {
436
547
  toolName: 'execute_command',
437
- status: 'completed',
438
- result: finalOutput,
439
- arguments: update.arguments
440
- }
441
- };
442
- return {
443
- ...prev,
444
- shellState: undefined,
445
- messageHistory: [...prev.messageHistory, shellMessage],
446
- currentMessage: null,
447
- isAiWorking: true
448
- };
449
- }
450
- else if (update.status === 'error') {
451
- // Shell failed - add to history as static error message
452
- const finalOutput = prev.shellState?.output || '';
453
- const errorMsg = update.error || 'Command failed';
454
- const shellMessage = {
455
- id: `shell-${Date.now()}`,
456
- role: 'tool',
457
- content: '',
458
- timestamp: new Date(),
459
- toolExecution: {
460
- toolName: 'execute_command',
461
- status: 'error',
548
+ status: update.status,
462
549
  result: finalOutput,
463
550
  error: errorMsg,
464
551
  arguments: update.arguments
@@ -469,10 +556,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
469
556
  shellState: undefined,
470
557
  messageHistory: [...prev.messageHistory, shellMessage],
471
558
  currentMessage: null,
472
- isAiWorking: true
559
+ isAiWorking: prev.isAiWorking // Preserve existing state
473
560
  };
474
- }
475
- // Early return to prevent fall-through to generic tool handling
561
+ });
562
+ return;
563
+ }
564
+ }
565
+ setState(prev => {
566
+ // Don't add tool messages if we're not on chat screen
567
+ if (prev.screen !== 'chat') {
476
568
  return prev;
477
569
  }
478
570
  const toolMessage = {
@@ -539,8 +631,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
539
631
  if (autoAcceptRef.current) {
540
632
  return true;
541
633
  }
542
- // Check if this is a file operation with preview (write_file or edit_file)
543
- const isFileOperation = (request.operationType === 'write_file' || request.operationType === 'edit_file') && request.preview;
634
+ // Check if this is a file operation with preview (write_file, write_to_file, or edit_file)
635
+ const isFileOperation = (request.operationType === 'write_file' || request.operationType === 'write_to_file' || request.operationType === 'edit_file') && request.preview;
544
636
  if (isFileOperation) {
545
637
  // File operations: Clear screen and switch to approval screen (focused view)
546
638
  clearScreen();
@@ -599,26 +691,19 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
599
691
  // Set up callback for tool streaming output
600
692
  React.useEffect(() => {
601
693
  onToolStreamingOutput((update) => {
694
+ // Handle pending shell buffer
695
+ if (update.toolName === 'execute_command' && pendingShellRef.current) {
696
+ pendingShellRef.current.output += update.chunk;
697
+ return;
698
+ }
602
699
  setState(prev => {
603
700
  // Update shell state if it's an execute_command
604
701
  if (update.toolName === 'execute_command' && prev.shellState) {
605
- const newOutput = prev.shellState.output + update.chunk;
606
- // Clear user input buffer when new output arrives
607
- // This handles cases where:
608
- // 1. Program echoes the input (input appears in output)
609
- // 2. Program outputs a newline (moving to next line)
610
- // 3. Program outputs a new prompt
611
- const shouldClearInput = prev.shellState.userInput && (
612
- // If output contains a newline, clear input (program moved to next line)
613
- update.chunk.includes('\n') ||
614
- // If output contains the user input (program echoed it), clear input
615
- (prev.shellState.userInput && newOutput.includes(prev.shellState.userInput)));
616
702
  return {
617
703
  ...prev,
618
704
  shellState: {
619
705
  ...prev.shellState,
620
- output: newOutput,
621
- userInput: shouldClearInput ? '' : prev.shellState.userInput
706
+ output: prev.shellState.output + update.chunk
622
707
  }
623
708
  };
624
709
  }
@@ -738,6 +823,64 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
738
823
  });
739
824
  });
740
825
  }, [clearScreen]);
826
+ // Set up callback for interactive editor mode (vim, nano, etc.)
827
+ // Ref to hold the current editor process for cleanup
828
+ const editorProcessRef = React.useRef(null);
829
+ React.useEffect(() => {
830
+ onInteractiveEditorMode((active, command, cwd, remoteContext) => {
831
+ if (active && command && cwd) {
832
+ // Enter interactive editor mode
833
+ setState(prev => ({ ...prev, isInteractiveEditorMode: true }));
834
+ // Common exit handler for all editor types
835
+ const handleEditorExit = (exitCode) => {
836
+ editorProcessRef.current = null;
837
+ // Clear screen before restoring Ink UI
838
+ clearScreen();
839
+ // Editor exited, restore Ink UI
840
+ setState(prev => ({ ...prev, isInteractiveEditorMode: false }));
841
+ };
842
+ // Wait a frame for React to process the state change
843
+ setTimeout(() => {
844
+ // Choose appropriate editor runner based on context
845
+ if (!remoteContext || remoteContext.type === 'local') {
846
+ // Local context: use standard interactive editor
847
+ editorProcessRef.current = runInteractiveEditor(command, cwd, handleEditorExit);
848
+ }
849
+ else if (remoteContext.type === 'wsl') {
850
+ // WSL context: use WSL editor with distribution name
851
+ const distribution = remoteContext.metadata?.distroName || 'Ubuntu';
852
+ editorProcessRef.current = runWSLEditor(distribution, command, cwd, handleEditorExit);
853
+ }
854
+ else if (remoteContext.type === 'docker') {
855
+ // Docker context: use Docker editor with container ID
856
+ const containerId = remoteContext.metadata?.containerId || '';
857
+ editorProcessRef.current = runDockerEditor(containerId, command, cwd, handleEditorExit);
858
+ }
859
+ else if (remoteContext.type === 'ssh') {
860
+ // SSH context: use SSH editor with client
861
+ const sshClient = remoteContext.handler?.client;
862
+ if (sshClient) {
863
+ editorProcessRef.current = runSSHEditor(sshClient, command, cwd, handleEditorExit);
864
+ }
865
+ else {
866
+ // Fallback if no client available
867
+ console.error('SSH client not available for editor mode');
868
+ setState(prev => ({ ...prev, isInteractiveEditorMode: false }));
869
+ }
870
+ }
871
+ }, 50);
872
+ }
873
+ else {
874
+ // Exit interactive editor mode (cleanup)
875
+ if (editorProcessRef.current) {
876
+ editorProcessRef.current.kill();
877
+ editorProcessRef.current = null;
878
+ }
879
+ clearScreen();
880
+ setState(prev => ({ ...prev, isInteractiveEditorMode: false }));
881
+ }
882
+ });
883
+ }, [onInteractiveEditorMode, clearScreen]);
741
884
  // Track Ctrl+C presses for double-press exit
742
885
  const ctrlCPressedRef = React.useRef(false);
743
886
  const ctrlCTimeoutRef = React.useRef(null);
@@ -800,6 +943,11 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
800
943
  }
801
944
  // Ctrl+C handling - double press to exit
802
945
  if (key.ctrl && input === 'c') {
946
+ // If shell is running, kill it
947
+ if (state.shellState && state.shellState.isRunning) {
948
+ onKillProcess();
949
+ return;
950
+ }
803
951
  // If AI is loading/executing, first Ctrl+C cancels the operation
804
952
  if (state.isLoading || state.isAiWorking) {
805
953
  // Cancel the current AI request
@@ -855,7 +1003,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
855
1003
  }, 5000);
856
1004
  return;
857
1005
  }
858
- });
1006
+ }, { isActive: !state.isInteractiveEditorMode });
859
1007
  const handleSubmit = useCallback(async (value) => {
860
1008
  // Trim the value to remove any leading/trailing whitespace or newlines
861
1009
  const trimmedValue = value.trim();
@@ -985,67 +1133,17 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
985
1133
  autoAcceptMode: !prev.autoAcceptMode
986
1134
  }));
987
1135
  }, []);
1136
+ // If in interactive editor mode, render minimal UI to keep Ink mounted
1137
+ // but don't render any visible content - the editor has the screen
1138
+ if (state.isInteractiveEditorMode) {
1139
+ return React.createElement(Box, null);
1140
+ }
988
1141
  return (React.createElement(Box, { flexDirection: "column" },
989
1142
  state.screen === 'chat' && (React.createElement(React.Fragment, null,
990
1143
  React.createElement(MessageList, { history: state.messageHistory, current: state.currentMessage, showBanner: true }),
991
- state.shellState && (React.createElement(InteractiveShell, { command: state.shellState.command, cwd: state.shellState.cwd, isRunning: state.shellState.isRunning, output: state.shellState.output, exitCode: state.shellState.exitCode, error: state.shellState.error, isFocused: state.shellState.isFocused, userInput: state.shellState.userInput, onInput: (input) => {
992
- // ============================================
993
- // LINE-BUFFERED INPUT HANDLING
994
- // ============================================
995
- // Regular characters are buffered locally.
996
- // Complete line is sent when Enter is pressed.
997
- // Special keys (arrows, Ctrl+C, etc.) are sent immediately.
998
- setState(prev => {
999
- if (!prev.shellState)
1000
- return prev;
1001
- // ============================================
1002
- // ENTER - Send complete line to process
1003
- // ============================================
1004
- if (input.includes('\r\n') || input.includes('\n')) {
1005
- // Send to process
1006
- onShellInput(input);
1007
- // Keep input visible until program responds
1008
- return prev;
1009
- }
1010
- // ============================================
1011
- // BACKSPACE - Remove from buffer
1012
- // ============================================
1013
- if (input === '\x08') {
1014
- const currentInput = prev.shellState.userInput || '';
1015
- return {
1016
- ...prev,
1017
- shellState: {
1018
- ...prev.shellState,
1019
- userInput: currentInput.slice(0, -1)
1020
- }
1021
- };
1022
- }
1023
- // ============================================
1024
- // CONTROL KEYS - Send immediately, don't buffer
1025
- // ============================================
1026
- if (input.length === 1 && input.charCodeAt(0) < 32 && input !== '\t') {
1027
- onShellInput(input);
1028
- return prev;
1029
- }
1030
- // ============================================
1031
- // ANSI SEQUENCES - Send immediately, don't buffer
1032
- // ============================================
1033
- if (input.startsWith('\x1b')) {
1034
- onShellInput(input);
1035
- return prev;
1036
- }
1037
- // ============================================
1038
- // REGULAR CHARACTERS - Add to buffer only
1039
- // ============================================
1040
- // Do NOT send to process yet - wait for Enter
1041
- return {
1042
- ...prev,
1043
- shellState: {
1044
- ...prev.shellState,
1045
- userInput: (prev.shellState.userInput || '') + input
1046
- }
1047
- };
1048
- });
1144
+ state.shellState && (React.createElement(InteractiveShell, { command: state.shellState.command, cwd: state.shellState.cwd, isRunning: state.shellState.isRunning, output: state.shellState.output, exitCode: state.shellState.exitCode, error: state.shellState.error, isFocused: state.shellState.isFocused, onResize: state.shellState.onResize, onInput: (input) => {
1145
+ // PTY MODE: Send everything immediately
1146
+ onShellInput(input);
1049
1147
  }, onFocusChange: (focused) => {
1050
1148
  setState(prev => ({
1051
1149
  ...prev,
@@ -1054,6 +1152,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1054
1152
  isFocused: focused
1055
1153
  } : undefined
1056
1154
  }));
1155
+ }, onSignal: (signal) => {
1156
+ onShellSignal(signal);
1057
1157
  } })),
1058
1158
  state.isAiWorking && !state.shellState && (React.createElement(Box, { marginBottom: 1, paddingLeft: 1 },
1059
1159
  React.createElement(LoadingIndicator, { key: "loading-indicator" }),
@@ -1156,14 +1256,12 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1156
1256
  exit();
1157
1257
  // Clear screen before showing install message
1158
1258
  process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
1159
- console.log('\nšŸ”„ Installing latest version...\n');
1160
1259
  const installProcess = spawn('npm', ['install', '-g', 'centaurus-cli@latest'], {
1161
1260
  stdio: 'inherit',
1162
1261
  shell: true
1163
1262
  });
1164
1263
  installProcess.on('close', (code) => {
1165
1264
  if (code === 0) {
1166
- console.log('\nāœ… Update complete! Restarting Centaurus CLI...\n');
1167
1265
  // Restart the CLI
1168
1266
  const restartProcess = spawn('centaurus', [], {
1169
1267
  stdio: 'inherit',
@@ -1174,7 +1272,6 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1174
1272
  process.exit(0);
1175
1273
  }
1176
1274
  else {
1177
- console.log('\nāŒ Update failed. Please try manually: npm install -g centaurus-cli@latest\n');
1178
1275
  process.exit(1);
1179
1276
  }
1180
1277
  });