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.
- package/dist/cli-adapter.d.ts +8 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +104 -37
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/mcp-config-manager.d.ts +53 -0
- package/dist/config/mcp-config-manager.d.ts.map +1 -0
- package/dist/config/mcp-config-manager.js +210 -0
- package/dist/config/mcp-config-manager.js.map +1 -0
- package/dist/config/slash-commands.d.ts +14 -0
- package/dist/config/slash-commands.d.ts.map +1 -0
- package/dist/config/slash-commands.js +65 -0
- package/dist/config/slash-commands.js.map +1 -0
- package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
- package/dist/context/handlers/wsl-handler.js +2 -1
- package/dist/context/handlers/wsl-handler.js.map +1 -1
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/mcp-command-handler.d.ts +16 -0
- package/dist/mcp/mcp-command-handler.d.ts.map +1 -0
- package/dist/mcp/mcp-command-handler.js +196 -0
- package/dist/mcp/mcp-command-handler.js.map +1 -0
- package/dist/mcp/mcp-server-manager.d.ts +30 -0
- package/dist/mcp/mcp-server-manager.d.ts.map +1 -0
- package/dist/mcp/mcp-server-manager.js +165 -0
- package/dist/mcp/mcp-server-manager.js.map +1 -0
- package/dist/mcp/mcp-tool-wrapper.d.ts +12 -0
- package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -0
- package/dist/mcp/mcp-tool-wrapper.js +57 -0
- package/dist/mcp/mcp-tool-wrapper.js.map +1 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +20 -2
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +18 -10
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/ui/components/App.d.ts +4 -0
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +235 -138
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/FileCreationPreview.d.ts +8 -0
- package/dist/ui/components/FileCreationPreview.d.ts.map +1 -0
- package/dist/ui/components/FileCreationPreview.js +63 -0
- package/dist/ui/components/FileCreationPreview.js.map +1 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +132 -4
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts +3 -1
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +46 -89
- 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 +16 -34
- 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 +10 -2
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/SlashCommandAutocomplete.d.ts +11 -0
- package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -0
- package/dist/ui/components/SlashCommandAutocomplete.js +15 -0
- package/dist/ui/components/SlashCommandAutocomplete.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/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +96 -32
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.js +28 -1
- package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
- package/dist/utils/editor-utils.d.ts +50 -0
- package/dist/utils/editor-utils.d.ts.map +1 -0
- package/dist/utils/editor-utils.js +501 -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 +4 -2
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/shell.d.ts +34 -1
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +130 -123
- 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 +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 {
|
|
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(
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
559
|
+
isAiWorking: prev.isAiWorking // Preserve existing state
|
|
473
560
|
};
|
|
474
|
-
}
|
|
475
|
-
|
|
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:
|
|
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,
|
|
992
|
-
//
|
|
993
|
-
|
|
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
|
});
|