centaurus-cli 2.9.4 → 2.9.6
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 +29 -4
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +700 -121
- 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 +10 -0
- package/dist/context/context-manager.d.ts.map +1 -1
- package/dist/context/context-manager.js +17 -0
- package/dist/context/context-manager.js.map +1 -1
- package/dist/context/handlers/docker-handler.d.ts +7 -1
- package/dist/context/handlers/docker-handler.d.ts.map +1 -1
- package/dist/context/handlers/docker-handler.js +89 -16
- 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/local-chat-storage.d.ts +3 -1
- package/dist/services/local-chat-storage.d.ts.map +1 -1
- package/dist/services/local-chat-storage.js +8 -3
- package/dist/services/local-chat-storage.js.map +1 -1
- 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 +106 -42
- 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/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/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/grep-search.d.ts +1 -1
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +80 -1
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/types.d.ts +3 -0
- package/dist/tools/types.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 +256 -66
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/ui/components/Breadcrumbs.js +22 -2
- package/dist/ui/components/Breadcrumbs.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 +188 -23
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts +2 -0
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +88 -26
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/KeyboardHelp.d.ts.map +1 -1
- package/dist/ui/components/KeyboardHelp.js +14 -6
- package/dist/ui/components/KeyboardHelp.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +35 -16
- 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 +12 -5
- package/dist/utils/ansi-encoder.js.map +1 -1
- package/dist/utils/editor-utils.d.ts +14 -0
- package/dist/utils/editor-utils.d.ts.map +1 -1
- package/dist/utils/editor-utils.js +172 -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 +3 -1
- package/dist/utils/terminal-output.d.ts.map +1 -1
- package/dist/utils/terminal-output.js +235 -195
- package/dist/utils/terminal-output.js.map +1 -1
- package/package.json +3 -1
package/dist/cli-adapter.js
CHANGED
|
@@ -46,6 +46,7 @@ import { BackgroundTaskManager } from './services/background-task-manager.js';
|
|
|
46
46
|
import { sessionQuotaManager } from './services/session-quota-manager.js';
|
|
47
47
|
import { ollamaService, OllamaService } from './services/ollama-service.js';
|
|
48
48
|
import { workflowStorage } from './services/workflow-storage.js';
|
|
49
|
+
import { CheckpointManager } from './services/checkpoint-manager.js';
|
|
49
50
|
export class CentaurusCLI {
|
|
50
51
|
configManager;
|
|
51
52
|
toolRegistry;
|
|
@@ -94,7 +95,7 @@ export class CentaurusCLI {
|
|
|
94
95
|
onRestoreMessagesCallback;
|
|
95
96
|
uiMessageHistory = []; // Mirror of App.tsx's messageHistory for saving
|
|
96
97
|
cwdStack = []; // Stack of CWDs for nested sessions (pushed when entering, popped when exiting)
|
|
97
|
-
|
|
98
|
+
connectionCommandStack = []; // Stack of commands used to connect (for nested sessions like SSH>SSH, SSH>Docker)
|
|
98
99
|
onBackgroundModeChange;
|
|
99
100
|
onBackgroundTaskCountChange;
|
|
100
101
|
onSetAutoMode;
|
|
@@ -116,9 +117,16 @@ export class CentaurusCLI {
|
|
|
116
117
|
onPromptAnswered; // Callback when AI answers a shell prompt
|
|
117
118
|
onShowWorkflowCreatorCallback; // Callback to show workflow creator screen with optional initial steps
|
|
118
119
|
onWorkflowSaveCallback; // Callback when workflow is saved
|
|
120
|
+
onAiAutoSuggestChange; // Callback for AI auto-suggest setting changes
|
|
121
|
+
onRevertToCheckpointCallback; // Callback for revert UI update
|
|
122
|
+
// Checkpoint manager for revert functionality
|
|
123
|
+
checkpointManager;
|
|
124
|
+
currentCheckpointId; // Track current checkpoint being created
|
|
119
125
|
// Workflow learning mode state
|
|
120
126
|
workflowLearningActive = false;
|
|
121
127
|
learnedWorkflowSteps = [];
|
|
128
|
+
// Callback to set input value (e.g., for revert)
|
|
129
|
+
onSetInputCallback = null;
|
|
122
130
|
constructor() {
|
|
123
131
|
this.configManager = new ConfigManager();
|
|
124
132
|
this.toolRegistry = new ToolRegistry();
|
|
@@ -233,6 +241,24 @@ export class CentaurusCLI {
|
|
|
233
241
|
setOnWorkflowSave(callback) {
|
|
234
242
|
this.onWorkflowSaveCallback = callback;
|
|
235
243
|
}
|
|
244
|
+
setOnAiAutoSuggestChange(callback) {
|
|
245
|
+
this.onAiAutoSuggestChange = callback;
|
|
246
|
+
}
|
|
247
|
+
setOnRevertToCheckpoint(callback) {
|
|
248
|
+
this.onRevertToCheckpointCallback = callback;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Get checkpoints for autocomplete dropdown
|
|
252
|
+
*/
|
|
253
|
+
getCheckpointsForAutocomplete() {
|
|
254
|
+
if (!this.checkpointManager)
|
|
255
|
+
return [];
|
|
256
|
+
return this.checkpointManager.list().map(cp => ({
|
|
257
|
+
id: cp.id,
|
|
258
|
+
prompt: cp.prompt,
|
|
259
|
+
timestamp: new Date(cp.createdAt)
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
236
262
|
/**
|
|
237
263
|
* Save a workflow from the workflow creator UI
|
|
238
264
|
*/
|
|
@@ -395,9 +421,12 @@ Begin executing now, starting with Step 1.`;
|
|
|
395
421
|
// starting 'fresh' in the remote user's home/default dir is safer for 'warpify' semantics regardless of type.
|
|
396
422
|
// However, to avoid regression for WSL/Docker usage where inheritance is expected, we restricts this fix to SSH.
|
|
397
423
|
const initialCwd = type === 'ssh' ? undefined : this.cwd;
|
|
398
|
-
|
|
424
|
+
// Use factory method to create a new handler instance for this session
|
|
425
|
+
// This prevents state issues where the singleton handler's client is overwritten by nested sessions
|
|
426
|
+
const newHandler = detection.handler.createNew();
|
|
427
|
+
context = await newHandler.connect(command, initialCwd);
|
|
399
428
|
}
|
|
400
|
-
this.
|
|
429
|
+
this.connectionCommandStack.push(command);
|
|
401
430
|
this.contextManager.pushContext(context);
|
|
402
431
|
// Explicitly sync this.cwd with the new context's CWD to ensure consistency immediately
|
|
403
432
|
if (context.metadata.workingDirectory) {
|
|
@@ -481,7 +510,7 @@ Begin executing now, starting with Step 1.`;
|
|
|
481
510
|
}
|
|
482
511
|
this.contextManager.updateWorkingDirectory(this.cwd);
|
|
483
512
|
if (newContext.type === 'local') {
|
|
484
|
-
this.
|
|
513
|
+
this.connectionCommandStack.pop();
|
|
485
514
|
}
|
|
486
515
|
// Notify CWD change
|
|
487
516
|
if (this.onCwdChange) {
|
|
@@ -530,7 +559,7 @@ Begin executing now, starting with Step 1.`;
|
|
|
530
559
|
}
|
|
531
560
|
this.contextManager.updateWorkingDirectory(this.cwd);
|
|
532
561
|
if (currentContext.type === 'local') {
|
|
533
|
-
this.
|
|
562
|
+
this.connectionCommandStack.pop();
|
|
534
563
|
}
|
|
535
564
|
// Notify CWD change
|
|
536
565
|
if (this.onCwdChange) {
|
|
@@ -1279,12 +1308,104 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1279
1308
|
// Start logging session and log user message
|
|
1280
1309
|
conversationLogger.startSession();
|
|
1281
1310
|
conversationLogger.logUserMessage(message);
|
|
1311
|
+
// Initialize and start checkpoint for revert functionality
|
|
1312
|
+
if (!this.checkpointManager) {
|
|
1313
|
+
this.checkpointManager = new CheckpointManager();
|
|
1314
|
+
}
|
|
1315
|
+
// Ensure we have a chat ID for checkpoints (even for the first message)
|
|
1316
|
+
if (!this.currentChatId) {
|
|
1317
|
+
this.currentChatId = localChatStorage.generateChatId();
|
|
1318
|
+
}
|
|
1319
|
+
if (this.currentChatId) {
|
|
1320
|
+
this.checkpointManager.setCurrentChatId(this.currentChatId);
|
|
1321
|
+
this.currentCheckpointId = undefined;
|
|
1322
|
+
// Start checkpoint snapshot for this user message
|
|
1323
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
1324
|
+
// Build remote session info and handler for non-local contexts
|
|
1325
|
+
let remoteSessionInfo;
|
|
1326
|
+
let remoteHandler;
|
|
1327
|
+
if (currentContext.type !== 'local' && currentContext.handler) {
|
|
1328
|
+
const metadata = currentContext.metadata;
|
|
1329
|
+
remoteSessionInfo = {
|
|
1330
|
+
hostname: metadata.hostname,
|
|
1331
|
+
username: metadata.username,
|
|
1332
|
+
sessionId: currentContext.sessionId,
|
|
1333
|
+
connectionString: metadata.hostname
|
|
1334
|
+
? `${metadata.username || 'user'}@${metadata.hostname}`
|
|
1335
|
+
: metadata.distroName || metadata.containerId || undefined,
|
|
1336
|
+
};
|
|
1337
|
+
// Use the handler if it implements the RemoteFileHandler interface
|
|
1338
|
+
if (typeof currentContext.handler.readFile === 'function' &&
|
|
1339
|
+
typeof currentContext.handler.writeFile === 'function' &&
|
|
1340
|
+
typeof currentContext.handler.executeCommand === 'function' &&
|
|
1341
|
+
typeof currentContext.handler.isConnected === 'function') {
|
|
1342
|
+
remoteHandler = currentContext.handler;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
try {
|
|
1346
|
+
const checkpointStartPromise = this.checkpointManager.startCheckpoint({
|
|
1347
|
+
prompt: message,
|
|
1348
|
+
cwd: this.cwd,
|
|
1349
|
+
contextType: currentContext.type,
|
|
1350
|
+
conversationIndex: this.conversationHistory.length - 1,
|
|
1351
|
+
uiMessageIndex: this.uiMessageHistory.length,
|
|
1352
|
+
remoteSessionInfo,
|
|
1353
|
+
handler: remoteHandler,
|
|
1354
|
+
});
|
|
1355
|
+
let checkpoint = null;
|
|
1356
|
+
if (currentContext.type !== 'local') {
|
|
1357
|
+
const remoteCheckpointTimeoutMs = 2000;
|
|
1358
|
+
const timeoutMarker = '__checkpoint_timeout__';
|
|
1359
|
+
let timeoutHandle = null;
|
|
1360
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1361
|
+
timeoutHandle = setTimeout(() => resolve(timeoutMarker), remoteCheckpointTimeoutMs);
|
|
1362
|
+
});
|
|
1363
|
+
const checkpointOrTimeout = await Promise.race([
|
|
1364
|
+
checkpointStartPromise,
|
|
1365
|
+
timeoutPromise,
|
|
1366
|
+
]);
|
|
1367
|
+
if (timeoutHandle) {
|
|
1368
|
+
clearTimeout(timeoutHandle);
|
|
1369
|
+
}
|
|
1370
|
+
if (checkpointOrTimeout === timeoutMarker) {
|
|
1371
|
+
quickLog(`[${new Date().toISOString()}] [Checkpoint] Remote checkpoint start exceeded ${remoteCheckpointTimeoutMs}ms (${currentContext.type}); continuing without blocking AI turn\n`);
|
|
1372
|
+
void checkpointStartPromise
|
|
1373
|
+
.then((lateCheckpoint) => {
|
|
1374
|
+
if (!lateCheckpoint || !this.checkpointManager) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
this.checkpointManager.discardCheckpointById(lateCheckpoint.id);
|
|
1378
|
+
quickLog(`[${new Date().toISOString()}] [Checkpoint] Discarded late checkpoint ${lateCheckpoint.id} created after timeout\n`);
|
|
1379
|
+
})
|
|
1380
|
+
.catch((lateError) => {
|
|
1381
|
+
quickLog(`[${new Date().toISOString()}] [Checkpoint] Late checkpoint creation failed after timeout: ${lateError?.message || lateError}\n`);
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
else {
|
|
1385
|
+
checkpoint = checkpointOrTimeout;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
checkpoint = await checkpointStartPromise;
|
|
1390
|
+
}
|
|
1391
|
+
if (checkpoint) {
|
|
1392
|
+
this.currentCheckpointId = checkpoint.id;
|
|
1393
|
+
quickLog(`[${new Date().toISOString()}] [Checkpoint] Started checkpoint ${checkpoint.id} (${currentContext.type}) for: "${message.slice(0, 50)}..."\n`);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
quickLog(`[${new Date().toISOString()}] [Checkpoint] Failed to start checkpoint: ${error.message}\n`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1282
1400
|
try {
|
|
1283
1401
|
const tools = this.toolRegistry.getSchemas();
|
|
1284
1402
|
const context = {
|
|
1285
1403
|
cwd: this.cwd,
|
|
1286
1404
|
contextManager: this.contextManager,
|
|
1287
1405
|
cliAdapter: this, // Pass CLI adapter reference for interactive process management
|
|
1406
|
+
checkpointManager: this.checkpointManager, // For session-aware diff tool
|
|
1407
|
+
currentCheckpointId: this.currentCheckpointId, // Active checkpoint for this request
|
|
1408
|
+
currentChatId: this.currentChatId || undefined, // Current chat session ID (for session-wide diffs)
|
|
1288
1409
|
requireApproval: async (message, risky, preview, operationType, operationDetails) => {
|
|
1289
1410
|
// Special bypass for shell input to running processes:
|
|
1290
1411
|
// If the AI is sending input to an existing shell (via shell_input), we bypass the separate approval step.
|
|
@@ -2391,6 +2512,17 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
2391
2512
|
throw new Error(`AI Error: ${error.message}`);
|
|
2392
2513
|
}
|
|
2393
2514
|
finally {
|
|
2515
|
+
// Finalize checkpoint if one was created for this message
|
|
2516
|
+
if (this.currentCheckpointId && this.checkpointManager) {
|
|
2517
|
+
try {
|
|
2518
|
+
await this.checkpointManager.finalizeCheckpoint(this.currentCheckpointId);
|
|
2519
|
+
quickLog(`[${new Date().toISOString()}] [Checkpoint] Finalized checkpoint ${this.currentCheckpointId}\n`);
|
|
2520
|
+
}
|
|
2521
|
+
catch (error) {
|
|
2522
|
+
quickLog(`[${new Date().toISOString()}] [Checkpoint] Failed to finalize checkpoint: ${error.message}\n`);
|
|
2523
|
+
}
|
|
2524
|
+
this.currentCheckpointId = undefined;
|
|
2525
|
+
}
|
|
2394
2526
|
// Clean up abort controller
|
|
2395
2527
|
this.currentAbortController = undefined;
|
|
2396
2528
|
}
|
|
@@ -2813,12 +2945,18 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2813
2945
|
const value = args[1].toLowerCase();
|
|
2814
2946
|
if (value === 'on') {
|
|
2815
2947
|
this.configManager.set('aiAutoSuggest', true);
|
|
2948
|
+
if (this.onAiAutoSuggestChange) {
|
|
2949
|
+
this.onAiAutoSuggestChange(true);
|
|
2950
|
+
}
|
|
2816
2951
|
responseMessage = '✅ **AI Auto-Suggestions Enabled**\n\n' +
|
|
2817
2952
|
'From now on, I will suggest commands after 5 seconds of inactivity.\n' +
|
|
2818
2953
|
'Suggestions will appear in grey text. Use the **Right Arrow** key to accept them.';
|
|
2819
2954
|
}
|
|
2820
2955
|
else if (value === 'off') {
|
|
2821
2956
|
this.configManager.set('aiAutoSuggest', false);
|
|
2957
|
+
if (this.onAiAutoSuggestChange) {
|
|
2958
|
+
this.onAiAutoSuggestChange(false);
|
|
2959
|
+
}
|
|
2822
2960
|
responseMessage = '✅ **AI Auto-Suggestions Disabled**\n\n' +
|
|
2823
2961
|
'I will no longer provide AI-powered command suggestions.';
|
|
2824
2962
|
}
|
|
@@ -3075,6 +3213,189 @@ Then try /models local again.`;
|
|
|
3075
3213
|
case 'exit':
|
|
3076
3214
|
process.exit(0);
|
|
3077
3215
|
break;
|
|
3216
|
+
case 'revert': {
|
|
3217
|
+
const checkpointArg = args[0];
|
|
3218
|
+
if (!checkpointArg) {
|
|
3219
|
+
responseMessage = '❌ Usage: `/revert <checkpoint-id>`\nType `/revert ` and use autocomplete to select a checkpoint.';
|
|
3220
|
+
break;
|
|
3221
|
+
}
|
|
3222
|
+
if (!this.checkpointManager) {
|
|
3223
|
+
responseMessage = '❌ No checkpoints available in this session.';
|
|
3224
|
+
break;
|
|
3225
|
+
}
|
|
3226
|
+
try {
|
|
3227
|
+
// Smart context matching: determine if we can revert based on current vs checkpoint context
|
|
3228
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
3229
|
+
const allCheckpoints = this.checkpointManager.list();
|
|
3230
|
+
const targetCheckpoint = allCheckpoints.find(c => c.id === checkpointArg);
|
|
3231
|
+
if (!targetCheckpoint) {
|
|
3232
|
+
responseMessage = `❌ Checkpoint "${checkpointArg}" not found.`;
|
|
3233
|
+
break;
|
|
3234
|
+
}
|
|
3235
|
+
// Context validation for revert
|
|
3236
|
+
const cpContextType = targetCheckpoint.contextType;
|
|
3237
|
+
const currentContextType = currentContext.type;
|
|
3238
|
+
if (cpContextType !== 'local' && currentContextType === 'local') {
|
|
3239
|
+
// Remote checkpoint but we're local — can't reconnect to SSH to revert files
|
|
3240
|
+
const sessionType = cpContextType.toUpperCase();
|
|
3241
|
+
const sessionInfo = targetCheckpoint.remoteSessionInfo;
|
|
3242
|
+
const target = sessionInfo?.connectionString || sessionInfo?.hostname || 'the remote machine';
|
|
3243
|
+
responseMessage = `❌ This checkpoint was created during a ${sessionType} session (${target}).\n` +
|
|
3244
|
+
`You are not currently connected to that session.\n\n` +
|
|
3245
|
+
`Please reconnect to the ${sessionType} session first, then retry /revert.`;
|
|
3246
|
+
break;
|
|
3247
|
+
}
|
|
3248
|
+
if (cpContextType === 'local' && currentContextType !== 'local') {
|
|
3249
|
+
// Local checkpoint but we're in a remote session — wrong context
|
|
3250
|
+
responseMessage = `❌ This checkpoint was created in a local session.\n` +
|
|
3251
|
+
`You are currently in a ${currentContextType.toUpperCase()} session.\n\n` +
|
|
3252
|
+
`Please exit your remote session first, then retry /revert.`;
|
|
3253
|
+
break;
|
|
3254
|
+
}
|
|
3255
|
+
if (cpContextType !== 'local' && currentContextType !== 'local' && cpContextType !== currentContextType) {
|
|
3256
|
+
// Both remote but different types (e.g., SSH checkpoint but in Docker now)
|
|
3257
|
+
responseMessage = `❌ This checkpoint was created in a ${cpContextType.toUpperCase()} session, ` +
|
|
3258
|
+
`but you are currently in a ${currentContextType.toUpperCase()} session.\n\n` +
|
|
3259
|
+
`Please connect to the correct ${cpContextType.toUpperCase()} session first.`;
|
|
3260
|
+
break;
|
|
3261
|
+
}
|
|
3262
|
+
// For remote checkpoints with mismatched hosts, validate the connection target
|
|
3263
|
+
if (cpContextType !== 'local' && targetCheckpoint.remoteSessionInfo) {
|
|
3264
|
+
const cpHost = targetCheckpoint.remoteSessionInfo.hostname;
|
|
3265
|
+
const currentHost = currentContext.metadata.hostname;
|
|
3266
|
+
if (cpHost && currentHost && cpHost !== currentHost) {
|
|
3267
|
+
responseMessage = `❌ This checkpoint was created on ${cpHost}, ` +
|
|
3268
|
+
`but you are currently connected to ${currentHost}.\n\n` +
|
|
3269
|
+
`Please connect to ${cpHost} first, then retry /revert.`;
|
|
3270
|
+
break;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
// Build handler for remote revert if needed
|
|
3274
|
+
let revertHandler;
|
|
3275
|
+
if (cpContextType !== 'local' && currentContext.handler) {
|
|
3276
|
+
if (typeof currentContext.handler.readFile === 'function' &&
|
|
3277
|
+
typeof currentContext.handler.writeFile === 'function' &&
|
|
3278
|
+
typeof currentContext.handler.executeCommand === 'function' &&
|
|
3279
|
+
typeof currentContext.handler.isConnected === 'function') {
|
|
3280
|
+
revertHandler = currentContext.handler;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
const result = await this.checkpointManager.revertToCheckpoint(checkpointArg, revertHandler);
|
|
3284
|
+
// Find checkpoint index for UI truncation
|
|
3285
|
+
const checkpoints = this.checkpointManager.list();
|
|
3286
|
+
const checkpointIndex = checkpoints.findIndex(c => c.id === checkpointArg);
|
|
3287
|
+
// Truncate conversation history to the checkpoint
|
|
3288
|
+
if (checkpointIndex >= 0) {
|
|
3289
|
+
const checkpoint = result.checkpoint;
|
|
3290
|
+
// Populate the input bar with the reverted prompt
|
|
3291
|
+
if (this.onSetInputCallback) {
|
|
3292
|
+
this.onSetInputCallback(checkpoint.prompt);
|
|
3293
|
+
}
|
|
3294
|
+
// Use conversationIndex from metadata if available (robust)
|
|
3295
|
+
// This avoids issues with duplicate prompts getting truncated at the wrong occurrence
|
|
3296
|
+
if (typeof checkpoint.conversationIndex === 'number' &&
|
|
3297
|
+
checkpoint.conversationIndex >= 0 &&
|
|
3298
|
+
checkpoint.conversationIndex < this.conversationHistory.length) {
|
|
3299
|
+
// Truncate to index (exclusive) to remove the message from history
|
|
3300
|
+
// This allows the user to "edit" the message in the input bar
|
|
3301
|
+
this.conversationHistory = this.conversationHistory.slice(0, checkpoint.conversationIndex);
|
|
3302
|
+
quickLog(`[${new Date().toISOString()}] [Revert] Truncated conversation history to index ${checkpoint.conversationIndex} (exclusive)\n`);
|
|
3303
|
+
}
|
|
3304
|
+
else {
|
|
3305
|
+
// Fallback to string matching (legacy or missing index)
|
|
3306
|
+
const targetPrompt = checkpoint.prompt;
|
|
3307
|
+
// Find the user message with this prompt in conversationHistory
|
|
3308
|
+
let foundIndex = -1;
|
|
3309
|
+
// Use a broader search to find the matching message
|
|
3310
|
+
// The prompt might be a substring or exact match
|
|
3311
|
+
const searchPrompt = targetPrompt.slice(0, 50);
|
|
3312
|
+
for (let i = 0; i < this.conversationHistory.length; i++) {
|
|
3313
|
+
const msg = this.conversationHistory[i];
|
|
3314
|
+
if (msg.role === 'user' && msg.content && msg.content.includes(searchPrompt)) {
|
|
3315
|
+
foundIndex = i;
|
|
3316
|
+
break;
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
if (foundIndex >= 0) {
|
|
3320
|
+
// Keep messages up to but NOT including this user message
|
|
3321
|
+
this.conversationHistory = this.conversationHistory.slice(0, foundIndex);
|
|
3322
|
+
quickLog(`[${new Date().toISOString()}] [Revert] Truncated conversation history to index ${foundIndex} (exclusive, using string match)\n`);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
// Fix: Also truncate UI message history to ensure consistency
|
|
3326
|
+
// Use uiMessageIndex from metadata if available (robust)
|
|
3327
|
+
if (typeof checkpoint.uiMessageIndex === 'number' &&
|
|
3328
|
+
checkpoint.uiMessageIndex >= 0 &&
|
|
3329
|
+
checkpoint.uiMessageIndex < this.uiMessageHistory.length) {
|
|
3330
|
+
// Truncate to index (exclusive)
|
|
3331
|
+
this.uiMessageHistory = this.uiMessageHistory.slice(0, checkpoint.uiMessageIndex);
|
|
3332
|
+
quickLog(`[${new Date().toISOString()}] [Revert] Truncated UI message history to index ${checkpoint.uiMessageIndex} (exclusive)\n`);
|
|
3333
|
+
}
|
|
3334
|
+
else {
|
|
3335
|
+
// Fallback to string matching (legacy or missing index)
|
|
3336
|
+
const targetPrompt = checkpoint.prompt;
|
|
3337
|
+
const searchPrompt = targetPrompt.slice(0, 50);
|
|
3338
|
+
let foundUiIndex = -1;
|
|
3339
|
+
for (let i = 0; i < this.uiMessageHistory.length; i++) {
|
|
3340
|
+
const msg = this.uiMessageHistory[i];
|
|
3341
|
+
if (msg.role === 'user' && msg.content && msg.content.includes(searchPrompt)) {
|
|
3342
|
+
foundUiIndex = i;
|
|
3343
|
+
break;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
if (foundUiIndex >= 0) {
|
|
3347
|
+
// Keep messages up to but NOT including this user message
|
|
3348
|
+
this.uiMessageHistory = this.uiMessageHistory.slice(0, foundUiIndex);
|
|
3349
|
+
quickLog(`[${new Date().toISOString()}] [Revert] Truncated UI message history to index ${foundUiIndex} (exclusive, using string match)\n`);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
// Remove the reverted checkpoint AND all checkpoints created after it
|
|
3353
|
+
// They should not appear in autocomplete anymore
|
|
3354
|
+
this.checkpointManager.removeCheckpointsFrom(checkpointArg);
|
|
3355
|
+
// Build the success message BEFORE calling handleChatPickerSelection
|
|
3356
|
+
// This will be passed as a parameter and included in the same React setState call
|
|
3357
|
+
quickLog(`[${new Date().toISOString()}] [Revert] Building success message for checkpoint "${checkpointArg}"\n`);
|
|
3358
|
+
const truncatedPrompt = result.checkpoint.prompt.length > 50
|
|
3359
|
+
? result.checkpoint.prompt.slice(0, 50) + '...'
|
|
3360
|
+
: result.checkpoint.prompt;
|
|
3361
|
+
let revertSuccessMessage = `✅ Reverted to: "${truncatedPrompt}"\n` +
|
|
3362
|
+
`Restored ${result.restored} files, removed ${result.removed} files.`;
|
|
3363
|
+
if (result.errors.length > 0) {
|
|
3364
|
+
revertSuccessMessage += `\n⚠️ Warnings: ${result.errors.join(', ')}`;
|
|
3365
|
+
}
|
|
3366
|
+
// Save the truncated state to disk
|
|
3367
|
+
if (this.currentChatId) {
|
|
3368
|
+
quickLog(`[${new Date().toISOString()}] [Revert] Saving truncated chat to disk (chatId: ${this.currentChatId})\n`);
|
|
3369
|
+
quickLog(`[${new Date().toISOString()}] [Revert] uiMessageHistory.length BEFORE save: ${this.uiMessageHistory.length}\n`);
|
|
3370
|
+
this.saveCurrentChat();
|
|
3371
|
+
// Reload the chat to force UI update ensures consistency
|
|
3372
|
+
// This simulates a /chat resume command which correctly populates the UI
|
|
3373
|
+
// Pass skipLoadedMessage=true and the revert success message
|
|
3374
|
+
// The success message is appended to restoredMessages in handleChatPickerSelection
|
|
3375
|
+
// This ensures it's part of the same React setState call, avoiding race conditions
|
|
3376
|
+
quickLog(`[${new Date().toISOString()}] [Revert] Calling handleChatPickerSelection with revertSuccessMessage="${revertSuccessMessage.substring(0, 50)}..."\n`);
|
|
3377
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
3378
|
+
const preserveRemote = currentContext.type !== 'local' && typeof currentContext.handler?.isConnected === 'function'
|
|
3379
|
+
? currentContext.handler.isConnected()
|
|
3380
|
+
: currentContext.type !== 'local';
|
|
3381
|
+
await this.handleChatPickerSelection(this.currentChatId, true, revertSuccessMessage, preserveRemote);
|
|
3382
|
+
// Clear responseMessage so it doesn't get sent via onDirectMessageCallback
|
|
3383
|
+
// (the message was already included in the restored messages above)
|
|
3384
|
+
quickLog(`[${new Date().toISOString()}] [Revert] handleChatPickerSelection completed, clearing responseMessage\n`);
|
|
3385
|
+
responseMessage = '';
|
|
3386
|
+
}
|
|
3387
|
+
else {
|
|
3388
|
+
// Fallback: if no current chat, show the message via normal callback
|
|
3389
|
+
quickLog(`[${new Date().toISOString()}] [Revert] No currentChatId, using fallback responseMessage\n`);
|
|
3390
|
+
responseMessage = revertSuccessMessage;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
catch (error) {
|
|
3395
|
+
responseMessage = `❌ Failed to revert: ${error.message}`;
|
|
3396
|
+
}
|
|
3397
|
+
break;
|
|
3398
|
+
}
|
|
3078
3399
|
case 'add-command':
|
|
3079
3400
|
case 'add-command-auto-detect':
|
|
3080
3401
|
// Handle custom command auto-detect management
|
|
@@ -3769,12 +4090,32 @@ Create once, run many times across different machines.`;
|
|
|
3769
4090
|
}
|
|
3770
4091
|
return;
|
|
3771
4092
|
}
|
|
4093
|
+
// Check if Docker is nested inside SSH
|
|
4094
|
+
const parentContext = this.contextManager.getParentContext();
|
|
3772
4095
|
// Start remote task first to get callbacks
|
|
3773
4096
|
const remoteTask = BackgroundTaskManager.startRemoteTask(command, effectiveCwd, remoteContextDisplay || 'docker');
|
|
3774
4097
|
taskId = remoteTask.id;
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
4098
|
+
if (parentContext && parentContext.type === 'ssh') {
|
|
4099
|
+
// Nested Docker inside SSH: route docker exec command through SSH
|
|
4100
|
+
const sshClient = parentContext.handler?.client;
|
|
4101
|
+
if (!sshClient) {
|
|
4102
|
+
if (this.onResponseCallback) {
|
|
4103
|
+
this.onResponseCallback('❌ SSH client not available for nested Docker background task');
|
|
4104
|
+
}
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
// Build docker exec command to run via SSH
|
|
4108
|
+
const escapedCommand = command.replace(/"/g, '\\"');
|
|
4109
|
+
const dockerCommand = `docker exec -w "${effectiveCwd}" ${containerId} sh -c "${escapedCommand}"`;
|
|
4110
|
+
// Create SSH PTY to run the docker command
|
|
4111
|
+
const sshPty = runSSHCommand(sshClient, dockerCommand, parentContext.metadata.workingDirectory || '~', remoteTask.onData, remoteTask.onExit);
|
|
4112
|
+
remoteTask.setRemotePty(sshPty);
|
|
4113
|
+
}
|
|
4114
|
+
else {
|
|
4115
|
+
// Local Docker: use standard runDockerCommand
|
|
4116
|
+
const dockerPty = runDockerCommand(containerId, command, effectiveCwd, remoteTask.onData, remoteTask.onExit);
|
|
4117
|
+
remoteTask.setRemotePty(dockerPty);
|
|
4118
|
+
}
|
|
3778
4119
|
}
|
|
3779
4120
|
else {
|
|
3780
4121
|
// Unknown remote type - fall back to local
|
|
@@ -3860,28 +4201,57 @@ Create once, run many times across different machines.`;
|
|
|
3860
4201
|
// Capture remote context if in remote environment (SSH/WSL/Docker)
|
|
3861
4202
|
// Use null to explicitly clear remote context when not in remote (so resuming won't reconnect)
|
|
3862
4203
|
let remoteContext = null;
|
|
4204
|
+
let remoteContextStack = null;
|
|
3863
4205
|
const currentContext = this.contextManager.getCurrentContext();
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
4206
|
+
// Build remoteContextStack for nested sessions
|
|
4207
|
+
if (currentContext.type !== 'local' && this.connectionCommandStack.length > 0) {
|
|
4208
|
+
remoteContextStack = [];
|
|
4209
|
+
// Build stack of contexts from the connectionCommandStack and cwdStack
|
|
4210
|
+
// cwdStack[0] = local CWD before first remote
|
|
4211
|
+
// cwdStack[1] = first remote CWD before second remote (if nested)
|
|
4212
|
+
// connectionCommandStack[0] = first connection command
|
|
4213
|
+
// connectionCommandStack[1] = second connection command (if nested)
|
|
4214
|
+
for (let i = 0; i < this.connectionCommandStack.length; i++) {
|
|
4215
|
+
const connCmd = this.connectionCommandStack[i];
|
|
4216
|
+
// Detect the type from the command
|
|
4217
|
+
let ctxType = 'ssh';
|
|
4218
|
+
if (connCmd.startsWith('docker ') || connCmd.startsWith('docker-compose ')) {
|
|
4219
|
+
ctxType = 'docker';
|
|
3876
4220
|
}
|
|
3877
|
-
|
|
4221
|
+
else if (connCmd.startsWith('wsl')) {
|
|
4222
|
+
ctxType = 'wsl';
|
|
4223
|
+
}
|
|
4224
|
+
// Get the CWD before this remote connection
|
|
4225
|
+
const cwdBefore = i < this.cwdStack.length ? this.cwdStack[i] : process.cwd();
|
|
4226
|
+
// For the last (current) context, use the actual metadata
|
|
4227
|
+
const isLastContext = i === this.connectionCommandStack.length - 1;
|
|
4228
|
+
const remoteCwd = isLastContext ? currentContext.metadata.workingDirectory : (this.cwdStack[i + 1] || '~');
|
|
4229
|
+
const storedCtx = {
|
|
4230
|
+
type: ctxType,
|
|
4231
|
+
connectionCommand: connCmd,
|
|
4232
|
+
remoteCwd: remoteCwd,
|
|
4233
|
+
localCwdBeforeRemote: cwdBefore,
|
|
4234
|
+
metadata: isLastContext ? {
|
|
4235
|
+
hostname: currentContext.metadata.hostname,
|
|
4236
|
+
username: currentContext.metadata.username,
|
|
4237
|
+
distroName: currentContext.metadata.distroName,
|
|
4238
|
+
containerId: currentContext.metadata.containerId,
|
|
4239
|
+
port: currentContext.metadata.port,
|
|
4240
|
+
} : {}
|
|
4241
|
+
};
|
|
4242
|
+
remoteContextStack.push(storedCtx);
|
|
4243
|
+
}
|
|
4244
|
+
// For backward compatibility, also set remoteContext to the last (current) context
|
|
4245
|
+
if (remoteContextStack.length > 0) {
|
|
4246
|
+
remoteContext = remoteContextStack[remoteContextStack.length - 1];
|
|
4247
|
+
}
|
|
3878
4248
|
}
|
|
3879
4249
|
// Determine the local CWD to save (use base of cwdStack if in remote, otherwise current cwd)
|
|
3880
4250
|
const cwdToSave = currentContext.type !== 'local' && this.cwdStack.length > 0
|
|
3881
4251
|
? this.cwdStack[0]
|
|
3882
4252
|
: this.cwd;
|
|
3883
4253
|
try {
|
|
3884
|
-
localChatStorage.saveChat(this.currentChatId, storedMessages, storedUIMessages, cwdToSave, remoteContext);
|
|
4254
|
+
localChatStorage.saveChat(this.currentChatId, storedMessages, storedUIMessages, cwdToSave, remoteContext, remoteContextStack);
|
|
3885
4255
|
// Also store the backend conversation ID (UUID) for file deletion
|
|
3886
4256
|
// This is the ID used for GCS file storage, not the local chat ID
|
|
3887
4257
|
const backendId = conversationManager.getCurrentConversationId();
|
|
@@ -3896,7 +4266,7 @@ Create once, run many times across different machines.`;
|
|
|
3896
4266
|
/**
|
|
3897
4267
|
* Load a chat from local storage and restore it
|
|
3898
4268
|
*/
|
|
3899
|
-
loadChat(chatId) {
|
|
4269
|
+
loadChat(chatId, options) {
|
|
3900
4270
|
const chat = localChatStorage.loadChat(chatId);
|
|
3901
4271
|
if (!chat) {
|
|
3902
4272
|
return false;
|
|
@@ -3908,11 +4278,32 @@ Create once, run many times across different machines.`;
|
|
|
3908
4278
|
tool_calls: msg.tool_calls,
|
|
3909
4279
|
tool_call_id: msg.tool_call_id,
|
|
3910
4280
|
}));
|
|
4281
|
+
// IMPORTANT: Also restore UI message history from saved chat
|
|
4282
|
+
// This ensures that when the chat is saved again (e.g., after revert),
|
|
4283
|
+
// the correct UI messages are preserved
|
|
4284
|
+
if (chat.uiMessages && chat.uiMessages.length > 0) {
|
|
4285
|
+
this.uiMessageHistory = chat.uiMessages.map(msg => ({
|
|
4286
|
+
id: msg.id,
|
|
4287
|
+
role: msg.role,
|
|
4288
|
+
content: msg.content,
|
|
4289
|
+
timestamp: msg.timestamp ? new Date(msg.timestamp) : undefined,
|
|
4290
|
+
toolExecution: msg.toolExecution,
|
|
4291
|
+
shouldStream: false,
|
|
4292
|
+
isCommandMode: msg.isCommandMode,
|
|
4293
|
+
tool_call_id: msg.tool_call_id,
|
|
4294
|
+
tool_calls: msg.tool_calls,
|
|
4295
|
+
thinkingDuration: msg.thinkingDuration,
|
|
4296
|
+
taskCompletion: msg.taskCompletion,
|
|
4297
|
+
planAccepted: msg.planAccepted,
|
|
4298
|
+
connectionStatus: msg.connectionStatus,
|
|
4299
|
+
}));
|
|
4300
|
+
}
|
|
3911
4301
|
// Set the current chat ID to continue the conversation
|
|
3912
4302
|
this.currentChatId = chatId;
|
|
3913
4303
|
this.conversationStarted = true;
|
|
3914
4304
|
// Restore CWD if saved (important for commands to run in correct directory)
|
|
3915
|
-
|
|
4305
|
+
// Allow skipping when preserving an active remote session
|
|
4306
|
+
if (chat.cwd && !options?.preserveCwd) {
|
|
3916
4307
|
this.cwd = chat.cwd;
|
|
3917
4308
|
// Update context manager if available
|
|
3918
4309
|
if (this.contextManager) {
|
|
@@ -3944,8 +4335,12 @@ Create once, run many times across different machines.`;
|
|
|
3944
4335
|
}
|
|
3945
4336
|
/**
|
|
3946
4337
|
* Handle chat picker selection
|
|
4338
|
+
* @param chatId The ID of the chat to load
|
|
4339
|
+
* @param skipLoadedMessage If true, skip showing the "Loaded chat" message (used during revert to avoid React setState race condition)
|
|
4340
|
+
* @param revertSuccessMessage If provided, this message will be appended to restoredMessages before calling onRestoreMessagesCallback (used during revert to include success message in the same React setState call, avoiding race conditions)
|
|
4341
|
+
* @param preserveRemoteSession If true, keep current SSH/WSL/Docker connection and skip reconnect logic (used during revert in a live remote session)
|
|
3947
4342
|
*/
|
|
3948
|
-
async handleChatPickerSelection(chatId) {
|
|
4343
|
+
async handleChatPickerSelection(chatId, skipLoadedMessage = false, revertSuccessMessage, preserveRemoteSession = false) {
|
|
3949
4344
|
const chat = localChatStorage.loadChat(chatId);
|
|
3950
4345
|
if (!chat) {
|
|
3951
4346
|
if (this.onResponseCallback) {
|
|
@@ -3956,7 +4351,8 @@ Create once, run many times across different machines.`;
|
|
|
3956
4351
|
// IMPORTANT: Clean up current remote session before loading new chat
|
|
3957
4352
|
// This ensures that switching from a remote chat to a local chat properly resets the state
|
|
3958
4353
|
const currentContext = this.contextManager.getCurrentContext();
|
|
3959
|
-
|
|
4354
|
+
const shouldPreserveRemote = preserveRemoteSession && currentContext.type !== 'local';
|
|
4355
|
+
if (!shouldPreserveRemote && currentContext.type !== 'local') {
|
|
3960
4356
|
// Disconnect from current remote session
|
|
3961
4357
|
if (currentContext.handler) {
|
|
3962
4358
|
try {
|
|
@@ -3970,11 +4366,21 @@ Create once, run many times across different machines.`;
|
|
|
3970
4366
|
this.contextManager.popContext();
|
|
3971
4367
|
// Clear remote context tracking
|
|
3972
4368
|
this.cwdStack = [];
|
|
3973
|
-
this.
|
|
4369
|
+
this.connectionCommandStack = [];
|
|
3974
4370
|
}
|
|
3975
4371
|
// Load AI context
|
|
3976
|
-
this.loadChat(chatId);
|
|
4372
|
+
this.loadChat(chatId, shouldPreserveRemote ? { preserveCwd: true } : undefined);
|
|
4373
|
+
// If we're preserving a live remote session (revert case), restore the current remote CWD
|
|
4374
|
+
if (shouldPreserveRemote) {
|
|
4375
|
+
this.cwd = currentContext.metadata.workingDirectory;
|
|
4376
|
+
this.contextManager.updateWorkingDirectory(this.cwd);
|
|
4377
|
+
if (this.onCwdChange) {
|
|
4378
|
+
this.onCwdChange(this.cwd);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
3977
4381
|
// Restore UI messages if available
|
|
4382
|
+
quickLog(`[${new Date().toISOString()}] [handleChatPickerSelection] chat.uiMessages exists: ${!!chat.uiMessages}, length: ${chat.uiMessages?.length ?? 0}\n`);
|
|
4383
|
+
quickLog(`[${new Date().toISOString()}] [handleChatPickerSelection] onRestoreMessagesCallback exists: ${!!this.onRestoreMessagesCallback}\n`);
|
|
3978
4384
|
if (chat.uiMessages && chat.uiMessages.length > 0 && this.onRestoreMessagesCallback) {
|
|
3979
4385
|
// Convert StoredUIMessage back to Message format
|
|
3980
4386
|
const restoredMessages = chat.uiMessages.map(msg => ({
|
|
@@ -3992,31 +4398,88 @@ Create once, run many times across different machines.`;
|
|
|
3992
4398
|
planAccepted: msg.planAccepted,
|
|
3993
4399
|
connectionStatus: msg.connectionStatus, // For SSH/WSL/Docker connection status boxes
|
|
3994
4400
|
}));
|
|
4401
|
+
// If a revert success message was provided, append it to the restored messages
|
|
4402
|
+
// This ensures the message is part of the same React setState call, avoiding race conditions
|
|
4403
|
+
quickLog(`[${new Date().toISOString()}] [handleChatPickerSelection] revertSuccessMessage provided: ${!!revertSuccessMessage}\n`);
|
|
4404
|
+
if (revertSuccessMessage) {
|
|
4405
|
+
const revertSystemMessage = {
|
|
4406
|
+
id: `revert-success-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
4407
|
+
role: 'assistant',
|
|
4408
|
+
content: revertSuccessMessage,
|
|
4409
|
+
timestamp: new Date(),
|
|
4410
|
+
shouldStream: false,
|
|
4411
|
+
};
|
|
4412
|
+
restoredMessages.push(revertSystemMessage);
|
|
4413
|
+
quickLog(`[${new Date().toISOString()}] [handleChatPickerSelection] Appended revert success message, restoredMessages.length: ${restoredMessages.length}\n`);
|
|
4414
|
+
}
|
|
4415
|
+
quickLog(`[${new Date().toISOString()}] [handleChatPickerSelection] Calling onRestoreMessagesCallback with ${restoredMessages.length} messages\n`);
|
|
3995
4416
|
this.onRestoreMessagesCallback(restoredMessages);
|
|
4417
|
+
quickLog(`[${new Date().toISOString()}] [handleChatPickerSelection] onRestoreMessagesCallback completed\n`);
|
|
3996
4418
|
}
|
|
3997
4419
|
// Attempt to restore remote context if chat was saved while in remote environment
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
//
|
|
4420
|
+
// Support both remoteContextStack (for nested sessions) and single remoteContext (backward compat)
|
|
4421
|
+
const contextStackToRestore = shouldPreserveRemote
|
|
4422
|
+
? null
|
|
4423
|
+
: (chat.remoteContextStack ?? (chat.remoteContext ? [chat.remoteContext] : null));
|
|
4424
|
+
if (contextStackToRestore && contextStackToRestore.length > 0) {
|
|
4425
|
+
// Get the base local CWD from the first context's localCwdBeforeRemote
|
|
4426
|
+
const baseLocalCwd = contextStackToRestore[0].localCwdBeforeRemote;
|
|
4427
|
+
// Initialize stacks
|
|
4428
|
+
this.cwdStack = [baseLocalCwd];
|
|
4429
|
+
this.connectionCommandStack = [];
|
|
4430
|
+
// Show initial reconnection notification
|
|
4431
|
+
const nestingInfo = contextStackToRestore.length > 1
|
|
4432
|
+
? ` (${contextStackToRestore.length} levels: ${contextStackToRestore.map(c => c.type).join(' > ')})`
|
|
4433
|
+
: '';
|
|
4004
4434
|
if (this.onDirectMessageCallback) {
|
|
4005
|
-
this.onDirectMessageCallback(`🔄 Reconnecting to ${
|
|
4006
|
-
}
|
|
4007
|
-
// Show connecting status
|
|
4008
|
-
if (this.onConnectionStatusUpdate) {
|
|
4009
|
-
this.onConnectionStatusUpdate({
|
|
4010
|
-
type: type,
|
|
4011
|
-
status: 'connecting',
|
|
4012
|
-
connectionString: this.buildConnectionString(type, chat.remoteContext.metadata)
|
|
4013
|
-
});
|
|
4435
|
+
this.onDirectMessageCallback(`🔄 Reconnecting to session${nestingInfo}...`);
|
|
4014
4436
|
}
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4437
|
+
// Sequential reconnection through the stack
|
|
4438
|
+
let previousCwd = baseLocalCwd;
|
|
4439
|
+
for (let i = 0; i < contextStackToRestore.length; i++) {
|
|
4440
|
+
const remoteCtx = contextStackToRestore[i];
|
|
4441
|
+
const { type, connectionCommand, remoteCwd } = remoteCtx;
|
|
4442
|
+
const levelInfo = contextStackToRestore.length > 1 ? ` [${i + 1}/${contextStackToRestore.length}]` : '';
|
|
4443
|
+
// Show connecting status for this level
|
|
4444
|
+
if (this.onDirectMessageCallback) {
|
|
4445
|
+
this.onDirectMessageCallback(`🔄${levelInfo} Connecting to ${type.toUpperCase()}...`);
|
|
4446
|
+
}
|
|
4447
|
+
if (this.onConnectionStatusUpdate) {
|
|
4448
|
+
this.onConnectionStatusUpdate({
|
|
4449
|
+
type: type,
|
|
4450
|
+
status: 'connecting',
|
|
4451
|
+
connectionString: this.buildConnectionString(type, remoteCtx.metadata)
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4454
|
+
try {
|
|
4455
|
+
// Detect and connect using the saved command
|
|
4456
|
+
const detection = this.commandDetector.detect(connectionCommand);
|
|
4457
|
+
if (!detection) {
|
|
4458
|
+
throw new Error(`Could not detect handler for: ${connectionCommand}`);
|
|
4459
|
+
}
|
|
4460
|
+
// Create a new instance of the handler to ensure each level of the connection
|
|
4461
|
+
// has its own state (client, stream, etc.), critical for nested sessions of the same type
|
|
4462
|
+
const handler = detection.handler.createNew();
|
|
4463
|
+
let context;
|
|
4464
|
+
// Check if this is a nested connection (i > 0 means we're inside a remote session)
|
|
4465
|
+
if (i > 0) {
|
|
4466
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
4467
|
+
if (handler.connectFromRemote) {
|
|
4468
|
+
context = await handler.connectFromRemote(connectionCommand, previousCwd, currentContext);
|
|
4469
|
+
}
|
|
4470
|
+
else {
|
|
4471
|
+
throw new Error(`Nested connections not supported by ${type} handler`);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
else {
|
|
4475
|
+
// First level: connect from local
|
|
4476
|
+
context = await handler.connect(connectionCommand, previousCwd);
|
|
4477
|
+
}
|
|
4478
|
+
// Push to stacks
|
|
4479
|
+
this.connectionCommandStack.push(connectionCommand);
|
|
4480
|
+
if (i > 0) {
|
|
4481
|
+
this.cwdStack.push(previousCwd); // Save the previous remote CWD for nested exits
|
|
4482
|
+
}
|
|
4020
4483
|
this.contextManager.pushContext(context);
|
|
4021
4484
|
// Navigate to saved remote CWD
|
|
4022
4485
|
if (remoteCwd && remoteCwd !== context.metadata.workingDirectory) {
|
|
@@ -4026,42 +4489,64 @@ Create once, run many times across different machines.`;
|
|
|
4026
4489
|
catch (cdError) {
|
|
4027
4490
|
// Failed to cd to saved path - warn but continue
|
|
4028
4491
|
if (this.onDirectMessageCallback) {
|
|
4029
|
-
this.onDirectMessageCallback(`⚠️ Could not restore
|
|
4492
|
+
this.onDirectMessageCallback(`⚠️ Could not restore ${type} directory: ${remoteCwd}`);
|
|
4030
4493
|
}
|
|
4031
4494
|
}
|
|
4032
4495
|
}
|
|
4033
|
-
//
|
|
4496
|
+
// Update previousCwd for next iteration
|
|
4497
|
+
previousCwd = this.contextManager.getCurrentContext().metadata.workingDirectory;
|
|
4498
|
+
// Show success for this level
|
|
4034
4499
|
if (this.onConnectionStatusUpdate) {
|
|
4035
4500
|
this.onConnectionStatusUpdate({
|
|
4036
4501
|
type: type,
|
|
4037
4502
|
status: 'connected',
|
|
4038
|
-
connectionString: this.buildConnectionString(type,
|
|
4503
|
+
connectionString: this.buildConnectionString(type, remoteCtx.metadata)
|
|
4039
4504
|
});
|
|
4040
4505
|
}
|
|
4506
|
+
}
|
|
4507
|
+
catch (error) {
|
|
4508
|
+
// Connection failed at this level - fall back to local mode
|
|
4509
|
+
// If we partially connected, disconnect all and reset
|
|
4510
|
+
while (this.contextManager.getCurrentContext().type !== 'local') {
|
|
4511
|
+
const ctx = this.contextManager.getCurrentContext();
|
|
4512
|
+
if (ctx.handler) {
|
|
4513
|
+
try {
|
|
4514
|
+
await ctx.handler.disconnect();
|
|
4515
|
+
}
|
|
4516
|
+
catch (e) { /* ignore */ }
|
|
4517
|
+
}
|
|
4518
|
+
this.contextManager.popContext();
|
|
4519
|
+
}
|
|
4520
|
+
this.cwdStack = [];
|
|
4521
|
+
this.connectionCommandStack = [];
|
|
4522
|
+
if (this.onConnectionStatusUpdate) {
|
|
4523
|
+
this.onConnectionStatusUpdate({
|
|
4524
|
+
type: type,
|
|
4525
|
+
status: 'error',
|
|
4526
|
+
connectionString: this.buildConnectionString(type, remoteCtx.metadata),
|
|
4527
|
+
error: error.message
|
|
4528
|
+
});
|
|
4529
|
+
}
|
|
4530
|
+
const failedAt = contextStackToRestore.length > 1 ? ` at level ${i + 1} (${type})` : '';
|
|
4531
|
+
if (this.onDirectMessageCallback) {
|
|
4532
|
+
this.onDirectMessageCallback(`⚠️ Loaded chat: "${chat.title}"\n\n❌ Could not reconnect${failedAt}: ${error.message}\n\n📁 Restored to local directory: ${baseLocalCwd}`);
|
|
4533
|
+
}
|
|
4041
4534
|
return;
|
|
4042
4535
|
}
|
|
4043
4536
|
}
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
status: 'error',
|
|
4052
|
-
connectionString: this.buildConnectionString(type, chat.remoteContext.metadata),
|
|
4053
|
-
error: error.message
|
|
4054
|
-
});
|
|
4055
|
-
}
|
|
4056
|
-
if (this.onDirectMessageCallback) {
|
|
4057
|
-
this.onDirectMessageCallback(`⚠️ Loaded chat: "${chat.title}"\n\n❌ Could not reconnect to ${type.toUpperCase()}: ${error.message}\n\n📁 Restored to local directory: ${localCwdBeforeRemote}`);
|
|
4537
|
+
// All levels connected successfully
|
|
4538
|
+
if (this.onDirectMessageCallback) {
|
|
4539
|
+
const successInfo = contextStackToRestore.length > 1
|
|
4540
|
+
? `🔗 Reconnected through ${contextStackToRestore.length} levels`
|
|
4541
|
+
: '';
|
|
4542
|
+
if (successInfo) {
|
|
4543
|
+
this.onDirectMessageCallback(successInfo);
|
|
4058
4544
|
}
|
|
4059
|
-
return;
|
|
4060
4545
|
}
|
|
4546
|
+
return;
|
|
4061
4547
|
}
|
|
4062
|
-
// No remote context - show regular confirmation
|
|
4063
4548
|
// No remote context - show regular confirmation and restore CWD
|
|
4064
|
-
if (chat.cwd && !chat.remoteContext) {
|
|
4549
|
+
if (!shouldPreserveRemote && chat.cwd && !chat.remoteContext) {
|
|
4065
4550
|
if (fs.existsSync(chat.cwd)) {
|
|
4066
4551
|
this.cwd = chat.cwd;
|
|
4067
4552
|
this.contextManager.updateWorkingDirectory(chat.cwd);
|
|
@@ -4070,7 +4555,8 @@ Create once, run many times across different machines.`;
|
|
|
4070
4555
|
}
|
|
4071
4556
|
}
|
|
4072
4557
|
}
|
|
4073
|
-
if (
|
|
4558
|
+
// Skip loaded message if called from revert (to avoid React setState race condition)
|
|
4559
|
+
if (this.onDirectMessageCallback && !skipLoadedMessage) {
|
|
4074
4560
|
const responseMessage = `✅ Loaded chat: "${chat.title}"\n\nYou have ${chat.messageCount} messages in AI context. Continue your conversation!`;
|
|
4075
4561
|
this.onDirectMessageCallback(responseMessage);
|
|
4076
4562
|
}
|
|
@@ -4102,12 +4588,19 @@ Create once, run many times across different machines.`;
|
|
|
4102
4588
|
* Start a new chat session
|
|
4103
4589
|
*/
|
|
4104
4590
|
startNewChat() {
|
|
4591
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
4105
4592
|
this.conversationHistory = [];
|
|
4106
4593
|
this.currentChatId = null;
|
|
4107
4594
|
this.conversationStarted = false;
|
|
4108
4595
|
this.uiMessageHistory = [];
|
|
4109
|
-
|
|
4110
|
-
|
|
4596
|
+
// Only clear stacks if we're in local context
|
|
4597
|
+
// If we're in a remote context (SSH/WSL/Docker), we want to PRESERVE the connection state
|
|
4598
|
+
// so the new chat continues in the same environment
|
|
4599
|
+
if (currentContext.type === 'local') {
|
|
4600
|
+
this.cwdStack = [];
|
|
4601
|
+
this.connectionCommandStack = [];
|
|
4602
|
+
}
|
|
4603
|
+
// Else: Preserve cwdStack and connectionCommandStack for remote sessions
|
|
4111
4604
|
// Reset context limit state
|
|
4112
4605
|
if (this.contextLimitReached) {
|
|
4113
4606
|
this.contextLimitReached = false;
|
|
@@ -4148,6 +4641,12 @@ Create once, run many times across different machines.`;
|
|
|
4148
4641
|
setOnRestoreMessagesCallback(callback) {
|
|
4149
4642
|
this.onRestoreMessagesCallback = callback;
|
|
4150
4643
|
}
|
|
4644
|
+
/**
|
|
4645
|
+
* Set callback for setting input value (e.g., for revert)
|
|
4646
|
+
*/
|
|
4647
|
+
setOnSetInput(callback) {
|
|
4648
|
+
this.onSetInputCallback = callback;
|
|
4649
|
+
}
|
|
4151
4650
|
/**
|
|
4152
4651
|
* Get environment context for backend
|
|
4153
4652
|
* Returns structured environment information to be sent to backend
|
|
@@ -4270,7 +4769,8 @@ Once the user approves the plan:
|
|
|
4270
4769
|
// Remote context (SSH, WSL, Docker) - pass remote context to App
|
|
4271
4770
|
// Use the remote context's working directory, not the local Windows CWD
|
|
4272
4771
|
const remoteCwd = currentContextForEditor.metadata?.workingDirectory || '~';
|
|
4273
|
-
this.
|
|
4772
|
+
const parentContext = this.contextManager.getParentContext() || undefined; // Convert null to undefined
|
|
4773
|
+
this.onInteractiveEditorMode(true, command, remoteCwd, currentContextForEditor, parentContext);
|
|
4274
4774
|
}
|
|
4275
4775
|
return;
|
|
4276
4776
|
}
|
|
@@ -4307,7 +4807,7 @@ Once the user approves the plan:
|
|
|
4307
4807
|
}
|
|
4308
4808
|
if (newContext.type === 'local') {
|
|
4309
4809
|
// Clear tracking when back to local
|
|
4310
|
-
this.
|
|
4810
|
+
this.connectionCommandStack.pop();
|
|
4311
4811
|
}
|
|
4312
4812
|
// Save chat - include remote context info if still in remote
|
|
4313
4813
|
this.saveCurrentChat();
|
|
@@ -4331,12 +4831,18 @@ Once the user approves the plan:
|
|
|
4331
4831
|
}
|
|
4332
4832
|
*/
|
|
4333
4833
|
// Special handling for cd command - change the actual working directory
|
|
4334
|
-
|
|
4834
|
+
// Check for chained commands: cd path && cmd or cd path ; cmd
|
|
4835
|
+
const chainedCdMatch = command.match(/^cd\s+([^;&]+?)\s*(&&|;)\s*(.+)$/);
|
|
4836
|
+
const simpleCdMatch = command.match(/^cd\s+(.+)$/);
|
|
4837
|
+
// Determine if this starts with cd
|
|
4838
|
+
const cdMatch = chainedCdMatch || simpleCdMatch;
|
|
4335
4839
|
if (cdMatch) {
|
|
4336
4840
|
const currentContext = this.contextManager.getCurrentContext();
|
|
4841
|
+
const targetDir = (chainedCdMatch ? chainedCdMatch[1] : cdMatch[1]).trim();
|
|
4842
|
+
const hasChainedCommand = !!chainedCdMatch;
|
|
4843
|
+
const chainedCommand = chainedCdMatch ? chainedCdMatch[3] : null;
|
|
4337
4844
|
if (currentContext.type === 'local') {
|
|
4338
4845
|
// Local cd handling
|
|
4339
|
-
const targetDir = cdMatch[1].trim();
|
|
4340
4846
|
const newCwd = path.resolve(this.cwd, targetDir);
|
|
4341
4847
|
if (!fs.existsSync(newCwd)) {
|
|
4342
4848
|
if (this.onResponseCallback) {
|
|
@@ -4350,19 +4856,31 @@ Once the user approves the plan:
|
|
|
4350
4856
|
}
|
|
4351
4857
|
return;
|
|
4352
4858
|
}
|
|
4859
|
+
this.cwd = newCwd;
|
|
4353
4860
|
this.contextManager.updateWorkingDirectory(newCwd);
|
|
4354
4861
|
if (this.onResponseCallback) {
|
|
4355
4862
|
this.onResponseCallback(`Changed directory to: ${newCwd}`);
|
|
4356
4863
|
}
|
|
4864
|
+
// If there's a chained command, execute it in the new directory
|
|
4865
|
+
if (hasChainedCommand && chainedCommand) {
|
|
4866
|
+
// Recursively handle the next command
|
|
4867
|
+
await this.handleCommandModeExecution(chainedCommand);
|
|
4868
|
+
}
|
|
4357
4869
|
return;
|
|
4358
4870
|
}
|
|
4359
4871
|
else {
|
|
4360
|
-
// Subshell cd handling - execute via handler
|
|
4361
|
-
const
|
|
4872
|
+
// Subshell cd handling - execute just the cd command via handler
|
|
4873
|
+
const cdOnlyCommand = `cd ${targetDir}`;
|
|
4874
|
+
const result = await this.contextManager.executeCommand(cdOnlyCommand);
|
|
4362
4875
|
if (result.exitCode === 0) {
|
|
4363
4876
|
if (this.onResponseCallback) {
|
|
4364
4877
|
this.onResponseCallback(`Changed directory to: ${currentContext.metadata.workingDirectory}`);
|
|
4365
4878
|
}
|
|
4879
|
+
// If there's a chained command, execute it in the new directory
|
|
4880
|
+
if (hasChainedCommand && chainedCommand) {
|
|
4881
|
+
// Recursively handle the next command
|
|
4882
|
+
await this.handleCommandModeExecution(chainedCommand);
|
|
4883
|
+
}
|
|
4366
4884
|
}
|
|
4367
4885
|
else {
|
|
4368
4886
|
if (this.onResponseCallback) {
|
|
@@ -4492,57 +5010,118 @@ Once the user approves the plan:
|
|
|
4492
5010
|
});
|
|
4493
5011
|
}
|
|
4494
5012
|
else if (currentContext.type === 'docker') {
|
|
4495
|
-
// Docker execution
|
|
5013
|
+
// Docker execution - check if nested inside SSH
|
|
5014
|
+
const parentContext = this.contextManager.getParentContext();
|
|
4496
5015
|
const remoteCwd = currentContext.metadata?.workingDirectory || '~';
|
|
4497
5016
|
const containerId = currentContext.metadata?.containerId || '';
|
|
4498
5017
|
let output = '';
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
error: `Exit Code: ${exitCode}`,
|
|
4515
|
-
arguments: { command, cwd: remoteCwd, remoteContext }
|
|
4516
|
-
});
|
|
5018
|
+
if (parentContext && parentContext.type === 'ssh') {
|
|
5019
|
+
// Nested Docker inside SSH: route docker exec command through SSH
|
|
5020
|
+
const sshClient = parentContext.handler?.client;
|
|
5021
|
+
if (!sshClient) {
|
|
5022
|
+
throw new Error('SSH client not available for nested Docker session');
|
|
5023
|
+
}
|
|
5024
|
+
// Build docker exec command to run via SSH
|
|
5025
|
+
const escapedCommand = command.replace(/"/g, '\\"');
|
|
5026
|
+
const dockerCommand = `docker exec -w "${remoteCwd}" ${containerId} sh -c "${escapedCommand}"`;
|
|
5027
|
+
await new Promise((resolve) => {
|
|
5028
|
+
const sshPty = runSSHCommand(sshClient, dockerCommand, parentContext.metadata.workingDirectory || '~', (data) => {
|
|
5029
|
+
// Stream output to UI
|
|
5030
|
+
output += data;
|
|
5031
|
+
if (this.onToolStreamingOutput) {
|
|
5032
|
+
this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
|
|
4517
5033
|
}
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
5034
|
+
}, (exitCode) => {
|
|
5035
|
+
// Notify UI of completion
|
|
5036
|
+
if (this.onToolExecutionUpdate) {
|
|
5037
|
+
if (exitCode !== 0) {
|
|
5038
|
+
this.onToolExecutionUpdate({
|
|
5039
|
+
toolName: 'execute_command',
|
|
5040
|
+
status: 'error',
|
|
5041
|
+
result: output,
|
|
5042
|
+
error: `Exit Code: ${exitCode}`,
|
|
5043
|
+
arguments: { command, cwd: remoteCwd, remoteContext }
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
else {
|
|
5047
|
+
this.onToolExecutionUpdate({
|
|
5048
|
+
toolName: 'execute_command',
|
|
5049
|
+
status: 'completed',
|
|
5050
|
+
result: output || 'Command executed successfully',
|
|
5051
|
+
arguments: { command, cwd: remoteCwd, remoteContext }
|
|
5052
|
+
});
|
|
5053
|
+
}
|
|
4525
5054
|
}
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
5055
|
+
// Record shell command to AI conversation history
|
|
5056
|
+
this.recordShellCommandToHistory(command, output, remoteCwd, exitCode);
|
|
5057
|
+
this.currentInteractiveProcess = undefined;
|
|
5058
|
+
resolve();
|
|
5059
|
+
});
|
|
5060
|
+
// Set up interactive process for stdin
|
|
5061
|
+
this.currentInteractiveProcess = {
|
|
5062
|
+
process: null,
|
|
5063
|
+
write: (data) => sshPty.write(data),
|
|
5064
|
+
kill: () => sshPty.kill(),
|
|
5065
|
+
signal: (sig) => {
|
|
5066
|
+
if (sig === 'SIGINT') {
|
|
5067
|
+
sshPty.write('\x03'); // Ctrl+C
|
|
5068
|
+
}
|
|
5069
|
+
},
|
|
5070
|
+
resize: (cols, rows) => sshPty.resize(cols, rows),
|
|
5071
|
+
isPty: true
|
|
5072
|
+
};
|
|
4531
5073
|
});
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
5074
|
+
}
|
|
5075
|
+
else {
|
|
5076
|
+
// Local Docker: use standard runDockerCommand
|
|
5077
|
+
await new Promise((resolve) => {
|
|
5078
|
+
const dockerPty = runDockerCommand(containerId, command, remoteCwd, (data) => {
|
|
5079
|
+
// Stream output to UI
|
|
5080
|
+
output += data;
|
|
5081
|
+
if (this.onToolStreamingOutput) {
|
|
5082
|
+
this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
|
|
4540
5083
|
}
|
|
4541
|
-
},
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
5084
|
+
}, (exitCode) => {
|
|
5085
|
+
// Notify UI of completion
|
|
5086
|
+
if (this.onToolExecutionUpdate) {
|
|
5087
|
+
if (exitCode !== 0) {
|
|
5088
|
+
this.onToolExecutionUpdate({
|
|
5089
|
+
toolName: 'execute_command',
|
|
5090
|
+
status: 'error',
|
|
5091
|
+
result: output,
|
|
5092
|
+
error: `Exit Code: ${exitCode}`,
|
|
5093
|
+
arguments: { command, cwd: remoteCwd, remoteContext }
|
|
5094
|
+
});
|
|
5095
|
+
}
|
|
5096
|
+
else {
|
|
5097
|
+
this.onToolExecutionUpdate({
|
|
5098
|
+
toolName: 'execute_command',
|
|
5099
|
+
status: 'completed',
|
|
5100
|
+
result: output || 'Command executed successfully',
|
|
5101
|
+
arguments: { command, cwd: remoteCwd, remoteContext }
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
// Record shell command to AI conversation history
|
|
5106
|
+
this.recordShellCommandToHistory(command, output, remoteCwd, exitCode);
|
|
5107
|
+
this.currentInteractiveProcess = undefined;
|
|
5108
|
+
resolve();
|
|
5109
|
+
});
|
|
5110
|
+
// Set up interactive process for stdin
|
|
5111
|
+
this.currentInteractiveProcess = {
|
|
5112
|
+
process: null,
|
|
5113
|
+
write: (data) => dockerPty.write(data),
|
|
5114
|
+
kill: () => dockerPty.kill(),
|
|
5115
|
+
signal: (sig) => {
|
|
5116
|
+
if (sig === 'SIGINT') {
|
|
5117
|
+
dockerPty.write('\x03'); // Ctrl+C
|
|
5118
|
+
}
|
|
5119
|
+
},
|
|
5120
|
+
resize: (cols, rows) => dockerPty.resize(cols, rows),
|
|
5121
|
+
isPty: true
|
|
5122
|
+
};
|
|
5123
|
+
});
|
|
5124
|
+
}
|
|
4546
5125
|
}
|
|
4547
5126
|
else if (currentContext.type === 'ssh') {
|
|
4548
5127
|
// SSH execution with PTY for proper TTY handling
|