centaurus-cli 2.9.5 → 2.9.7
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 +27 -2
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +410 -17
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +7 -0
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/context/context-manager.d.ts +4 -0
- package/dist/context/context-manager.d.ts.map +1 -1
- package/dist/context/context-manager.js +6 -0
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/handlers/docker-handler.d.ts +5 -1
- package/dist/context/handlers/docker-handler.d.ts.map +1 -1
- package/dist/context/handlers/docker-handler.js +27 -10
- package/dist/context/handlers/docker-handler.js.map +1 -1
- package/dist/context/handlers/ssh-handler.d.ts +47 -1
- package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
- package/dist/context/handlers/ssh-handler.js +546 -73
- package/dist/context/handlers/ssh-handler.js.map +1 -1
- package/dist/context/handlers/wsl-handler.d.ts +5 -1
- package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
- package/dist/context/handlers/wsl-handler.js +24 -6
- package/dist/context/handlers/wsl-handler.js.map +1 -1
- package/dist/context/subshell-handler.d.ts +8 -2
- package/dist/context/subshell-handler.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/services/checkpoint-manager.d.ts +162 -0
- package/dist/services/checkpoint-manager.d.ts.map +1 -0
- package/dist/services/checkpoint-manager.js +926 -0
- package/dist/services/checkpoint-manager.js.map +1 -0
- package/dist/services/fast-context-agent.d.ts +12 -0
- package/dist/services/fast-context-agent.d.ts.map +1 -0
- package/dist/services/fast-context-agent.js +253 -0
- package/dist/services/fast-context-agent.js.map +1 -0
- package/dist/tools/background-command.d.ts.map +1 -1
- package/dist/tools/background-command.js +132 -24
- package/dist/tools/background-command.js.map +1 -1
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +14 -4
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/create-image.d.ts.map +1 -1
- package/dist/tools/create-image.js +43 -18
- package/dist/tools/create-image.js.map +1 -1
- package/dist/tools/fast-context.d.ts +3 -0
- package/dist/tools/fast-context.d.ts.map +1 -0
- package/dist/tools/fast-context.js +72 -0
- package/dist/tools/fast-context.js.map +1 -0
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +12 -12
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/find-files.d.ts +2 -1
- package/dist/tools/find-files.d.ts.map +1 -1
- package/dist/tools/find-files.js +62 -2
- package/dist/tools/find-files.js.map +1 -1
- package/dist/tools/get-diff.d.ts +9 -45
- package/dist/tools/get-diff.d.ts.map +1 -1
- package/dist/tools/get-diff.js +288 -171
- package/dist/tools/get-diff.js.map +1 -1
- package/dist/tools/types.d.ts +4 -1
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/components/App.d.ts +8 -0
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +290 -85
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/ConfirmPrompt.d.ts +2 -0
- package/dist/ui/components/ConfirmPrompt.d.ts.map +1 -1
- package/dist/ui/components/ConfirmPrompt.js +8 -3
- package/dist/ui/components/ConfirmPrompt.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +6 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +130 -6
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +50 -15
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +2 -2
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +213 -18
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/utils/ansi-encoder.d.ts +5 -0
- package/dist/utils/ansi-encoder.d.ts.map +1 -1
- package/dist/utils/ansi-encoder.js +5 -5
- package/dist/utils/ansi-encoder.js.map +1 -1
- package/dist/utils/editor-utils.d.ts +5 -0
- package/dist/utils/editor-utils.d.ts.map +1 -1
- package/dist/utils/editor-utils.js +67 -0
- package/dist/utils/editor-utils.js.map +1 -1
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +2 -1
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/terminal-output.d.ts.map +1 -1
- package/dist/utils/terminal-output.js +162 -103
- package/dist/utils/terminal-output.js.map +1 -1
- package/package.json +3 -1
|
@@ -4,6 +4,7 @@ import SelectInput from 'ink-select-input';
|
|
|
4
4
|
import { CircularSelectInput } from './CircularSelectInput.js';
|
|
5
5
|
import stripAnsi from 'strip-ansi';
|
|
6
6
|
import TextInput from 'ink-text-input';
|
|
7
|
+
import * as path from 'path';
|
|
7
8
|
import { quickLog } from '../../utils/conversation-logger.js';
|
|
8
9
|
import { WelcomeBanner } from './WelcomeBanner.js';
|
|
9
10
|
import { InputBox } from './InputBox.js';
|
|
@@ -41,6 +42,26 @@ import { conversationManager } from '../../services/conversation-manager.js';
|
|
|
41
42
|
import { logDebug, logError } from '../../utils/logger.js';
|
|
42
43
|
import { getTerminalDimensions } from '../../hooks/useTerminalDimensions.js';
|
|
43
44
|
import { ConfigManager } from '../../config/manager.js';
|
|
45
|
+
/**
|
|
46
|
+
* Check if a file path is outside the current working directory.
|
|
47
|
+
* Returns true if the path is NOT within the CWD or its subdirectories.
|
|
48
|
+
*/
|
|
49
|
+
function isOutsideCwd(filePath, cwd) {
|
|
50
|
+
if (!filePath || !cwd)
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
// Normalize paths for cross-platform comparison
|
|
54
|
+
// IMPORTANT: Resolve filePath against cwd to handle relative paths correctly
|
|
55
|
+
// This ensures remote paths (SSH/Docker) are resolved against their session CWD, not local process.cwd()
|
|
56
|
+
const normalizedFilePath = path.resolve(cwd, filePath).toLowerCase();
|
|
57
|
+
const normalizedCwd = path.resolve(cwd).toLowerCase();
|
|
58
|
+
// Check if the file path starts with the CWD (meaning it's inside or a subdirectory)
|
|
59
|
+
return !normalizedFilePath.startsWith(normalizedCwd);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
44
65
|
// Banner item with stable timestamp - created once outside component
|
|
45
66
|
const BANNER_ITEM = { id: '__banner__', role: '__banner__', content: '', timestamp: new Date(0) };
|
|
46
67
|
const MessageList = React.memo(({ history, current, showBanner }) => {
|
|
@@ -117,13 +138,18 @@ const ApprovalSection = React.memo(({ approvalRequest, onApprove }) => {
|
|
|
117
138
|
// Memoize callbacks to prevent re-renders
|
|
118
139
|
const handleYes = React.useCallback(() => onApprove(true), [onApprove]);
|
|
119
140
|
const handleNo = React.useCallback(() => onApprove(false), [onApprove]);
|
|
141
|
+
// Generate warning message if operation is outside CWD
|
|
142
|
+
const warningMessage = approvalRequest.isOutsideCwd
|
|
143
|
+
? 'This operation targets a location OUTSIDE your current working directory!'
|
|
144
|
+
: undefined;
|
|
120
145
|
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
121
146
|
approvalRequest.preview && approvalRequest.preview.type === 'code' && (React.createElement(FileCreationPreview, { content: approvalRequest.preview.content, filePath: approvalRequest.operationDetails?.file_path || 'New File' })),
|
|
122
147
|
approvalRequest.preview && approvalRequest.preview.type === 'diff' && (React.createElement(DiffViewer, { diff: approvalRequest.preview.content, filePath: approvalRequest.operationDetails?.file_path || 'Modified File', fullDiff: approvalRequest.preview.fullDiff })),
|
|
123
|
-
React.createElement(ConfirmPrompt, { message: approvalRequest.message, onYes: handleYes, onNo: handleNo })));
|
|
148
|
+
React.createElement(ConfirmPrompt, { message: approvalRequest.message, onYes: handleYes, onNo: handleNo, warningMessage: warningMessage })));
|
|
124
149
|
}, (prevProps, nextProps) => {
|
|
125
|
-
// Only re-render if the approval request message changes
|
|
126
|
-
return prevProps.approvalRequest.message === nextProps.approvalRequest.message
|
|
150
|
+
// Only re-render if the approval request message or isOutsideCwd changes
|
|
151
|
+
return prevProps.approvalRequest.message === nextProps.approvalRequest.message &&
|
|
152
|
+
prevProps.approvalRequest.isOutsideCwd === nextProps.approvalRequest.isOutsideCwd;
|
|
127
153
|
});
|
|
128
154
|
// Simple rename input screen component
|
|
129
155
|
const RenameInputScreen = ({ currentTitle, onRename, onCancel }) => {
|
|
@@ -149,11 +175,12 @@ const RenameInputScreen = ({ currentTitle, onRename, onCancel }) => {
|
|
|
149
175
|
React.createElement(Box, { marginTop: 1 },
|
|
150
176
|
React.createElement(Text, { dimColor: true }, "Press Enter to save, ESC to cancel"))));
|
|
151
177
|
};
|
|
152
|
-
export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onClearStreamedResponse, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onPlanCreated, onTaskCompleted, onCommandModeChange, onToggleCommandMode, onBackgroundModeChange, onToggleBackgroundMode, onBackgroundTaskCountChange, onSubAgentCountChange, onSetAutoModeSetup, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode, onConnectionStatusUpdate, onTokenCountUpdate, onContextLimitReached, onChatPickerSetup, onChatPickerSelection, onChatDeletePickerSetup, onChatDeletePickerSelection, onChatListSetup, onChatRenamePickerSetup, onChatRename, onRestoreMessagesSetup, onUIMessageHistoryUpdate, onBackgroundTaskListSetup, onBackgroundTaskSelection, onBackgroundTaskCancelSetup, onBackgroundTaskCancel, onBackgroundTaskViewSetup, onSessionQuotaUpdate, onMCPAddScreenSetup, onMCPRemoveScreenSetup, onMCPEnableScreenSetup, onMCPDisableScreenSetup, onMCPListScreenSetup, onMCPAddServer, onMCPRemoveServer, onMCPEnableServer, onMCPDisableServer, onMCPValidateConfig, onPromptAnswered, getMainConversation, onWarpifySession, onWorkflowCreatorSetup, onWorkflowSave }) => {
|
|
178
|
+
export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onClearStreamedResponse, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onPlanCreated, onTaskCompleted, onCommandModeChange, onToggleCommandMode, onBackgroundModeChange, onToggleBackgroundMode, onBackgroundTaskCountChange, onSubAgentCountChange, onSetAutoModeSetup, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode, onConnectionStatusUpdate, onTokenCountUpdate, onContextLimitReached, onChatPickerSetup, onChatPickerSelection, onChatDeletePickerSetup, onChatDeletePickerSelection, onChatListSetup, onChatRenamePickerSetup, onChatRename, onRestoreMessagesSetup, onUIMessageHistoryUpdate, onBackgroundTaskListSetup, onBackgroundTaskSelection, onBackgroundTaskCancelSetup, onBackgroundTaskCancel, onBackgroundTaskViewSetup, onSessionQuotaUpdate, onMCPAddScreenSetup, onMCPRemoveScreenSetup, onMCPEnableScreenSetup, onMCPDisableScreenSetup, onMCPListScreenSetup, onMCPAddServer, onMCPRemoveServer, onMCPEnableServer, onMCPDisableServer, onMCPValidateConfig, onPromptAnswered, getMainConversation, onWarpifySession, onAiAutoSuggestChange, onWorkflowCreatorSetup, onWorkflowSave, getCheckpoints, onRevertToCheckpointSetup, onSetInputSetup }) => {
|
|
153
179
|
const { exit } = useApp();
|
|
154
180
|
// Calculate limit for paginated lists (75% of terminal height)
|
|
155
181
|
const listLimit = Math.max(5, Math.floor((process.stdout.rows || 24) * 0.75));
|
|
156
182
|
const autoAcceptRef = React.useRef(false);
|
|
183
|
+
const cwdRef = React.useRef(process.cwd()); // Track CWD for async callback access
|
|
157
184
|
const setAutoModeCallbackRef = React.useRef(null);
|
|
158
185
|
// Connectivity State
|
|
159
186
|
const isConnected = useConnectivity();
|
|
@@ -367,6 +394,10 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
367
394
|
React.useEffect(() => {
|
|
368
395
|
autoAcceptRef.current = state.autoAcceptMode;
|
|
369
396
|
}, [state.autoAcceptMode]);
|
|
397
|
+
// Keep CWD ref in sync with state
|
|
398
|
+
React.useEffect(() => {
|
|
399
|
+
cwdRef.current = state.currentWorkingDirectory;
|
|
400
|
+
}, [state.currentWorkingDirectory]);
|
|
370
401
|
// Push shell output updates to InputDetectionAgent when agent control is active
|
|
371
402
|
// This ensures the detection agent always has the latest output for polling
|
|
372
403
|
React.useEffect(() => {
|
|
@@ -378,6 +409,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
378
409
|
// This is set to true when an Agent-Controlled shell exits, essentially killing the "AI Agent"
|
|
379
410
|
// associated with it from the UI perspective. It is reset when the user manually inputs a new message.
|
|
380
411
|
const ignoreIncomingAIUpdatesRef = React.useRef(false);
|
|
412
|
+
// True only during Alt+E warpify transition, used to suppress expected shell termination noise.
|
|
413
|
+
const warpifyInProgressRef = React.useRef(false);
|
|
414
|
+
const warpifyTerminatingCommandRef = React.useRef(null);
|
|
381
415
|
// NOTE: Token count is now updated via onTokenCountUpdate callback from cli-adapter
|
|
382
416
|
// which tracks the actual AI conversation history including system prompt.
|
|
383
417
|
// The old UI-based calculation has been removed to avoid overwriting correct values.
|
|
@@ -898,6 +932,14 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
898
932
|
}));
|
|
899
933
|
});
|
|
900
934
|
}, []); // Empty dependency array - only register once
|
|
935
|
+
React.useEffect(() => {
|
|
936
|
+
onAiAutoSuggestChange((enabled) => {
|
|
937
|
+
setState(prev => ({
|
|
938
|
+
...prev,
|
|
939
|
+
aiAutoSuggest: enabled
|
|
940
|
+
}));
|
|
941
|
+
});
|
|
942
|
+
}, [onAiAutoSuggestChange]);
|
|
901
943
|
// Set up callback for setting Auto mode (used after background task starts)
|
|
902
944
|
React.useEffect(() => {
|
|
903
945
|
onSetAutoModeSetup((enabled) => {
|
|
@@ -910,14 +952,82 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
910
952
|
// Set up callback for message restoration (when resuming a chat)
|
|
911
953
|
React.useEffect(() => {
|
|
912
954
|
onRestoreMessagesSetup((restoredMessages) => {
|
|
913
|
-
//
|
|
955
|
+
// Use two-phase restore to work around Ink's Static component caching:
|
|
956
|
+
// 1. First, clear messageHistory to unmount the Static component
|
|
957
|
+
// 2. Then after a short delay, set the restored messages
|
|
958
|
+
// This ensures Static re-renders fresh with the new message list
|
|
959
|
+
quickLog(`[${new Date().toISOString()}] [App] onRestoreMessagesSetup callback invoked with ${restoredMessages.length} messages\n`);
|
|
960
|
+
quickLog(`[${new Date().toISOString()}] [App] First message: ${restoredMessages[0]?.role} - "${restoredMessages[0]?.content?.substring(0, 50)}..."\n`);
|
|
961
|
+
quickLog(`[${new Date().toISOString()}] [App] Last message: ${restoredMessages[restoredMessages.length - 1]?.role} - "${restoredMessages[restoredMessages.length - 1]?.content?.substring(0, 50)}..."\n`);
|
|
962
|
+
// Phase 1: Clear screen and empty message history to unmount Static
|
|
914
963
|
clearScreen();
|
|
915
964
|
setState(prev => ({
|
|
916
965
|
...prev,
|
|
917
|
-
messageHistory:
|
|
918
|
-
|
|
966
|
+
messageHistory: [],
|
|
967
|
+
currentMessage: null,
|
|
968
|
+
screen: prev.screen === 'password-prompt' ? 'password-prompt' : 'chat',
|
|
919
969
|
isLoading: false
|
|
920
970
|
}));
|
|
971
|
+
quickLog(`[${new Date().toISOString()}] [App] Phase 1: Cleared messageHistory to unmount Static\n`);
|
|
972
|
+
// Phase 2: After brief delay, restore the messages
|
|
973
|
+
setTimeout(() => {
|
|
974
|
+
clearScreen();
|
|
975
|
+
setState(prev => ({
|
|
976
|
+
...prev,
|
|
977
|
+
messageHistory: restoredMessages,
|
|
978
|
+
screen: prev.screen === 'password-prompt' ? 'password-prompt' : 'chat',
|
|
979
|
+
isLoading: false
|
|
980
|
+
}));
|
|
981
|
+
quickLog(`[${new Date().toISOString()}] [App] Phase 2: Restored ${restoredMessages.length} messages to messageHistory\n`);
|
|
982
|
+
}, 50); // Short delay to allow React to unmount and remount Static
|
|
983
|
+
});
|
|
984
|
+
}, []); // Empty dependency array - only register once
|
|
985
|
+
// Set up callback for revert to checkpoint (truncates UI message history)
|
|
986
|
+
React.useEffect(() => {
|
|
987
|
+
onRevertToCheckpointSetup((checkpointIndex, prompt) => {
|
|
988
|
+
setState(prev => {
|
|
989
|
+
let truncateAtIndex = -1;
|
|
990
|
+
// Message history length check for safety
|
|
991
|
+
if (typeof checkpointIndex === 'number' && checkpointIndex >= 0 && checkpointIndex < prev.messageHistory.length) {
|
|
992
|
+
// Use the explicit index provided by the checkpoint
|
|
993
|
+
truncateAtIndex = checkpointIndex;
|
|
994
|
+
quickLog(`[${new Date().toISOString()}] [App] Revert: using explicit checkpoint index ${checkpointIndex}\n`);
|
|
995
|
+
// Verify it matches the prompt (sanity check)
|
|
996
|
+
const targetMsg = prev.messageHistory[checkpointIndex];
|
|
997
|
+
if (targetMsg.role === 'user' && (!targetMsg.content || !targetMsg.content.includes(prompt.slice(0, 20)))) {
|
|
998
|
+
quickLog(`[${new Date().toISOString()}] [App] Revert WARNING: Message at index ${checkpointIndex} does not match prompt "${prompt.slice(0, 20)}..."\n`);
|
|
999
|
+
// We still use the index as it's likely more reliable than string matching for duplicates
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
// Fallback: Find the user message via prompt matching
|
|
1004
|
+
// Search BACKWARDS to find the most recent occurrence (handling duplicates better)
|
|
1005
|
+
for (let i = prev.messageHistory.length - 1; i >= 0; i--) {
|
|
1006
|
+
const msg = prev.messageHistory[i];
|
|
1007
|
+
if (msg.role === 'user' && msg.content && msg.content.includes(prompt.slice(0, 50))) {
|
|
1008
|
+
truncateAtIndex = i;
|
|
1009
|
+
quickLog(`[${new Date().toISOString()}] [App] Revert: found message by prompt at index ${i}\n`);
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (truncateAtIndex >= 0) {
|
|
1015
|
+
// Truncate EXCLUSIVELY (remove the target message and everything after)
|
|
1016
|
+
// This allows the user to edit and resubmit the reverted message
|
|
1017
|
+
const truncatedHistory = prev.messageHistory.slice(0, truncateAtIndex);
|
|
1018
|
+
quickLog(`[${new Date().toISOString()}] [App] Reverted UI: truncated message history from ${prev.messageHistory.length} to ${truncatedHistory.length} messages (exclusive)\n`);
|
|
1019
|
+
return {
|
|
1020
|
+
...prev,
|
|
1021
|
+
messageHistory: truncatedHistory,
|
|
1022
|
+
currentMessage: null, // Clear any current message
|
|
1023
|
+
isLoading: false,
|
|
1024
|
+
isAiWorking: false
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
// If we couldn't find the message, log but don't crash
|
|
1028
|
+
quickLog(`[${new Date().toISOString()}] [App] Revert: could not find message matching prompt "${prompt.slice(0, 50)}"\n`);
|
|
1029
|
+
return prev;
|
|
1030
|
+
});
|
|
921
1031
|
});
|
|
922
1032
|
}, []); // Empty dependency array - only register once
|
|
923
1033
|
// Set up callback for background task list picker
|
|
@@ -1202,6 +1312,23 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1202
1312
|
}
|
|
1203
1313
|
// COMPLETION / ERROR
|
|
1204
1314
|
if (update.status === 'completed' || update.status === 'error') {
|
|
1315
|
+
const updateCommand = update.arguments?.command || update.arguments?.CommandLine || update.arguments?.commandLine || '';
|
|
1316
|
+
const terminatingCommand = warpifyTerminatingCommandRef.current || '';
|
|
1317
|
+
const isWarpifyTerminationUpdate = !terminatingCommand || !updateCommand || updateCommand === terminatingCommand;
|
|
1318
|
+
// During warpify, the PTY shell is intentionally terminated (SIGTERM).
|
|
1319
|
+
// Ignore the resulting execute_command completion/error update so we don't
|
|
1320
|
+
// show a false red shell card that can replace the tunneling status message.
|
|
1321
|
+
if (warpifyInProgressRef.current && isWarpifyTerminationUpdate) {
|
|
1322
|
+
if (pendingShellRef.current) {
|
|
1323
|
+
clearTimeout(pendingShellRef.current.timeoutId);
|
|
1324
|
+
pendingShellRef.current = null;
|
|
1325
|
+
}
|
|
1326
|
+
try {
|
|
1327
|
+
quickLog(`[${new Date().toISOString()}] [App] Suppressed execute_command ${update.status} during warpify transition\n`);
|
|
1328
|
+
}
|
|
1329
|
+
catch (e) { }
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1205
1332
|
// Check if we have a pending shell (fast command)
|
|
1206
1333
|
if (pendingShellRef.current) {
|
|
1207
1334
|
// Command finished VERY fast (within 50ms)
|
|
@@ -1505,8 +1632,31 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1505
1632
|
// Set up callback for tool approval requests
|
|
1506
1633
|
React.useEffect(() => {
|
|
1507
1634
|
onToolApprovalRequest(async (request) => {
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1635
|
+
// Determine if operation targets a path outside CWD
|
|
1636
|
+
let operationOutsideCwd = false;
|
|
1637
|
+
const currentCwd = cwdRef.current;
|
|
1638
|
+
if (currentCwd) {
|
|
1639
|
+
if (request.operationType === 'execute_command') {
|
|
1640
|
+
// For commands, check the Cwd parameter
|
|
1641
|
+
const commandCwd = request.operationDetails?.Cwd;
|
|
1642
|
+
if (commandCwd) {
|
|
1643
|
+
operationOutsideCwd = isOutsideCwd(commandCwd, currentCwd);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
else if (request.operationType === 'write_file' ||
|
|
1647
|
+
request.operationType === 'write_to_file' ||
|
|
1648
|
+
request.operationType === 'edit_file' ||
|
|
1649
|
+
request.operationType === 'multi_edit_file') {
|
|
1650
|
+
// For file operations, check the file_path
|
|
1651
|
+
const filePath = request.operationDetails?.file_path;
|
|
1652
|
+
if (filePath) {
|
|
1653
|
+
operationOutsideCwd = isOutsideCwd(filePath, currentCwd);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
// Auto-approve if auto-accept mode is on AND operation is NOT outside CWD
|
|
1658
|
+
// Operations outside CWD always require explicit user approval for safety
|
|
1659
|
+
if (autoAcceptRef.current && !operationOutsideCwd) {
|
|
1510
1660
|
return true;
|
|
1511
1661
|
}
|
|
1512
1662
|
// Check if this is a file operation with preview (write_file, write_to_file, edit_file, or multi_edit_file)
|
|
@@ -1527,6 +1677,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1527
1677
|
preview: request.preview,
|
|
1528
1678
|
operationType: request.operationType,
|
|
1529
1679
|
operationDetails: request.operationDetails,
|
|
1680
|
+
isOutsideCwd: operationOutsideCwd,
|
|
1530
1681
|
resolve: (approved) => {
|
|
1531
1682
|
// First unmount the approval section to stop rendering
|
|
1532
1683
|
setState(prev => ({
|
|
@@ -1560,6 +1711,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1560
1711
|
preview: request.preview,
|
|
1561
1712
|
operationType: request.operationType,
|
|
1562
1713
|
operationDetails: request.operationDetails,
|
|
1714
|
+
isOutsideCwd: operationOutsideCwd,
|
|
1563
1715
|
resolve: (approved) => {
|
|
1564
1716
|
// Clear approval request
|
|
1565
1717
|
setState(prev => ({
|
|
@@ -1645,19 +1797,43 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1645
1797
|
}
|
|
1646
1798
|
};
|
|
1647
1799
|
}
|
|
1648
|
-
// Update
|
|
1800
|
+
// Update tool in currentMessage if it matches
|
|
1801
|
+
let updatedCurrentMessage = prev.currentMessage;
|
|
1649
1802
|
if (prev.currentMessage?.role === 'tool' &&
|
|
1650
|
-
prev.currentMessage.toolExecution?.toolName === update.toolName
|
|
1803
|
+
prev.currentMessage.toolExecution?.toolName === update.toolName &&
|
|
1804
|
+
prev.currentMessage.toolExecution?.status === 'executing') {
|
|
1651
1805
|
const existingOutput = prev.currentMessage.toolExecution.streamingOutput || '';
|
|
1652
|
-
|
|
1653
|
-
...prev,
|
|
1654
|
-
|
|
1655
|
-
...prev.currentMessage,
|
|
1806
|
+
updatedCurrentMessage = {
|
|
1807
|
+
...prev.currentMessage,
|
|
1808
|
+
toolExecution: {
|
|
1809
|
+
...prev.currentMessage.toolExecution,
|
|
1810
|
+
streamingOutput: existingOutput + update.chunk
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
// Also update any matching executing tool in messageHistory
|
|
1815
|
+
let messageHistoryChanged = false;
|
|
1816
|
+
const updatedHistory = prev.messageHistory.map(msg => {
|
|
1817
|
+
if (msg.role === 'tool' &&
|
|
1818
|
+
msg.toolExecution?.toolName === update.toolName &&
|
|
1819
|
+
msg.toolExecution?.status === 'executing') {
|
|
1820
|
+
messageHistoryChanged = true;
|
|
1821
|
+
const existingOutput = msg.toolExecution.streamingOutput || '';
|
|
1822
|
+
return {
|
|
1823
|
+
...msg,
|
|
1656
1824
|
toolExecution: {
|
|
1657
|
-
...
|
|
1825
|
+
...msg.toolExecution,
|
|
1658
1826
|
streamingOutput: existingOutput + update.chunk
|
|
1659
1827
|
}
|
|
1660
|
-
}
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
return msg;
|
|
1831
|
+
});
|
|
1832
|
+
if (updatedCurrentMessage !== prev.currentMessage || messageHistoryChanged) {
|
|
1833
|
+
return {
|
|
1834
|
+
...prev,
|
|
1835
|
+
currentMessage: updatedCurrentMessage,
|
|
1836
|
+
messageHistory: updatedHistory
|
|
1661
1837
|
};
|
|
1662
1838
|
}
|
|
1663
1839
|
return prev;
|
|
@@ -1837,23 +2013,25 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1837
2013
|
return new Promise((resolve, reject) => {
|
|
1838
2014
|
setState(prev => ({
|
|
1839
2015
|
...prev,
|
|
1840
|
-
screen
|
|
2016
|
+
// Keep chat screen when possible so connection/tunneling status remains visible.
|
|
2017
|
+
// Use dedicated password screen only if we're currently in another modal screen.
|
|
2018
|
+
screen: prev.screen === 'chat' ? 'chat' : 'password-prompt',
|
|
1841
2019
|
passwordRequest: {
|
|
1842
2020
|
message,
|
|
1843
2021
|
resolve: (password) => {
|
|
1844
|
-
// Return to chat screen
|
|
2022
|
+
// Return to chat screen if we were in dedicated password screen mode
|
|
1845
2023
|
setState(prev2 => ({
|
|
1846
2024
|
...prev2,
|
|
1847
|
-
screen: 'chat',
|
|
2025
|
+
screen: prev2.screen === 'password-prompt' ? 'chat' : prev2.screen,
|
|
1848
2026
|
passwordRequest: undefined
|
|
1849
2027
|
}));
|
|
1850
2028
|
resolve(password);
|
|
1851
2029
|
},
|
|
1852
2030
|
reject: () => {
|
|
1853
|
-
// Return to chat screen
|
|
2031
|
+
// Return to chat screen if we were in dedicated password screen mode
|
|
1854
2032
|
setState(prev2 => ({
|
|
1855
2033
|
...prev2,
|
|
1856
|
-
screen: 'chat',
|
|
2034
|
+
screen: prev2.screen === 'password-prompt' ? 'chat' : prev2.screen,
|
|
1857
2035
|
passwordRequest: undefined
|
|
1858
2036
|
}));
|
|
1859
2037
|
reject(new Error('Password input cancelled'));
|
|
@@ -2097,24 +2275,29 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2097
2275
|
return; // Don't submit empty values
|
|
2098
2276
|
// Check if this is a slash command
|
|
2099
2277
|
const isSlashCommand = trimmedValue.startsWith('/');
|
|
2100
|
-
// Check if this is the
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2278
|
+
// Check if this is the clear command (slash or no slash)
|
|
2279
|
+
const lowerValue = trimmedValue.toLowerCase();
|
|
2280
|
+
if (lowerValue === '/clear' || lowerValue === 'clear' || lowerValue === '/cls' || lowerValue === 'cls' || lowerValue === '/reset') {
|
|
2281
|
+
// Soft Clear: Push history up by creating a blank space
|
|
2282
|
+
// We do NOT use clearScreen() here because that wipes the scrollback buffer (\x1b[3J)
|
|
2283
|
+
// Instead, we add a "spacer" message that pushes previous content up out of the viewport
|
|
2284
|
+
const height = process.stdout.rows || 24;
|
|
2285
|
+
// Use \u200B (Zero Width Space) to prevent trim() from emptying the string
|
|
2286
|
+
// in MessageDisplay.tsx, which would cause it to not render.
|
|
2287
|
+
const spacer = Array(height).fill('\u200B\n').join('');
|
|
2288
|
+
const clearMessage = {
|
|
2289
|
+
id: `clear-${Date.now()}`,
|
|
2290
|
+
role: 'system',
|
|
2291
|
+
content: spacer,
|
|
2292
|
+
timestamp: new Date(),
|
|
2293
|
+
isSpacer: true
|
|
2294
|
+
};
|
|
2104
2295
|
setState(prev => ({
|
|
2105
2296
|
...prev,
|
|
2106
|
-
messageHistory: [],
|
|
2297
|
+
messageHistory: [...prev.messageHistory, clearMessage],
|
|
2107
2298
|
currentMessage: null,
|
|
2108
|
-
|
|
2109
|
-
currentTokens: 0
|
|
2299
|
+
// We don't reset other state (tokens, loading) to preserve context
|
|
2110
2300
|
}));
|
|
2111
|
-
// Still call onMessage to clear backend history
|
|
2112
|
-
try {
|
|
2113
|
-
await onMessage(trimmedValue);
|
|
2114
|
-
}
|
|
2115
|
-
catch (error) {
|
|
2116
|
-
// Ignore errors for clear command
|
|
2117
|
-
}
|
|
2118
2301
|
return;
|
|
2119
2302
|
}
|
|
2120
2303
|
// Check if this is the /clean-ui command (refresh UI without clearing history)
|
|
@@ -2486,7 +2669,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2486
2669
|
ShellInputAgent.terminateSession(shellId);
|
|
2487
2670
|
quickLog(`[${new Date().toISOString()}] [AgentControl] Terminated ShellInputAgent session for ${shellId}\n`);
|
|
2488
2671
|
}
|
|
2489
|
-
}, onWarpifySession: async () => {
|
|
2672
|
+
}, onWarpifySession: state.shellState?.isBackgroundTask ? undefined : async () => {
|
|
2490
2673
|
// Warpify: Detect if there's an active remote session and establish ssh2 connection
|
|
2491
2674
|
const shellCommand = state.shellState?.command || '';
|
|
2492
2675
|
const currentOutput = state.shellState?.output || '';
|
|
@@ -2513,54 +2696,66 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2513
2696
|
// Remote session detected - warpify it!
|
|
2514
2697
|
const description = getSessionDescription(session);
|
|
2515
2698
|
quickLog(`[${new Date().toISOString()}] [Warpify] Warpifying: ${description}\n`);
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
historyMessages
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2699
|
+
warpifyInProgressRef.current = true;
|
|
2700
|
+
warpifyTerminatingCommandRef.current = shellCommand;
|
|
2701
|
+
try {
|
|
2702
|
+
// Terminate the PTY session (we'll use ssh2 handler instead)
|
|
2703
|
+
onShellSignal('SIGTERM');
|
|
2704
|
+
// Capture history commands
|
|
2705
|
+
const historyMessages = [];
|
|
2706
|
+
// 1. Add the command the user typed (e.g., "ssh user@host")
|
|
2707
|
+
if (shellCommand) {
|
|
2708
|
+
historyMessages.push({
|
|
2709
|
+
id: `warpify-cmd-${Date.now()}`,
|
|
2710
|
+
role: 'user',
|
|
2711
|
+
content: shellCommand,
|
|
2712
|
+
timestamp: new Date(),
|
|
2713
|
+
isCommandMode: false // Treat as a normal chat message for history
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
// 2. Add the terminal output (e.g., login banner, password prompt output)
|
|
2717
|
+
if (currentOutput && currentOutput.trim().length > 0) {
|
|
2718
|
+
historyMessages.push({
|
|
2719
|
+
id: `warpify-out-${Date.now()}`,
|
|
2720
|
+
role: 'tool', // Use 'tool' role to mimic command execution output
|
|
2721
|
+
content: '',
|
|
2722
|
+
timestamp: new Date(),
|
|
2723
|
+
toolExecution: {
|
|
2724
|
+
toolName: 'execute_command',
|
|
2725
|
+
status: 'completed',
|
|
2726
|
+
result: currentOutput, // This contains the PTY output including ANSI codes
|
|
2727
|
+
arguments: {
|
|
2728
|
+
command: shellCommand,
|
|
2729
|
+
isPty: true
|
|
2730
|
+
}
|
|
2544
2731
|
}
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
// Clear shell-related UI state and append captured history
|
|
2735
|
+
// Reset isAiWorking to prevent "Boosting..." spinner from appearing
|
|
2736
|
+
setState(prev => ({
|
|
2737
|
+
...prev,
|
|
2738
|
+
messageHistory: [...prev.messageHistory, ...historyMessages],
|
|
2739
|
+
shellState: undefined,
|
|
2740
|
+
currentMessage: null,
|
|
2741
|
+
isAiWorking: false
|
|
2742
|
+
}));
|
|
2743
|
+
// Clear screen
|
|
2744
|
+
clearScreen();
|
|
2745
|
+
// Establish proper ssh2 connection via cli-adapter
|
|
2746
|
+
// This will prompt for password and set up full SSH functionality
|
|
2747
|
+
const success = await onWarpifySession(shellCommand, session.type, session.connectionString);
|
|
2748
|
+
if (!success) {
|
|
2749
|
+
// Connection failed - error already shown by cli.warpifySession
|
|
2750
|
+
quickLog(`[${new Date().toISOString()}] [Warpify] Failed to establish ssh2 connection\n`);
|
|
2751
|
+
}
|
|
2547
2752
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
isAiWorking: false
|
|
2555
|
-
}));
|
|
2556
|
-
// Clear screen
|
|
2557
|
-
clearScreen();
|
|
2558
|
-
// Establish proper ssh2 connection via cli-adapter
|
|
2559
|
-
// This will prompt for password and set up full SSH functionality
|
|
2560
|
-
const success = await onWarpifySession(shellCommand, session.type, session.connectionString);
|
|
2561
|
-
if (!success) {
|
|
2562
|
-
// Connection failed - error already shown by cli.warpifySession
|
|
2563
|
-
quickLog(`[${new Date().toISOString()}] [Warpify] Failed to establish ssh2 connection\n`);
|
|
2753
|
+
finally {
|
|
2754
|
+
// Keep suppression active briefly so delayed shell completion events are ignored.
|
|
2755
|
+
setTimeout(() => {
|
|
2756
|
+
warpifyInProgressRef.current = false;
|
|
2757
|
+
warpifyTerminatingCommandRef.current = null;
|
|
2758
|
+
}, 400);
|
|
2564
2759
|
}
|
|
2565
2760
|
} })),
|
|
2566
2761
|
state.shellState?.isAgentControlled && state.shellState?.isFocused && state.shellState?.isRunning && (React.createElement(MonitorModeAIPanel, { messages: [
|
|
@@ -2577,7 +2772,17 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2577
2772
|
!(state.currentMessage?.toolExecution?.status === 'executing') && (React.createElement(Box, { marginBottom: 1, paddingLeft: 1 },
|
|
2578
2773
|
React.createElement(LoadingIndicator, { key: "loading-indicator" }),
|
|
2579
2774
|
React.createElement(AgentTimer, { key: "agent-timer" }))),
|
|
2580
|
-
!state.shellState?.isFocused && (state.
|
|
2775
|
+
!state.shellState?.isFocused && (state.passwordRequest ? (React.createElement(PasswordPrompt, { message: state.passwordRequest.message, onSubmit: (password) => {
|
|
2776
|
+
const resolve = state.passwordRequest?.resolve;
|
|
2777
|
+
if (resolve) {
|
|
2778
|
+
resolve(password);
|
|
2779
|
+
}
|
|
2780
|
+
}, onCancel: () => {
|
|
2781
|
+
const reject = state.passwordRequest?.reject;
|
|
2782
|
+
if (reject) {
|
|
2783
|
+
reject();
|
|
2784
|
+
}
|
|
2785
|
+
} })) : state.approvalRequest ? (React.createElement(ApprovalSection, { key: `approval-${state.approvalRequest.message}`, approvalRequest: state.approvalRequest, onApprove: (approved) => {
|
|
2581
2786
|
const resolve = state.approvalRequest?.resolve;
|
|
2582
2787
|
if (resolve) {
|
|
2583
2788
|
resolve(approved);
|
|
@@ -2590,7 +2795,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2590
2795
|
handleSubmit(value, clipboardImages);
|
|
2591
2796
|
}, autoAcceptMode: state.autoAcceptMode, model: state.currentModel, planMode: state.planMode, commandMode: state.commandMode, backgroundMode: state.backgroundMode, currentWorkingDirectory: state.currentWorkingDirectory, commandHistory: state.commandHistory, onToggleAutoAccept: handleToggleAutoAccept, onToggleCommandMode: onToggleCommandMode, onToggleBackgroundMode: onToggleBackgroundMode, isActive: true, subshellContext: state.subshellContext, subshellContextStack: state.subshellContextStack, currentTokens: state.currentTokens, maxTokens: state.maxTokens, contextLimitReached: state.contextLimitReached, isShellRunning: state.shellState?.isRunning, backgroundTaskCount: state.backgroundTaskCount, subAgentCount: state.subAgentCount, initialValue: preservedInputTextRef.current, onValueChange: handleInputValueChange, onSetAutoModeSetup: (callback) => {
|
|
2592
2797
|
setAutoModeCallbackRef.current = callback;
|
|
2593
|
-
}, sessionQuotaExhausted: state.sessionQuotaExhausted, sessionQuotaTimeRemaining: state.sessionQuotaTimeRemaining, aiAutoSuggestEnabled: state.aiAutoSuggest, sessionCommands: sessionCommands }))),
|
|
2798
|
+
}, sessionQuotaExhausted: state.sessionQuotaExhausted, sessionQuotaTimeRemaining: state.sessionQuotaTimeRemaining, aiAutoSuggestEnabled: state.aiAutoSuggest, sessionCommands: sessionCommands, getCheckpoints: getCheckpoints, onSetInputSetup: onSetInputSetup }))),
|
|
2594
2799
|
state.showExitWarning && (React.createElement(Box, { marginTop: 1 },
|
|
2595
2800
|
React.createElement(Text, { color: "#ffaa00", bold: true }, "\u26A0\uFE0F Press Ctrl+C again to exit"))),
|
|
2596
2801
|
!isConnected && (React.createElement(Box, { marginTop: 0, marginLeft: state.showExitWarning ? 2 : 0 },
|