centaurus-cli 2.7.3 → 2.8.1

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 (97) hide show
  1. package/dist/cli-adapter.d.ts +10 -6
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +613 -154
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/slash-commands.d.ts.map +1 -1
  6. package/dist/config/slash-commands.js +1 -0
  7. package/dist/config/slash-commands.js.map +1 -1
  8. package/dist/context/context-manager.d.ts +4 -1
  9. package/dist/context/context-manager.d.ts.map +1 -1
  10. package/dist/context/context-manager.js +30 -7
  11. package/dist/context/context-manager.js.map +1 -1
  12. package/dist/context/handlers/wsl-handler.d.ts +10 -0
  13. package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
  14. package/dist/context/handlers/wsl-handler.js +31 -2
  15. package/dist/context/handlers/wsl-handler.js.map +1 -1
  16. package/dist/index.js +33 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/services/ai-service-client.d.ts +1 -0
  19. package/dist/services/ai-service-client.d.ts.map +1 -1
  20. package/dist/services/ai-service-client.js +20 -0
  21. package/dist/services/ai-service-client.js.map +1 -1
  22. package/dist/tools/command.d.ts.map +1 -1
  23. package/dist/tools/command.js +136 -21
  24. package/dist/tools/command.js.map +1 -1
  25. package/dist/tools/file-ops.d.ts +1 -0
  26. package/dist/tools/file-ops.d.ts.map +1 -1
  27. package/dist/tools/file-ops.js +144 -3
  28. package/dist/tools/file-ops.js.map +1 -1
  29. package/dist/tools/inspect-symbol.js +27 -27
  30. package/dist/tools/inspect-symbol.js.map +1 -1
  31. package/dist/tools/plan-mode.d.ts +55 -19
  32. package/dist/tools/plan-mode.d.ts.map +1 -1
  33. package/dist/tools/plan-mode.js +204 -123
  34. package/dist/tools/plan-mode.js.map +1 -1
  35. package/dist/tools/types.d.ts +1 -1
  36. package/dist/tools/types.d.ts.map +1 -1
  37. package/dist/types/index.d.ts +11 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/ui/components/App.d.ts +6 -5
  40. package/dist/ui/components/App.d.ts.map +1 -1
  41. package/dist/ui/components/App.js +277 -125
  42. package/dist/ui/components/App.js.map +1 -1
  43. package/dist/ui/components/InputBox.d.ts.map +1 -1
  44. package/dist/ui/components/InputBox.js +24 -5
  45. package/dist/ui/components/InputBox.js.map +1 -1
  46. package/dist/ui/components/InteractiveShell.d.ts +2 -1
  47. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  48. package/dist/ui/components/InteractiveShell.js +41 -106
  49. package/dist/ui/components/InteractiveShell.js.map +1 -1
  50. package/dist/ui/components/MarkdownRenderer.d.ts.map +1 -1
  51. package/dist/ui/components/MarkdownRenderer.js +12 -8
  52. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  53. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  54. package/dist/ui/components/MessageDisplay.js +11 -3
  55. package/dist/ui/components/MessageDisplay.js.map +1 -1
  56. package/dist/ui/components/PlanAcceptedMessage.d.ts +12 -0
  57. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -0
  58. package/dist/ui/components/PlanAcceptedMessage.js +22 -0
  59. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -0
  60. package/dist/ui/components/PlanReviewScreen.d.ts +14 -0
  61. package/dist/ui/components/PlanReviewScreen.d.ts.map +1 -0
  62. package/dist/ui/components/PlanReviewScreen.js +52 -0
  63. package/dist/ui/components/PlanReviewScreen.js.map +1 -0
  64. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  65. package/dist/ui/components/StreamingMessageDisplay.js +5 -5
  66. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  67. package/dist/ui/components/TaskCompletedMessage.d.ts +14 -0
  68. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -0
  69. package/dist/ui/components/TaskCompletedMessage.js +25 -0
  70. package/dist/ui/components/TaskCompletedMessage.js.map +1 -0
  71. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  72. package/dist/ui/components/ToolExecutionMessage.js +174 -17
  73. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  74. package/dist/utils/conversation-logger.d.ts +127 -0
  75. package/dist/utils/conversation-logger.d.ts.map +1 -0
  76. package/dist/utils/conversation-logger.js +283 -0
  77. package/dist/utils/conversation-logger.js.map +1 -0
  78. package/dist/utils/editor-utils.d.ts +87 -0
  79. package/dist/utils/editor-utils.d.ts.map +1 -0
  80. package/dist/utils/editor-utils.js +712 -0
  81. package/dist/utils/editor-utils.js.map +1 -0
  82. package/dist/utils/input-classifier.d.ts.map +1 -1
  83. package/dist/utils/input-classifier.js +12 -4
  84. package/dist/utils/input-classifier.js.map +1 -1
  85. package/dist/utils/markdown-parser.d.ts.map +1 -1
  86. package/dist/utils/markdown-parser.js +4 -2
  87. package/dist/utils/markdown-parser.js.map +1 -1
  88. package/dist/utils/shell.d.ts +32 -1
  89. package/dist/utils/shell.d.ts.map +1 -1
  90. package/dist/utils/shell.js +97 -161
  91. package/dist/utils/shell.js.map +1 -1
  92. package/dist/utils/syntax-checker.d.ts +24 -0
  93. package/dist/utils/syntax-checker.d.ts.map +1 -0
  94. package/dist/utils/syntax-checker.js +320 -0
  95. package/dist/utils/syntax-checker.js.map +1 -0
  96. package/package.json +4 -3
  97. package/prompts/system-prompt-autonomous.md +0 -377
@@ -17,6 +17,10 @@ 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';
21
+ import { PlanReviewScreen } from './PlanReviewScreen.js';
22
+ import { TaskCompletedMessage } from './TaskCompletedMessage.js';
23
+ import { PlanAcceptedMessage } from './PlanAcceptedMessage.js';
20
24
  // Banner item with stable timestamp - created once outside component
21
25
  const BANNER_ITEM = { id: '__banner__', role: '__banner__', content: '', timestamp: new Date(0) };
22
26
  const MessageList = React.memo(({ history, current, showBanner }) => {
@@ -29,6 +33,15 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
29
33
  if (item.id === '__banner__') {
30
34
  return React.createElement(WelcomeBanner, { key: "__banner__" });
31
35
  }
36
+ // Special rendering for task completion messages
37
+ const msg = item;
38
+ if (msg.taskCompletion) {
39
+ return (React.createElement(TaskCompletedMessage, { key: item.id, taskNumber: msg.taskCompletion.taskNumber, totalTasks: msg.taskCompletion.totalTasks, taskDescription: msg.taskCompletion.taskDescription, completionNote: msg.taskCompletion.completionNote }));
40
+ }
41
+ // Special rendering for plan accepted messages
42
+ if (msg.planAccepted) {
43
+ return (React.createElement(PlanAcceptedMessage, { key: item.id, planTitle: msg.planAccepted.planTitle, totalTasks: msg.planAccepted.totalTasks }));
44
+ }
32
45
  return React.createElement(MessageDisplay, { key: item.id, message: item });
33
46
  }),
34
47
  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 })))));
@@ -61,7 +74,7 @@ const ApprovalSection = React.memo(({ approvalRequest, onApprove }) => {
61
74
  // Only re-render if the approval request message changes
62
75
  return prevProps.approvalRequest.message === nextProps.approvalRequest.message;
63
76
  });
64
- 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 }) => {
77
+ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onPlanCreated, onTaskCompleted, onCommandModeChange, onToggleCommandMode, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode }) => {
65
78
  const { exit } = useApp();
66
79
  const autoAcceptRef = React.useRef(false);
67
80
  // Helper to clear screen
@@ -118,6 +131,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
118
131
  currentTokens: 0,
119
132
  maxTokens: getMaxTokensForModel(initialModel || 'gemini-2.5-flash'),
120
133
  shellState: undefined,
134
+ isInteractiveEditorMode: false,
121
135
  pickerOptions: undefined,
122
136
  approvalRequest: undefined,
123
137
  planApprovalRequest: undefined,
@@ -207,10 +221,20 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
207
221
  // Ignore logging errors
208
222
  }
209
223
  setState(prev => {
210
- // If we don't have a current assistant message, create one for the thoughts
211
- if (!prev.currentMessage || prev.currentMessage.role !== 'assistant') {
224
+ // Check if current message is a thought-only message (empty content with thinkingDuration)
225
+ // These should NOT trigger new message creation - they're waiting for tool execution
226
+ const isThoughtOnlyMessage = prev.currentMessage?.role === 'assistant' &&
227
+ prev.currentMessage.content.trim() === '' &&
228
+ prev.currentMessage.thinkingDuration !== undefined;
229
+ // If we don't have a current assistant message, OR the current one already has thinkingDuration
230
+ // (meaning it's from a completed turn), create a new one for the thoughts
231
+ // BUT: Don't create new if current is a thought-only message waiting for tool
232
+ const needsNewMessage = !prev.currentMessage ||
233
+ prev.currentMessage.role !== 'assistant' ||
234
+ (prev.currentMessage.thinkingDuration !== undefined && !isThoughtOnlyMessage);
235
+ if (needsNewMessage) {
212
236
  try {
213
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Creating new assistant message for thoughts\n`);
237
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Creating new assistant message for thoughts (reason: ${!prev.currentMessage ? 'no current' : prev.currentMessage.role !== 'assistant' ? 'not assistant' : 'has thinkingDuration'})\n`);
214
238
  }
215
239
  catch (e) {
216
240
  // Ignore logging errors
@@ -229,13 +253,31 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
229
253
  shouldStream: true,
230
254
  thoughts: last3Lines
231
255
  };
256
+ // If we have a completed assistant message (with thinkingDuration), move it to history
257
+ // BUT: Don't move thought-only messages - they're waiting for tool execution
258
+ let newHistory = prev.messageHistory;
259
+ if (prev.currentMessage && prev.currentMessage.role === 'assistant' &&
260
+ prev.currentMessage.thinkingDuration !== undefined &&
261
+ prev.currentMessage.content.trim() !== '') { // Only move if has actual content
262
+ newHistory = [...prev.messageHistory, prev.currentMessage];
263
+ }
232
264
  return {
233
265
  ...prev,
266
+ messageHistory: newHistory,
234
267
  currentMessage: newMessage,
235
268
  isLoading: false,
236
269
  isAiWorking: true
237
270
  };
238
271
  }
272
+ // If current is thought-only message, just skip this new thought chunk
273
+ // (don't accumulate - the tool will handle displaying)
274
+ if (isThoughtOnlyMessage) {
275
+ try {
276
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Skipping thought chunk - current is thought-only message waiting for tool\n`);
277
+ }
278
+ catch (e) { }
279
+ return prev;
280
+ }
239
281
  // Accumulate the new thought chunk with existing thought text
240
282
  // Add a newline between chunks to separate them
241
283
  thoughtAccumulatorRef.current += '\n' + thought;
@@ -461,7 +503,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
461
503
  isRunning: true,
462
504
  exitCode: undefined,
463
505
  error: undefined,
464
- isFocused: false
506
+ isFocused: false,
507
+ isPty: update.arguments?.isPty || false
465
508
  },
466
509
  // IMPORTANT: Always preserve current message by moving it to history first
467
510
  messageHistory: prev.currentMessage
@@ -482,7 +525,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
482
525
  pendingShellRef.current = {
483
526
  timeoutId,
484
527
  command,
485
- cwd: state.currentWorkingDirectory,
528
+ // Use the cwd from update.arguments (already computed correctly in cli-adapter.ts)
529
+ // This has the correct remote CWD for WSL/Docker/SSH contexts
530
+ cwd: update.arguments?.cwd || state.currentWorkingDirectory,
486
531
  output: ''
487
532
  };
488
533
  return;
@@ -560,8 +605,40 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
560
605
  }
561
606
  }
562
607
  setState(prev => {
563
- // Don't add tool messages if we're not on chat screen
608
+ // Debug logging to trace tool display issues
609
+ try {
610
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] onToolExecutionUpdate setState - toolName: ${update.toolName}, status: ${update.status}\n`);
611
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Current state: screen=${prev.screen}, currentMessage.role=${prev.currentMessage?.role}, currentMessage.id=${prev.currentMessage?.id}, historyLen=${prev.messageHistory.length}\n`);
612
+ if (prev.currentMessage?.thinkingDuration !== undefined) {
613
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Current message has thinkingDuration: ${prev.currentMessage.thinkingDuration}s\n`);
614
+ }
615
+ }
616
+ catch (e) { }
617
+ // IMPORTANT: When on approval screen, we still need to track completed tools
618
+ // but we should NOT try to update the display (will be handled when returning to chat)
619
+ // For 'executing' status on approval screen, the tool is waiting for approval, so skip
564
620
  if (prev.screen !== 'chat') {
621
+ // Only track completed tools, skip executing tools (they need approval flow)
622
+ if (update.status === 'completed' || update.status === 'error') {
623
+ try {
624
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Tool ${update.toolName} completed while on ${prev.screen} screen, adding to history\n`);
625
+ }
626
+ catch (e) { }
627
+ // Add completed tool directly to history
628
+ const completedToolMessage = {
629
+ id: `tool-${update.toolName}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
630
+ role: 'tool',
631
+ content: '',
632
+ timestamp: new Date(),
633
+ toolExecution: { ...update }
634
+ };
635
+ return {
636
+ ...prev,
637
+ messageHistory: [...prev.messageHistory, completedToolMessage],
638
+ isAiWorking: true
639
+ };
640
+ }
641
+ // Skip 'executing' status when not on chat screen
565
642
  return prev;
566
643
  }
567
644
  const toolMessage = {
@@ -571,12 +648,16 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
571
648
  timestamp: new Date(),
572
649
  toolExecution: { ...update }
573
650
  };
574
- // If current message is a tool with same name and executing, update it and move to history
651
+ // If current message is a tool with matching execution, update it and move to history
575
652
  if (prev.currentMessage?.role === 'tool' &&
576
- prev.currentMessage.toolExecution?.toolName === update.toolName &&
577
653
  prev.currentMessage.toolExecution?.status === 'executing' &&
578
654
  update.status !== 'executing') {
579
655
  // Tool completed - move to history with updated status
656
+ // Note: We now match ANY executing tool, not just same toolName (handles overlapping tools)
657
+ try {
658
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Tool ${update.toolName} completed, moving current tool to history\n`);
659
+ }
660
+ catch (e) { }
580
661
  const completedToolMessage = {
581
662
  ...prev.currentMessage,
582
663
  toolExecution: { ...update }
@@ -585,38 +666,72 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
585
666
  ...prev,
586
667
  messageHistory: [...prev.messageHistory, completedToolMessage],
587
668
  currentMessage: null,
588
- isAiWorking: true // Keep AI working state active (more tool calls or response may follow)
669
+ isAiWorking: true
589
670
  };
590
671
  }
672
+ // For pending status, don't modify state yet - wait for executing
673
+ // This ensures thought-only messages stay as currentMessage until executing arrives
674
+ if (update.status === 'pending') {
675
+ try {
676
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Tool ${update.toolName} pending - skipping state modification, waiting for executing\n`);
677
+ }
678
+ catch (e) { }
679
+ return prev; // Return unchanged state
680
+ }
591
681
  // If starting new tool execution
592
682
  if (update.status === 'executing') {
593
683
  // IMPORTANT: Always preserve current message by moving it to history first
594
684
  let newHistory = prev.messageHistory;
685
+ let pendingThinkingDuration = undefined;
595
686
  if (prev.currentMessage) {
596
- // Move ANY current message to history before showing tool
597
- newHistory = [...prev.messageHistory, prev.currentMessage];
687
+ // Check if this is a thought-only message (empty content but has thinkingDuration)
688
+ const isThoughtOnlyMessage = prev.currentMessage.role === 'assistant' &&
689
+ prev.currentMessage.content.trim() === '' &&
690
+ prev.currentMessage.thinkingDuration !== undefined;
691
+ if (isThoughtOnlyMessage) {
692
+ // Carry the thinkingDuration to the tool message, don't add empty message to history
693
+ pendingThinkingDuration = prev.currentMessage.thinkingDuration;
694
+ try {
695
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Found thought-only message, saving thinkingDuration ${pendingThinkingDuration}s for tool ${update.toolName}\n`);
696
+ }
697
+ catch (e) { }
698
+ }
699
+ else {
700
+ try {
701
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Moving current message (${prev.currentMessage.role}) to history for new tool ${update.toolName}\n`);
702
+ }
703
+ catch (e) { }
704
+ // Move ANY current message to history before showing tool
705
+ newHistory = [...prev.messageHistory, prev.currentMessage];
706
+ }
598
707
  }
708
+ // Create tool message with thinkingDuration if applicable
709
+ const toolMessageWithDuration = {
710
+ ...toolMessage,
711
+ thinkingDuration: pendingThinkingDuration
712
+ };
599
713
  return {
600
714
  ...prev,
601
715
  messageHistory: newHistory,
602
- currentMessage: toolMessage,
716
+ currentMessage: toolMessageWithDuration,
603
717
  isLoading: false,
604
718
  isAiWorking: true
605
719
  };
606
720
  }
607
- // For completed tools without a current executing one
608
- // IMPORTANT: Preserve current message before adding tool to history
721
+ // For completed tools without a current executing one (edge case)
722
+ try {
723
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Tool ${update.toolName} completed without current executing tool, adding to history\n`);
724
+ }
725
+ catch (e) { }
609
726
  let newHistory = prev.messageHistory;
610
- // If there's a current message, move it to history first
611
727
  if (prev.currentMessage) {
612
728
  newHistory = [...prev.messageHistory, prev.currentMessage];
613
729
  }
614
- // Add tool message to history
615
730
  return {
616
731
  ...prev,
617
732
  messageHistory: [...newHistory, toolMessage],
618
733
  currentMessage: null,
619
- isAiWorking: true // Keep AI working state active
734
+ isAiWorking: true
620
735
  };
621
736
  });
622
737
  });
@@ -696,23 +811,11 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
696
811
  setState(prev => {
697
812
  // Update shell state if it's an execute_command
698
813
  if (update.toolName === 'execute_command' && prev.shellState) {
699
- const newOutput = prev.shellState.output + update.chunk;
700
- // Clear user input buffer when new output arrives
701
- // This handles cases where:
702
- // 1. Program echoes the input (input appears in output)
703
- // 2. Program outputs a newline (moving to next line)
704
- // 3. Program outputs a new prompt
705
- const shouldClearInput = prev.shellState.userInput && (
706
- // If output contains a newline, clear input (program moved to next line)
707
- update.chunk.includes('\n') ||
708
- // If output contains the user input (program echoed it), clear input
709
- (prev.shellState.userInput && newOutput.includes(prev.shellState.userInput)));
710
814
  return {
711
815
  ...prev,
712
816
  shellState: {
713
817
  ...prev.shellState,
714
- output: newOutput,
715
- userInput: shouldClearInput ? '' : prev.shellState.userInput
818
+ output: prev.shellState.output + update.chunk
716
819
  }
717
820
  };
718
821
  }
@@ -790,7 +893,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
790
893
  ...prev,
791
894
  screen: 'plan-approval',
792
895
  planApprovalRequest: {
793
- ...plan,
896
+ plan,
794
897
  resolve
795
898
  },
796
899
  isLoading: false
@@ -798,6 +901,46 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
798
901
  });
799
902
  });
800
903
  }, [clearScreen]);
904
+ // Set up callback for plan created notification (optional - for future use)
905
+ React.useEffect(() => {
906
+ onPlanCreated((plan) => {
907
+ // The plan will be displayed via the plan-approval screen
908
+ // This callback can be used for additional notifications if needed
909
+ try {
910
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] Plan created: ${plan.title} with ${plan.steps.length} tasks\n`);
911
+ }
912
+ catch (e) { }
913
+ });
914
+ }, [onPlanCreated]);
915
+ // Set up callback for task completion
916
+ React.useEffect(() => {
917
+ onTaskCompleted((task, taskNumber, totalTasks, completionNote) => {
918
+ // Add task completion message to history
919
+ setState(prev => {
920
+ const taskCompletedMessage = {
921
+ id: `task-complete-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
922
+ role: 'system',
923
+ content: '', // We'll render this specially
924
+ timestamp: new Date(),
925
+ taskCompletion: {
926
+ taskNumber,
927
+ totalTasks,
928
+ taskDescription: task.description,
929
+ completionNote
930
+ }
931
+ };
932
+ // Move current message to history if exists
933
+ const newHistory = prev.currentMessage
934
+ ? [...prev.messageHistory, prev.currentMessage, taskCompletedMessage]
935
+ : [...prev.messageHistory, taskCompletedMessage];
936
+ return {
937
+ ...prev,
938
+ messageHistory: newHistory,
939
+ currentMessage: null
940
+ };
941
+ });
942
+ });
943
+ }, [onTaskCompleted]);
801
944
  // Set up callback for password requests
802
945
  React.useEffect(() => {
803
946
  onPasswordRequest(async (message) => {
@@ -832,6 +975,64 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
832
975
  });
833
976
  });
834
977
  }, [clearScreen]);
978
+ // Set up callback for interactive editor mode (vim, nano, etc.)
979
+ // Ref to hold the current editor process for cleanup
980
+ const editorProcessRef = React.useRef(null);
981
+ React.useEffect(() => {
982
+ onInteractiveEditorMode((active, command, cwd, remoteContext) => {
983
+ if (active && command && cwd) {
984
+ // Enter interactive editor mode
985
+ setState(prev => ({ ...prev, isInteractiveEditorMode: true }));
986
+ // Common exit handler for all editor types
987
+ const handleEditorExit = (exitCode) => {
988
+ editorProcessRef.current = null;
989
+ // Clear screen before restoring Ink UI
990
+ clearScreen();
991
+ // Editor exited, restore Ink UI
992
+ setState(prev => ({ ...prev, isInteractiveEditorMode: false }));
993
+ };
994
+ // Wait a frame for React to process the state change
995
+ setTimeout(() => {
996
+ // Choose appropriate editor runner based on context
997
+ if (!remoteContext || remoteContext.type === 'local') {
998
+ // Local context: use standard interactive editor
999
+ editorProcessRef.current = runInteractiveEditor(command, cwd, handleEditorExit);
1000
+ }
1001
+ else if (remoteContext.type === 'wsl') {
1002
+ // WSL context: use WSL editor with distribution name
1003
+ const distribution = remoteContext.metadata?.distroName || 'Ubuntu';
1004
+ editorProcessRef.current = runWSLEditor(distribution, command, cwd, handleEditorExit);
1005
+ }
1006
+ else if (remoteContext.type === 'docker') {
1007
+ // Docker context: use Docker editor with container ID
1008
+ const containerId = remoteContext.metadata?.containerId || '';
1009
+ editorProcessRef.current = runDockerEditor(containerId, command, cwd, handleEditorExit);
1010
+ }
1011
+ else if (remoteContext.type === 'ssh') {
1012
+ // SSH context: use SSH editor with client
1013
+ const sshClient = remoteContext.handler?.client;
1014
+ if (sshClient) {
1015
+ editorProcessRef.current = runSSHEditor(sshClient, command, cwd, handleEditorExit);
1016
+ }
1017
+ else {
1018
+ // Fallback if no client available
1019
+ console.error('SSH client not available for editor mode');
1020
+ setState(prev => ({ ...prev, isInteractiveEditorMode: false }));
1021
+ }
1022
+ }
1023
+ }, 50);
1024
+ }
1025
+ else {
1026
+ // Exit interactive editor mode (cleanup)
1027
+ if (editorProcessRef.current) {
1028
+ editorProcessRef.current.kill();
1029
+ editorProcessRef.current = null;
1030
+ }
1031
+ clearScreen();
1032
+ setState(prev => ({ ...prev, isInteractiveEditorMode: false }));
1033
+ }
1034
+ });
1035
+ }, [onInteractiveEditorMode, clearScreen]);
835
1036
  // Track Ctrl+C presses for double-press exit
836
1037
  const ctrlCPressedRef = React.useRef(false);
837
1038
  const ctrlCTimeoutRef = React.useRef(null);
@@ -954,7 +1155,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
954
1155
  }, 5000);
955
1156
  return;
956
1157
  }
957
- });
1158
+ }, { isActive: !state.isInteractiveEditorMode });
958
1159
  const handleSubmit = useCallback(async (value) => {
959
1160
  // Trim the value to remove any leading/trailing whitespace or newlines
960
1161
  const trimmedValue = value.trim();
@@ -1084,67 +1285,17 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1084
1285
  autoAcceptMode: !prev.autoAcceptMode
1085
1286
  }));
1086
1287
  }, []);
1288
+ // If in interactive editor mode, render minimal UI to keep Ink mounted
1289
+ // but don't render any visible content - the editor has the screen
1290
+ if (state.isInteractiveEditorMode) {
1291
+ return React.createElement(Box, null);
1292
+ }
1087
1293
  return (React.createElement(Box, { flexDirection: "column" },
1088
1294
  state.screen === 'chat' && (React.createElement(React.Fragment, null,
1089
1295
  React.createElement(MessageList, { history: state.messageHistory, current: state.currentMessage, showBanner: true }),
1090
- 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) => {
1091
- // ============================================
1092
- // LINE-BUFFERED INPUT HANDLING
1093
- // ============================================
1094
- // Regular characters are buffered locally.
1095
- // Complete line is sent when Enter is pressed.
1096
- // Special keys (arrows, Ctrl+C, etc.) are sent immediately.
1097
- setState(prev => {
1098
- if (!prev.shellState)
1099
- return prev;
1100
- // ============================================
1101
- // ENTER - Send complete line to process
1102
- // ============================================
1103
- if (input.includes('\r\n') || input.includes('\n')) {
1104
- // Send to process
1105
- onShellInput(input);
1106
- // Keep input visible until program responds
1107
- return prev;
1108
- }
1109
- // ============================================
1110
- // BACKSPACE - Remove from buffer
1111
- // ============================================
1112
- if (input === '\x08') {
1113
- const currentInput = prev.shellState.userInput || '';
1114
- return {
1115
- ...prev,
1116
- shellState: {
1117
- ...prev.shellState,
1118
- userInput: currentInput.slice(0, -1)
1119
- }
1120
- };
1121
- }
1122
- // ============================================
1123
- // CONTROL KEYS - Send immediately, don't buffer
1124
- // ============================================
1125
- if (input.length === 1 && input.charCodeAt(0) < 32 && input !== '\t') {
1126
- onShellInput(input);
1127
- return prev;
1128
- }
1129
- // ============================================
1130
- // ANSI SEQUENCES - Send immediately, don't buffer
1131
- // ============================================
1132
- if (input.startsWith('\x1b')) {
1133
- onShellInput(input);
1134
- return prev;
1135
- }
1136
- // ============================================
1137
- // REGULAR CHARACTERS - Add to buffer only
1138
- // ============================================
1139
- // Do NOT send to process yet - wait for Enter
1140
- return {
1141
- ...prev,
1142
- shellState: {
1143
- ...prev.shellState,
1144
- userInput: (prev.shellState.userInput || '') + input
1145
- }
1146
- };
1147
- });
1296
+ 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) => {
1297
+ // PTY MODE: Send everything immediately
1298
+ onShellInput(input);
1148
1299
  }, onFocusChange: (focused) => {
1149
1300
  setState(prev => ({
1150
1301
  ...prev,
@@ -1204,41 +1355,42 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1204
1355
  });
1205
1356
  }
1206
1357
  } })),
1207
- state.screen === 'plan-approval' && state.planApprovalRequest && (React.createElement(Box, { flexDirection: "column" },
1208
- React.createElement(Box, { marginBottom: 1 },
1209
- React.createElement(Text, { bold: true, color: "#00ccff" }, "\uD83D\uDCCB Implementation Plan")),
1210
- React.createElement(Box, { flexDirection: "column", marginBottom: 1, paddingX: 2 },
1211
- React.createElement(Text, { bold: true, color: "#ffaa00" }, "Tasks:"),
1212
- state.planApprovalRequest.tasks.map((task, index) => (React.createElement(Box, { key: index, marginTop: 1 },
1213
- React.createElement(Text, { color: "#666666" },
1214
- index + 1,
1215
- ". "),
1216
- React.createElement(Text, null, task))))),
1217
- React.createElement(Box, { marginTop: 1 },
1218
- React.createElement(Text, { color: "#00ccff" }, state.planApprovalRequest.question)),
1219
- React.createElement(ConfirmPrompt, { message: "Implement this plan?", onYes: () => {
1220
- const resolve = state.planApprovalRequest?.resolve;
1221
- if (resolve) {
1222
- resolve(true);
1223
- }
1224
- setState(prev => ({
1225
- ...prev,
1226
- screen: 'chat',
1227
- planApprovalRequest: undefined,
1228
- isLoading: true,
1229
- isAiWorking: true
1230
- }));
1231
- }, onNo: () => {
1232
- const resolve = state.planApprovalRequest?.resolve;
1233
- if (resolve) {
1234
- resolve(false);
1358
+ state.screen === 'plan-approval' && state.planApprovalRequest && (React.createElement(PlanReviewScreen, { plan: state.planApprovalRequest.plan, onApprove: () => {
1359
+ const resolve = state.planApprovalRequest?.resolve;
1360
+ const plan = state.planApprovalRequest?.plan;
1361
+ if (resolve) {
1362
+ resolve(true);
1363
+ }
1364
+ // Add "Plan Accepted" message to history
1365
+ const planAcceptedMessage = {
1366
+ id: `plan-accepted-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1367
+ role: 'system',
1368
+ content: '',
1369
+ timestamp: new Date(),
1370
+ planAccepted: {
1371
+ planTitle: plan?.title,
1372
+ totalTasks: plan?.steps?.length
1235
1373
  }
1236
- setState(prev => ({
1237
- ...prev,
1238
- screen: 'chat',
1239
- planApprovalRequest: undefined
1240
- }));
1241
- } }))),
1374
+ };
1375
+ setState(prev => ({
1376
+ ...prev,
1377
+ screen: 'chat',
1378
+ planApprovalRequest: undefined,
1379
+ messageHistory: [...prev.messageHistory, planAcceptedMessage],
1380
+ isLoading: true,
1381
+ isAiWorking: true
1382
+ }));
1383
+ }, onEdit: () => {
1384
+ const resolve = state.planApprovalRequest?.resolve;
1385
+ if (resolve) {
1386
+ resolve(false);
1387
+ }
1388
+ setState(prev => ({
1389
+ ...prev,
1390
+ screen: 'chat',
1391
+ planApprovalRequest: undefined
1392
+ }));
1393
+ } })),
1242
1394
  state.screen === 'password-prompt' && state.passwordRequest && (React.createElement(PasswordPrompt, { message: state.passwordRequest.message, onSubmit: (password) => {
1243
1395
  const resolve = state.passwordRequest?.resolve;
1244
1396
  if (resolve) {