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.
- package/dist/cli-adapter.d.ts +10 -6
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +613 -154
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +1 -0
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/context/context-manager.d.ts +4 -1
- package/dist/context/context-manager.d.ts.map +1 -1
- package/dist/context/context-manager.js +30 -7
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/handlers/wsl-handler.d.ts +10 -0
- package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
- package/dist/context/handlers/wsl-handler.js +31 -2
- package/dist/context/handlers/wsl-handler.js.map +1 -1
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -1
- package/dist/services/ai-service-client.d.ts +1 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +20 -0
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +136 -21
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.d.ts +1 -0
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +144 -3
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/inspect-symbol.js +27 -27
- package/dist/tools/inspect-symbol.js.map +1 -1
- package/dist/tools/plan-mode.d.ts +55 -19
- package/dist/tools/plan-mode.d.ts.map +1 -1
- package/dist/tools/plan-mode.js +204 -123
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/types.d.ts +1 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/components/App.d.ts +6 -5
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +277 -125
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +24 -5
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts +2 -1
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +41 -106
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/ui/components/MarkdownRenderer.js +12 -8
- package/dist/ui/components/MarkdownRenderer.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +11 -3
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.d.ts +12 -0
- package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -0
- package/dist/ui/components/PlanAcceptedMessage.js +22 -0
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -0
- package/dist/ui/components/PlanReviewScreen.d.ts +14 -0
- package/dist/ui/components/PlanReviewScreen.d.ts.map +1 -0
- package/dist/ui/components/PlanReviewScreen.js +52 -0
- package/dist/ui/components/PlanReviewScreen.js.map +1 -0
- package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +5 -5
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.d.ts +14 -0
- package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -0
- package/dist/ui/components/TaskCompletedMessage.js +25 -0
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -0
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +174 -17
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/utils/conversation-logger.d.ts +127 -0
- package/dist/utils/conversation-logger.d.ts.map +1 -0
- package/dist/utils/conversation-logger.js +283 -0
- package/dist/utils/conversation-logger.js.map +1 -0
- package/dist/utils/editor-utils.d.ts +87 -0
- package/dist/utils/editor-utils.d.ts.map +1 -0
- package/dist/utils/editor-utils.js +712 -0
- package/dist/utils/editor-utils.js.map +1 -0
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +12 -4
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/markdown-parser.d.ts.map +1 -1
- package/dist/utils/markdown-parser.js +4 -2
- package/dist/utils/markdown-parser.js.map +1 -1
- package/dist/utils/shell.d.ts +32 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +97 -161
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/syntax-checker.d.ts +24 -0
- package/dist/utils/syntax-checker.d.ts.map +1 -0
- package/dist/utils/syntax-checker.js +320 -0
- package/dist/utils/syntax-checker.js.map +1 -0
- package/package.json +4 -3
- 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
|
-
//
|
|
211
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
597
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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,
|
|
1091
|
-
//
|
|
1092
|
-
|
|
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(
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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) {
|