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.
Files changed (99) hide show
  1. package/dist/cli-adapter.d.ts +27 -2
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +410 -17
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/slash-commands.d.ts.map +1 -1
  6. package/dist/config/slash-commands.js +7 -0
  7. package/dist/config/slash-commands.js.map +1 -1
  8. package/dist/context/context-manager.d.ts +4 -0
  9. package/dist/context/context-manager.d.ts.map +1 -1
  10. package/dist/context/context-manager.js +6 -0
  11. package/dist/context/context-manager.js.map +1 -1
  12. package/dist/context/handlers/docker-handler.d.ts +5 -1
  13. package/dist/context/handlers/docker-handler.d.ts.map +1 -1
  14. package/dist/context/handlers/docker-handler.js +27 -10
  15. package/dist/context/handlers/docker-handler.js.map +1 -1
  16. package/dist/context/handlers/ssh-handler.d.ts +47 -1
  17. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  18. package/dist/context/handlers/ssh-handler.js +546 -73
  19. package/dist/context/handlers/ssh-handler.js.map +1 -1
  20. package/dist/context/handlers/wsl-handler.d.ts +5 -1
  21. package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
  22. package/dist/context/handlers/wsl-handler.js +24 -6
  23. package/dist/context/handlers/wsl-handler.js.map +1 -1
  24. package/dist/context/subshell-handler.d.ts +8 -2
  25. package/dist/context/subshell-handler.d.ts.map +1 -1
  26. package/dist/index.js +12 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/services/checkpoint-manager.d.ts +162 -0
  29. package/dist/services/checkpoint-manager.d.ts.map +1 -0
  30. package/dist/services/checkpoint-manager.js +926 -0
  31. package/dist/services/checkpoint-manager.js.map +1 -0
  32. package/dist/services/fast-context-agent.d.ts +12 -0
  33. package/dist/services/fast-context-agent.d.ts.map +1 -0
  34. package/dist/services/fast-context-agent.js +253 -0
  35. package/dist/services/fast-context-agent.js.map +1 -0
  36. package/dist/tools/background-command.d.ts.map +1 -1
  37. package/dist/tools/background-command.js +132 -24
  38. package/dist/tools/background-command.js.map +1 -1
  39. package/dist/tools/command.d.ts.map +1 -1
  40. package/dist/tools/command.js +14 -4
  41. package/dist/tools/command.js.map +1 -1
  42. package/dist/tools/create-image.d.ts.map +1 -1
  43. package/dist/tools/create-image.js +43 -18
  44. package/dist/tools/create-image.js.map +1 -1
  45. package/dist/tools/fast-context.d.ts +3 -0
  46. package/dist/tools/fast-context.d.ts.map +1 -0
  47. package/dist/tools/fast-context.js +72 -0
  48. package/dist/tools/fast-context.js.map +1 -0
  49. package/dist/tools/file-ops.d.ts.map +1 -1
  50. package/dist/tools/file-ops.js +12 -12
  51. package/dist/tools/file-ops.js.map +1 -1
  52. package/dist/tools/find-files.d.ts +2 -1
  53. package/dist/tools/find-files.d.ts.map +1 -1
  54. package/dist/tools/find-files.js +62 -2
  55. package/dist/tools/find-files.js.map +1 -1
  56. package/dist/tools/get-diff.d.ts +9 -45
  57. package/dist/tools/get-diff.d.ts.map +1 -1
  58. package/dist/tools/get-diff.js +288 -171
  59. package/dist/tools/get-diff.js.map +1 -1
  60. package/dist/tools/types.d.ts +4 -1
  61. package/dist/tools/types.d.ts.map +1 -1
  62. package/dist/types/index.d.ts +1 -0
  63. package/dist/types/index.d.ts.map +1 -1
  64. package/dist/ui/components/App.d.ts +8 -0
  65. package/dist/ui/components/App.d.ts.map +1 -1
  66. package/dist/ui/components/App.js +290 -85
  67. package/dist/ui/components/App.js.map +1 -1
  68. package/dist/ui/components/ConfirmPrompt.d.ts +2 -0
  69. package/dist/ui/components/ConfirmPrompt.d.ts.map +1 -1
  70. package/dist/ui/components/ConfirmPrompt.js +8 -3
  71. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  72. package/dist/ui/components/InputBox.d.ts +6 -0
  73. package/dist/ui/components/InputBox.d.ts.map +1 -1
  74. package/dist/ui/components/InputBox.js +130 -6
  75. package/dist/ui/components/InputBox.js.map +1 -1
  76. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  77. package/dist/ui/components/InteractiveShell.js +50 -15
  78. package/dist/ui/components/InteractiveShell.js.map +1 -1
  79. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  80. package/dist/ui/components/MessageDisplay.js +2 -2
  81. package/dist/ui/components/MessageDisplay.js.map +1 -1
  82. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  83. package/dist/ui/components/ToolExecutionMessage.js +213 -18
  84. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  85. package/dist/utils/ansi-encoder.d.ts +5 -0
  86. package/dist/utils/ansi-encoder.d.ts.map +1 -1
  87. package/dist/utils/ansi-encoder.js +5 -5
  88. package/dist/utils/ansi-encoder.js.map +1 -1
  89. package/dist/utils/editor-utils.d.ts +5 -0
  90. package/dist/utils/editor-utils.d.ts.map +1 -1
  91. package/dist/utils/editor-utils.js +67 -0
  92. package/dist/utils/editor-utils.js.map +1 -1
  93. package/dist/utils/input-classifier.d.ts.map +1 -1
  94. package/dist/utils/input-classifier.js +2 -1
  95. package/dist/utils/input-classifier.js.map +1 -1
  96. package/dist/utils/terminal-output.d.ts.map +1 -1
  97. package/dist/utils/terminal-output.js +162 -103
  98. package/dist/utils/terminal-output.js.map +1 -1
  99. 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
- // Clear the screen and restore messages
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: restoredMessages,
918
- screen: 'chat',
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
- // Auto-approve if auto-accept mode is on
1509
- if (autoAcceptRef.current) {
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 current message if it's a tool with matching name
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
- return {
1653
- ...prev,
1654
- currentMessage: {
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
- ...prev.currentMessage.toolExecution,
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: 'password-prompt',
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 /clear command
2101
- if (trimmedValue === '/clear' || trimmedValue === '/cls' || trimmedValue === '/reset') {
2102
- // Clear the screen and reset state
2103
- clearScreen();
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
- isLoading: false,
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
- // Terminate the PTY session (we'll use ssh2 handler instead)
2517
- onShellSignal('SIGTERM');
2518
- // Capture history commands
2519
- const historyMessages = [];
2520
- // 1. Add the command the user typed (e.g., "ssh user@host")
2521
- if (shellCommand) {
2522
- historyMessages.push({
2523
- id: `warpify-cmd-${Date.now()}`,
2524
- role: 'user',
2525
- content: shellCommand,
2526
- timestamp: new Date(),
2527
- isCommandMode: false // Treat as a normal chat message for history
2528
- });
2529
- }
2530
- // 2. Add the terminal output (e.g., login banner, password prompt output)
2531
- if (currentOutput && currentOutput.trim().length > 0) {
2532
- historyMessages.push({
2533
- id: `warpify-out-${Date.now()}`,
2534
- role: 'tool', // Use 'tool' role to mimic command execution output
2535
- content: '',
2536
- timestamp: new Date(),
2537
- toolExecution: {
2538
- toolName: 'execute_command',
2539
- status: 'completed',
2540
- result: currentOutput, // This contains the PTY output including ANSI codes
2541
- arguments: {
2542
- command: shellCommand,
2543
- isPty: true
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
- // Clear the shell state AND append the history messages
2549
- // Also reset isAiWorking to prevent "Boosting..." spinner from appearing
2550
- setState(prev => ({
2551
- ...prev,
2552
- messageHistory: [...prev.messageHistory, ...historyMessages],
2553
- shellState: undefined,
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.approvalRequest ? (React.createElement(ApprovalSection, { key: `approval-${state.approvalRequest.message}`, approvalRequest: state.approvalRequest, onApprove: (approved) => {
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 },