centaurus-cli 2.9.3 → 2.9.4

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 (92) hide show
  1. package/dist/cli-adapter.d.ts +72 -8
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +607 -141
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/commands/CommandParser.d.ts +1 -1
  6. package/dist/commands/CommandParser.d.ts.map +1 -1
  7. package/dist/commands/CommandParser.js +113 -0
  8. package/dist/commands/CommandParser.js.map +1 -1
  9. package/dist/config/slash-commands.d.ts +2 -0
  10. package/dist/config/slash-commands.d.ts.map +1 -1
  11. package/dist/config/slash-commands.js +28 -0
  12. package/dist/config/slash-commands.js.map +1 -1
  13. package/dist/context/context-manager.d.ts +1 -1
  14. package/dist/context/context-manager.d.ts.map +1 -1
  15. package/dist/context/context-manager.js +3 -1
  16. package/dist/context/context-manager.js.map +1 -1
  17. package/dist/context/handlers/docker-handler.d.ts +9 -0
  18. package/dist/context/handlers/docker-handler.d.ts.map +1 -1
  19. package/dist/context/handlers/docker-handler.js +99 -10
  20. package/dist/context/handlers/docker-handler.js.map +1 -1
  21. package/dist/context/handlers/ssh-handler.d.ts +20 -0
  22. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  23. package/dist/context/handlers/ssh-handler.js +129 -1
  24. package/dist/context/handlers/ssh-handler.js.map +1 -1
  25. package/dist/context/subshell-handler.d.ts +15 -0
  26. package/dist/context/subshell-handler.d.ts.map +1 -1
  27. package/dist/index.js +10 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/services/ai-service-client.d.ts.map +1 -1
  30. package/dist/services/ai-service-client.js +33 -11
  31. package/dist/services/ai-service-client.js.map +1 -1
  32. package/dist/services/api-client.js +1 -1
  33. package/dist/services/api-client.js.map +1 -1
  34. package/dist/services/warpify-detector.d.ts +43 -0
  35. package/dist/services/warpify-detector.d.ts.map +1 -0
  36. package/dist/services/warpify-detector.js +203 -0
  37. package/dist/services/warpify-detector.js.map +1 -0
  38. package/dist/services/workflow-storage.d.ts +72 -0
  39. package/dist/services/workflow-storage.d.ts.map +1 -0
  40. package/dist/services/workflow-storage.js +239 -0
  41. package/dist/services/workflow-storage.js.map +1 -0
  42. package/dist/tools/command.d.ts.map +1 -1
  43. package/dist/tools/command.js +14 -0
  44. package/dist/tools/command.js.map +1 -1
  45. package/dist/tools/enter-remote-session.d.ts +13 -0
  46. package/dist/tools/enter-remote-session.d.ts.map +1 -0
  47. package/dist/tools/enter-remote-session.js +226 -0
  48. package/dist/tools/enter-remote-session.js.map +1 -0
  49. package/dist/tools/find-files.d.ts.map +1 -1
  50. package/dist/tools/find-files.js +9 -2
  51. package/dist/tools/find-files.js.map +1 -1
  52. package/dist/tools/grep-search.d.ts +104 -31
  53. package/dist/tools/grep-search.d.ts.map +1 -1
  54. package/dist/tools/grep-search.js +699 -430
  55. package/dist/tools/grep-search.js.map +1 -1
  56. package/dist/tools/workflow-tool.d.ts +11 -0
  57. package/dist/tools/workflow-tool.d.ts.map +1 -0
  58. package/dist/tools/workflow-tool.js +87 -0
  59. package/dist/tools/workflow-tool.js.map +1 -0
  60. package/dist/types/workflow.d.ts +110 -0
  61. package/dist/types/workflow.d.ts.map +1 -0
  62. package/dist/types/workflow.js +8 -0
  63. package/dist/types/workflow.js.map +1 -0
  64. package/dist/ui/components/App.d.ts +10 -1
  65. package/dist/ui/components/App.d.ts.map +1 -1
  66. package/dist/ui/components/App.js +117 -4
  67. package/dist/ui/components/App.js.map +1 -1
  68. package/dist/ui/components/Breadcrumbs.d.ts +4 -3
  69. package/dist/ui/components/Breadcrumbs.d.ts.map +1 -1
  70. package/dist/ui/components/Breadcrumbs.js +60 -54
  71. package/dist/ui/components/Breadcrumbs.js.map +1 -1
  72. package/dist/ui/components/ConnectionStatusMessage.js +2 -2
  73. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  74. package/dist/ui/components/InputBox.d.ts +1 -0
  75. package/dist/ui/components/InputBox.d.ts.map +1 -1
  76. package/dist/ui/components/InputBox.js +168 -2
  77. package/dist/ui/components/InputBox.js.map +1 -1
  78. package/dist/ui/components/InteractiveShell.d.ts +2 -0
  79. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  80. package/dist/ui/components/InteractiveShell.js +13 -3
  81. package/dist/ui/components/InteractiveShell.js.map +1 -1
  82. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  83. package/dist/ui/components/ToolExecutionMessage.js +164 -25
  84. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  85. package/dist/ui/components/WorkflowCreatorScreen.d.ts +25 -0
  86. package/dist/ui/components/WorkflowCreatorScreen.d.ts.map +1 -0
  87. package/dist/ui/components/WorkflowCreatorScreen.js +164 -0
  88. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -0
  89. package/dist/utils/input-classifier.d.ts.map +1 -1
  90. package/dist/utils/input-classifier.js +2 -1
  91. package/dist/utils/input-classifier.js.map +1 -1
  92. package/package.json +1 -1
@@ -20,6 +20,8 @@ import { readBinaryFileTool } from './tools/read-binary-file.js';
20
20
  import { createImageTool } from './tools/create-image.js';
21
21
  import { backgroundCommandTool } from './tools/background-command.js';
22
22
  import { subAgentTool } from './tools/sub-agent.js';
23
+ import { enterRemoteSessionTool } from './tools/enter-remote-session.js';
24
+ import { workflowTool } from './tools/workflow-tool.js';
23
25
  import { SubAgentManager } from './services/sub-agent-manager.js';
24
26
  import { ShellInputAgent } from './services/shell-input-agent.js';
25
27
  import { apiClient } from './services/api-client.js';
@@ -43,6 +45,7 @@ import { logWarning } from './utils/logger.js';
43
45
  import { BackgroundTaskManager } from './services/background-task-manager.js';
44
46
  import { sessionQuotaManager } from './services/session-quota-manager.js';
45
47
  import { ollamaService, OllamaService } from './services/ollama-service.js';
48
+ import { workflowStorage } from './services/workflow-storage.js';
46
49
  export class CentaurusCLI {
47
50
  configManager;
48
51
  toolRegistry;
@@ -90,7 +93,7 @@ export class CentaurusCLI {
90
93
  onShowChatRenamePickerCallback;
91
94
  onRestoreMessagesCallback;
92
95
  uiMessageHistory = []; // Mirror of App.tsx's messageHistory for saving
93
- localCwdBeforeRemote = null; // Track local CWD before entering remote session
96
+ cwdStack = []; // Stack of CWDs for nested sessions (pushed when entering, popped when exiting)
94
97
  lastConnectionCommand = null; // Track the command used to connect to remote
95
98
  onBackgroundModeChange;
96
99
  onBackgroundTaskCountChange;
@@ -111,6 +114,11 @@ export class CentaurusCLI {
111
114
  onShowMCPListScreen;
112
115
  onSubAgentCountChange; // Callback for sub-agent count changes
113
116
  onPromptAnswered; // Callback when AI answers a shell prompt
117
+ onShowWorkflowCreatorCallback; // Callback to show workflow creator screen with optional initial steps
118
+ onWorkflowSaveCallback; // Callback when workflow is saved
119
+ // Workflow learning mode state
120
+ workflowLearningActive = false;
121
+ learnedWorkflowSteps = [];
114
122
  constructor() {
115
123
  this.configManager = new ConfigManager();
116
124
  this.toolRegistry = new ToolRegistry();
@@ -120,13 +128,14 @@ export class CentaurusCLI {
120
128
  this.commandDetector = new CommandDetector();
121
129
  this.aiContextInjector = new AIContextInjector();
122
130
  // Register context change callback to update cwd
123
- this.contextManager.onContextChange((context) => {
131
+ // Register context change callback to update cwd
132
+ this.contextManager.onContextChange((context, stack) => {
124
133
  this.cwd = context.metadata.workingDirectory;
125
134
  if (this.onCwdChange) {
126
135
  this.onCwdChange(this.cwd);
127
136
  }
128
137
  if (this.onSubshellContextChange) {
129
- this.onSubshellContextChange(context);
138
+ this.onSubshellContextChange(context, stack);
130
139
  }
131
140
  });
132
141
  // Initialize MCP
@@ -218,6 +227,355 @@ export class CentaurusCLI {
218
227
  // Wire this callback to ShellInputAgent
219
228
  ShellInputAgent.setOnPromptAnswered(callback);
220
229
  }
230
+ setOnShowWorkflowCreator(callback) {
231
+ this.onShowWorkflowCreatorCallback = callback;
232
+ }
233
+ setOnWorkflowSave(callback) {
234
+ this.onWorkflowSaveCallback = callback;
235
+ }
236
+ /**
237
+ * Save a workflow from the workflow creator UI
238
+ */
239
+ saveWorkflow(name, steps, description) {
240
+ const workflow = workflowStorage.createWorkflow(name, steps, description);
241
+ workflowStorage.save(workflow);
242
+ if (this.onWorkflowSaveCallback) {
243
+ this.onWorkflowSaveCallback(workflow);
244
+ }
245
+ }
246
+ /**
247
+ * Toggle workflow learning mode.
248
+ * First call: Start learning and return a message to display.
249
+ * Second call: Stop learning and show workflow creator with learned steps.
250
+ * @returns Message to display to user, or null if showing workflow creator
251
+ */
252
+ toggleWorkflowLearning() {
253
+ if (this.workflowLearningActive) {
254
+ // Stop learning mode
255
+ this.workflowLearningActive = false;
256
+ const steps = [...this.learnedWorkflowSteps];
257
+ this.learnedWorkflowSteps = []; // Clear for next learning session
258
+ if (steps.length === 0) {
259
+ return '⚠️ No steps were recorded. Learning mode cancelled.';
260
+ }
261
+ // Show workflow creator with the learned steps
262
+ if (this.onShowWorkflowCreatorCallback) {
263
+ this.onShowWorkflowCreatorCallback(steps);
264
+ return null; // UI will handle the screen change
265
+ }
266
+ else {
267
+ return '❌ Workflow creator not available. Please update the CLI.';
268
+ }
269
+ }
270
+ else {
271
+ // Start learning mode
272
+ this.workflowLearningActive = true;
273
+ this.learnedWorkflowSteps = [];
274
+ return `📚 **Learning mode started!**
275
+
276
+ Commands and prompts from here will be recorded to create your workflow.
277
+
278
+ **Instructions:**
279
+ • Run commands normally - they'll be saved as command steps
280
+ • Type AI prompts - they'll be saved as instruction steps
281
+ • Run \`/workflow new learn-workflow\` again when you're done
282
+
283
+ Recording will begin with your next input.`;
284
+ }
285
+ }
286
+ /**
287
+ * Check if workflow learning is currently active
288
+ */
289
+ isWorkflowLearningActive() {
290
+ return this.workflowLearningActive;
291
+ }
292
+ /**
293
+ * Record a step during workflow learning mode
294
+ * @param type Type of step (command or instruction)
295
+ * @param content The command or instruction content
296
+ */
297
+ recordWorkflowStep(type, content) {
298
+ if (this.workflowLearningActive && content.trim()) {
299
+ this.learnedWorkflowSteps.push({ type, content: content.trim() });
300
+ quickLog(`[${new Date().toISOString()}] [WorkflowLearning] Recorded ${type}: ${content.trim()}\n`);
301
+ }
302
+ }
303
+ /**
304
+ * Cancel workflow learning mode with a message
305
+ * Used when conflicting commands are run during learning (e.g., /exit, /clear, /workflow run)
306
+ * @param reason The reason for cancellation
307
+ * @returns Message to display to user, or empty string if not in learning mode
308
+ */
309
+ cancelWorkflowLearning(reason) {
310
+ if (!this.workflowLearningActive) {
311
+ return '';
312
+ }
313
+ this.workflowLearningActive = false;
314
+ const stepsCount = this.learnedWorkflowSteps.length;
315
+ this.learnedWorkflowSteps = [];
316
+ quickLog(`[${new Date().toISOString()}] [WorkflowLearning] Cancelled: ${reason} (${stepsCount} steps discarded)\n`);
317
+ return `⚠️ **Learning mode cancelled**: ${reason}\n\n` +
318
+ `${stepsCount > 0 ? `${stepsCount} recorded step(s) were discarded.` : 'No steps were recorded.'}\n` +
319
+ `Run \`/workflow new learn-workflow\` to start again.`;
320
+ }
321
+ /**
322
+ * Build a prompt for the AI to execute a workflow
323
+ * This formats the workflow steps into a clear instruction set
324
+ */
325
+ buildWorkflowPrompt(workflow) {
326
+ const stepsText = workflow.steps.map((step, index) => {
327
+ const stepNum = index + 1;
328
+ if (step.type === 'command') {
329
+ return `Step ${stepNum}: [RUN COMMAND] ${step.content}`;
330
+ }
331
+ else {
332
+ return `Step ${stepNum}: [INSTRUCTION] ${step.content}`;
333
+ }
334
+ }).join('\n');
335
+ return `Execute the following workflow "${workflow.name}" step by step:
336
+
337
+ ${stepsText}
338
+
339
+ IMPORTANT:
340
+ - For [RUN COMMAND] steps, execute the exact command shown using the execute_command tool.
341
+ - For [INSTRUCTION] steps, follow the instruction using appropriate tools.
342
+ - Execute each step in order, waiting for completion before moving to the next.
343
+ - If any step fails, stop and report the error.
344
+ - After all steps complete successfully, summarize what was done.
345
+
346
+ Begin executing now, starting with Step 1.`;
347
+ }
348
+ /**
349
+ * Warpify a detected SSH/WSL/Docker session.
350
+ * This establishes a proper ssh2/handler connection (will prompt for password)
351
+ * so commands can be executed via the handler, not just PTY passthrough.
352
+ *
353
+ * @param command - The original command (e.g., "ssh rohan@localhost")
354
+ * @param type - The detected session type
355
+ * @param connectionString - The connection string (e.g., "rohan@localhost")
356
+ * @returns Promise<boolean> - true if warpify succeeded
357
+ */
358
+ async warpifySession(command, type, connectionString) {
359
+ try {
360
+ // Show connecting status
361
+ if (this.onConnectionStatusUpdate) {
362
+ this.onConnectionStatusUpdate({
363
+ type: type,
364
+ status: 'connecting',
365
+ connectionString: connectionString
366
+ });
367
+ }
368
+ // Detect and connect using the command
369
+ const detection = this.commandDetector.detect(command);
370
+ if (!detection) {
371
+ throw new Error(`Could not detect handler for command: ${command}`);
372
+ }
373
+ // Check if we are already in a remote session
374
+ const currentContext = this.contextManager.getCurrentContext();
375
+ let context;
376
+ // Save current CWD to stack BEFORE entering any new session (local or nested)
377
+ // This enables proper restoration when exiting, regardless of nesting level
378
+ this.cwdStack.push(this.cwd);
379
+ if (currentContext.type !== 'local') {
380
+ // Nested session: connect from remote
381
+ if (detection.handler.connectFromRemote) {
382
+ quickLog(`[${new Date().toISOString()}] [Warpify] nesting connection: ${currentContext.type} -> ${type}\n`);
383
+ context = await detection.handler.connectFromRemote(command, this.cwd, currentContext);
384
+ }
385
+ else {
386
+ // If nested connection fails, pop the CWD we just pushed
387
+ this.cwdStack.pop();
388
+ throw new Error(`Nested connections are not supported by the ${type} handler yet.`);
389
+ }
390
+ }
391
+ else {
392
+ // Local session: connect from local
393
+ // For SSH, we shouldn't pass the local (potentially Windows) CWD as it won't exist remotely.
394
+ // For local->WSL or local->Docker, the handler might translate it, but strictly speaking
395
+ // starting 'fresh' in the remote user's home/default dir is safer for 'warpify' semantics regardless of type.
396
+ // However, to avoid regression for WSL/Docker usage where inheritance is expected, we restricts this fix to SSH.
397
+ const initialCwd = type === 'ssh' ? undefined : this.cwd;
398
+ context = await detection.handler.connect(command, initialCwd);
399
+ }
400
+ this.lastConnectionCommand = command;
401
+ this.contextManager.pushContext(context);
402
+ // Explicitly sync this.cwd with the new context's CWD to ensure consistency immediately
403
+ if (context.metadata.workingDirectory) {
404
+ this.cwd = context.metadata.workingDirectory;
405
+ }
406
+ // Set up disconnect callback for remote connections
407
+ if (context.handler && context.handler.setDisconnectCallback) {
408
+ context.handler.setDisconnectCallback((error) => {
409
+ this.handleRemoteDisconnect(connectionString || '', type, error);
410
+ });
411
+ }
412
+ // Show success
413
+ if (this.onConnectionStatusUpdate) {
414
+ this.onConnectionStatusUpdate({
415
+ type: type,
416
+ status: 'connected',
417
+ connectionString: connectionString
418
+ });
419
+ }
420
+ // Update CWD to remote working directory
421
+ if (this.onCwdChange && context.metadata.workingDirectory) {
422
+ this.onCwdChange(context.metadata.workingDirectory);
423
+ }
424
+ quickLog(`[${new Date().toISOString()}] [Warpify] Successfully established ${type} connection via ssh2/handler\n`);
425
+ return true;
426
+ }
427
+ catch (error) {
428
+ // Connection failed
429
+ if (this.onConnectionStatusUpdate) {
430
+ this.onConnectionStatusUpdate({
431
+ type: type,
432
+ status: 'error',
433
+ connectionString: connectionString,
434
+ error: error.message
435
+ });
436
+ }
437
+ quickLog(`[${new Date().toISOString()}] [Warpify] Failed to establish connection: ${error.message}\n`);
438
+ return false;
439
+ }
440
+ }
441
+ /**
442
+ * Exit the current remote session and return to parent environment.
443
+ * Called by the AI agent's enter_remote_session tool with action="exit".
444
+ * @returns Promise<boolean> - true if there was a session to exit
445
+ */
446
+ async exitRemoteSession() {
447
+ const currentContext = this.contextManager.getCurrentContext();
448
+ // Check if we're in a remote session
449
+ if (currentContext.type === 'local') {
450
+ return false; // No remote session to exit
451
+ }
452
+ // Get connection info for logging
453
+ const sessionType = currentContext.type;
454
+ const metadata = currentContext.metadata;
455
+ const connectionString = metadata.connectionString ||
456
+ metadata.host ||
457
+ metadata.containerId ||
458
+ metadata.distro ||
459
+ metadata.workingDirectory ||
460
+ 'unknown';
461
+ quickLog(`[${new Date().toISOString()}] [ExitRemoteSession] Exiting ${sessionType} session: ${connectionString}\n`);
462
+ // Close the handler if it has a disconnect method
463
+ if (currentContext.handler && typeof currentContext.handler.disconnect === 'function') {
464
+ try {
465
+ await currentContext.handler.disconnect();
466
+ }
467
+ catch (error) {
468
+ quickLog(`[${new Date().toISOString()}] [ExitRemoteSession] Error during handler disconnect: ${error.message}\n`);
469
+ }
470
+ }
471
+ // Pop the context
472
+ this.contextManager.popContext();
473
+ // Restore CWD from stack
474
+ const previousCwd = this.cwdStack.pop();
475
+ const newContext = this.contextManager.getCurrentContext();
476
+ if (previousCwd) {
477
+ this.cwd = previousCwd;
478
+ }
479
+ else if (newContext.type !== 'local') {
480
+ this.cwd = newContext.metadata.workingDirectory;
481
+ }
482
+ this.contextManager.updateWorkingDirectory(this.cwd);
483
+ if (newContext.type === 'local') {
484
+ this.lastConnectionCommand = null;
485
+ }
486
+ // Notify CWD change
487
+ if (this.onCwdChange) {
488
+ this.onCwdChange(this.cwd);
489
+ }
490
+ // Save chat state
491
+ this.saveCurrentChat();
492
+ // Update UI connection status
493
+ if (this.onConnectionStatusUpdate) {
494
+ if (newContext.type === 'local') {
495
+ this.onConnectionStatusUpdate({
496
+ type: sessionType,
497
+ status: 'disconnected',
498
+ connectionString: connectionString
499
+ });
500
+ }
501
+ else {
502
+ // Still in a nested session
503
+ const newMetadata = newContext.metadata;
504
+ this.onConnectionStatusUpdate({
505
+ type: newContext.type,
506
+ status: 'connected',
507
+ connectionString: newMetadata.connectionString || newMetadata.host || newMetadata.workingDirectory
508
+ });
509
+ }
510
+ }
511
+ return true;
512
+ }
513
+ /**
514
+ * Handle disconnection from a remote session
515
+ */
516
+ handleRemoteDisconnect(connectionString, type, error) {
517
+ quickLog(`[${new Date().toISOString()}] [Warpify] Disconnected from ${type} session: ${connectionString} (${error || 'clean exit'})\n`);
518
+ // Pop the context (this was the session that just ended)
519
+ const endedContext = this.contextManager.popContext();
520
+ // Now look at the NEW current context (the parent)
521
+ const currentContext = this.contextManager.getCurrentContext();
522
+ // Restore CWD from stack - this handles any nesting level
523
+ const previousCwd = this.cwdStack.pop();
524
+ if (previousCwd) {
525
+ this.cwd = previousCwd;
526
+ }
527
+ else if (currentContext.type !== 'local') {
528
+ // Fallback: use parent context's CWD if no stack entry
529
+ this.cwd = currentContext.metadata.workingDirectory;
530
+ }
531
+ this.contextManager.updateWorkingDirectory(this.cwd);
532
+ if (currentContext.type === 'local') {
533
+ this.lastConnectionCommand = null;
534
+ }
535
+ // Notify CWD change
536
+ if (this.onCwdChange) {
537
+ this.onCwdChange(this.cwd);
538
+ }
539
+ // Save chat state
540
+ this.saveCurrentChat();
541
+ // Update UI connection status
542
+ if (this.onConnectionStatusUpdate) {
543
+ if (currentContext.type === 'local') {
544
+ this.onConnectionStatusUpdate({
545
+ type: type,
546
+ status: 'disconnected',
547
+ connectionString
548
+ });
549
+ }
550
+ else {
551
+ // Update status to reflect the parent session
552
+ let parentConnString = '';
553
+ if (currentContext.type === 'ssh') {
554
+ parentConnString = `${currentContext.metadata.username}@${currentContext.metadata.hostname}`;
555
+ }
556
+ else if (currentContext.type === 'wsl') {
557
+ parentConnString = currentContext.metadata.distroName || '';
558
+ }
559
+ else if (currentContext.type === 'docker') {
560
+ parentConnString = currentContext.metadata.containerId || '';
561
+ }
562
+ this.onConnectionStatusUpdate({
563
+ type: currentContext.type,
564
+ status: 'connected',
565
+ connectionString: parentConnString
566
+ });
567
+ }
568
+ }
569
+ // If there's an active tool execution (shell running), mark it as error
570
+ if (this.onToolExecutionUpdate && this.currentInteractiveProcess) {
571
+ this.onToolExecutionUpdate({
572
+ toolName: 'execute_command',
573
+ status: 'error',
574
+ error: `Disconnected from ${type}: ${error || 'Connection lost'}`
575
+ });
576
+ this.currentInteractiveProcess = undefined;
577
+ }
578
+ }
221
579
  /**
222
580
  * Calculate and update token count based on current conversation history
223
581
  * This ensures UI is always in sync with the actual AI context
@@ -498,6 +856,11 @@ export class CentaurusCLI {
498
856
  * Notify UI about tool execution status
499
857
  */
500
858
  notifyToolStatus(toolName, status, args, result, error) {
859
+ // Skip UI status updates for enter_remote_session - the password prompt
860
+ // and "Established Wormhole" message already provide sufficient feedback
861
+ if (toolName === 'enter_remote_session') {
862
+ return;
863
+ }
501
864
  if (this.onToolExecutionUpdate) {
502
865
  // Get current context for remote prefix
503
866
  const currentContext = this.contextManager.getCurrentContext();
@@ -574,6 +937,8 @@ export class CentaurusCLI {
574
937
  this.toolRegistry.register(createImageTool);
575
938
  this.toolRegistry.register(backgroundCommandTool);
576
939
  this.toolRegistry.register(subAgentTool);
940
+ this.toolRegistry.register(enterRemoteSessionTool);
941
+ this.toolRegistry.register(workflowTool);
577
942
  // Initialize SubAgentManager with tool registry
578
943
  SubAgentManager.initialize(this.toolRegistry);
579
944
  SubAgentManager.setOnSubAgentCountChange((count) => {
@@ -769,14 +1134,22 @@ Press Enter to continue...
769
1134
  }
770
1135
  }
771
1136
  }
772
- async handleMessage(message) {
1137
+ async handleMessage(message, options = {}) {
773
1138
  // Handle command mode - execute commands directly
774
1139
  if (this.commandMode) {
1140
+ // Record command step if workflow learning mode is active
1141
+ if (this.workflowLearningActive) {
1142
+ this.recordWorkflowStep('command', message);
1143
+ }
775
1144
  await this.handleCommandModeExecution(message);
776
1145
  return;
777
1146
  }
778
1147
  // Handle background mode - execute commands in background
779
1148
  if (this.backgroundMode) {
1149
+ // Record command step if workflow learning mode is active
1150
+ if (this.workflowLearningActive) {
1151
+ this.recordWorkflowStep('command', message);
1152
+ }
780
1153
  this.handleBackgroundModeExecution(message);
781
1154
  return;
782
1155
  }
@@ -785,6 +1158,11 @@ Press Enter to continue...
785
1158
  await this.handleSlashCommand(message);
786
1159
  return;
787
1160
  }
1161
+ // Record step if workflow learning mode is active
1162
+ // AI prompts are recorded as instruction steps
1163
+ if (this.workflowLearningActive) {
1164
+ this.recordWorkflowStep('instruction', message);
1165
+ }
788
1166
  // Check authentication
789
1167
  if (!apiClient.isAuthenticated()) {
790
1168
  throw new Error('Authentication required. Please sign in to use AI features.');
@@ -887,6 +1265,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
887
1265
  role: 'user',
888
1266
  content: userMessageContent,
889
1267
  });
1268
+ // Calculate start index for AI context (0 for normal, current index for isolated workflow)
1269
+ const contextStartIndex = options.isolatedWorkflow ? this.conversationHistory.length - 1 : 0;
1270
+ // Helper to get messages for AI context respecting isolation
1271
+ const getMessagesForContext = () => {
1272
+ if (options.isolatedWorkflow) {
1273
+ return this.conversationHistory.slice(contextStartIndex);
1274
+ }
1275
+ return [...this.conversationHistory];
1276
+ };
890
1277
  // Messages are stored locally only - no backend persistence needed
891
1278
  // Local storage is handled by saveCurrentChat() which saves to ~/.centaurus/chats/
892
1279
  // Start logging session and log user message
@@ -931,7 +1318,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
931
1318
  // SAFETY: Clean up any orphaned tool calls before making AI request
932
1319
  // This prevents "improperly formed request" errors from corrupted history
933
1320
  this.cleanupOrphanedToolCalls();
934
- let messages = [...this.conversationHistory];
1321
+ let messages = getMessagesForContext();
935
1322
  // Inject subshell context if in a subshell environment
936
1323
  const currentContext = this.contextManager.getCurrentContext();
937
1324
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
@@ -1380,7 +1767,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1380
1767
  // Clear pending plan request
1381
1768
  this.pendingPlanRequest = null;
1382
1769
  // Update messages array for this turn
1383
- messages = [...this.conversationHistory];
1770
+ messages = getMessagesForContext();
1384
1771
  // Continue the loop - AI will now execute with plan context
1385
1772
  continue;
1386
1773
  }
@@ -1445,7 +1832,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1445
1832
  });
1446
1833
  // Mark this tool call as handled so it's not duplicated
1447
1834
  handledToolCallIds.add(toolCall.id);
1448
- messages = [...this.conversationHistory];
1835
+ messages = getMessagesForContext();
1449
1836
  }
1450
1837
  }
1451
1838
  else {
@@ -1469,7 +1856,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1469
1856
  });
1470
1857
  // Mark this tool call as handled so it's not duplicated
1471
1858
  handledToolCallIds.add(toolCall.id);
1472
- messages = [...this.conversationHistory];
1859
+ messages = getMessagesForContext();
1473
1860
  }
1474
1861
  continue;
1475
1862
  }
@@ -1536,7 +1923,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1536
1923
  // Mark as handled
1537
1924
  handledToolCallIds.add(toolCall.id);
1538
1925
  // Update messages and continue
1539
- messages = [...this.conversationHistory];
1926
+ messages = getMessagesForContext();
1540
1927
  continue;
1541
1928
  }
1542
1929
  }
@@ -1878,7 +2265,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1878
2265
  // Rebuild messages array with updated history
1879
2266
  // During agent loop: keep ALL thinking for current task
1880
2267
  // (Thinking from previous tasks was already stripped at request start)
1881
- messages = [...this.conversationHistory];
2268
+ messages = getMessagesForContext();
1882
2269
  // No need to reset currentTurnThinking - keep accumulating for the task
1883
2270
  // Re-inject subshell context
1884
2271
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
@@ -1921,7 +2308,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1921
2308
  }
1922
2309
  // Rebuild messages array with updated history
1923
2310
  // Backend will inject system prompt
1924
- messages = [...this.conversationHistory];
2311
+ messages = getMessagesForContext();
1925
2312
  // Re-inject subshell context
1926
2313
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
1927
2314
  // Continue loop to get AI's response (removed 500ms delay for faster response)
@@ -1942,7 +2329,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1942
2329
  });
1943
2330
  // Rebuild messages array with updated history
1944
2331
  // Backend will inject system prompt
1945
- messages = [...this.conversationHistory];
2332
+ messages = getMessagesForContext();
1946
2333
  // Re-inject subshell context
1947
2334
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
1948
2335
  // Add delay before prompting
@@ -2276,6 +2663,11 @@ Start by listing the directory structure to understand what you're working with.
2276
2663
  break;
2277
2664
  case 'logout':
2278
2665
  try {
2666
+ // Cancel workflow learning if active before logout
2667
+ const logoutCancelMsg = this.cancelWorkflowLearning('Logging out');
2668
+ if (logoutCancelMsg && this.onDirectMessageCallback) {
2669
+ this.onDirectMessageCallback(logoutCancelMsg);
2670
+ }
2279
2671
  await apiClient.logout();
2280
2672
  responseMessage = '✅ Logged out successfully.\n\n' +
2281
2673
  'Your session has been cleared.\n' +
@@ -2354,6 +2746,11 @@ Start by listing the directory structure to understand what you're working with.
2354
2746
  '• Manual completion detection';
2355
2747
  break;
2356
2748
  case 'clear':
2749
+ // Cancel workflow learning if active before clearing
2750
+ const clearCancelMsg = this.cancelWorkflowLearning('Clearing chat session');
2751
+ if (clearCancelMsg && this.onDirectMessageCallback) {
2752
+ this.onDirectMessageCallback(clearCancelMsg);
2753
+ }
2357
2754
  // Start a new chat session (clears history and generates new chat ID)
2358
2755
  this.startNewChat();
2359
2756
  // Don't send any response message - the UI will handle clearing
@@ -2792,6 +3189,163 @@ Then try /models local again.`;
2792
3189
  ' /background-task cancel - Cancel a running background task';
2793
3190
  }
2794
3191
  break;
3192
+ case 'workflow':
3193
+ case 'wf':
3194
+ // Workflow management commands
3195
+ const wfSubCommand = args[0]?.toLowerCase();
3196
+ if (!wfSubCommand) {
3197
+ // Show workflow help
3198
+ responseMessage = `📋 **Workflow Commands:**
3199
+
3200
+ **/workflow new** - Create a new workflow
3201
+ **/workflow list** - List saved workflows
3202
+ **/workflow run <name>** - Run a workflow
3203
+ **/workflow view <name>** - View workflow steps
3204
+ **/workflow delete <name>** - Delete a workflow
3205
+
3206
+ **What are workflows?**
3207
+ Workflows let you save and replay sequences of commands and instructions.
3208
+ Create once, run many times across different machines.`;
3209
+ }
3210
+ else if (wfSubCommand === 'new' || wfSubCommand === 'create') {
3211
+ // Check for nested subcommand (manual or learn-workflow)
3212
+ const newSubCommand = args[1]?.toLowerCase();
3213
+ if (newSubCommand === 'manual') {
3214
+ // If learning mode is active, cancel it instead of starting manual mode
3215
+ if (this.workflowLearningActive) {
3216
+ responseMessage = this.cancelWorkflowLearning('Switching to manual mode');
3217
+ break; // Don't start manual mode, just show cancellation message
3218
+ }
3219
+ // Show workflow creator screen (manual mode)
3220
+ if (this.onShowWorkflowCreatorCallback) {
3221
+ this.onShowWorkflowCreatorCallback();
3222
+ return; // Don't send text response, UI will handle it
3223
+ }
3224
+ else {
3225
+ responseMessage = '❌ Workflow creator not available. Please update the CLI.';
3226
+ }
3227
+ }
3228
+ else if (newSubCommand === 'learn-workflow') {
3229
+ // Toggle workflow learning mode
3230
+ const result = this.toggleWorkflowLearning();
3231
+ if (result === null) {
3232
+ return; // UI will handle the screen change
3233
+ }
3234
+ responseMessage = result;
3235
+ }
3236
+ else if (newSubCommand) {
3237
+ // Unknown subcommand
3238
+ responseMessage = `❌ Unknown workflow new subcommand: ${newSubCommand}. Use 'manual' or 'learn-workflow'.`;
3239
+ }
3240
+ else {
3241
+ // No subcommand - show help for new options
3242
+ responseMessage = `📋 **Create a New Workflow:**
3243
+
3244
+ **/workflow new manual** - Manually create by typing steps
3245
+ **/workflow new learn-workflow** - Learn from your commands and prompts
3246
+
3247
+ **Manual Mode:** Type each step one at a time, toggle between command/instruction mode.
3248
+
3249
+ **Learn Mode:** Start learning, then run commands and prompts naturally. Run the command again to save.`;
3250
+ }
3251
+ }
3252
+ else if (wfSubCommand === 'list' || wfSubCommand === 'ls') {
3253
+ // List saved workflows
3254
+ const workflows = workflowStorage.list();
3255
+ if (workflows.length === 0) {
3256
+ responseMessage = '📭 No workflows saved yet.\n\nCreate one with: /workflow new';
3257
+ }
3258
+ else {
3259
+ responseMessage = `📋 **Saved Workflows (${workflows.length}):**\n\n`;
3260
+ for (const wf of workflows) {
3261
+ const date = new Date(wf.updatedAt).toLocaleDateString();
3262
+ responseMessage += `• **${wf.name}** (${wf.stepCount} steps)\n`;
3263
+ if (wf.description) {
3264
+ responseMessage += ` ${wf.description}\n`;
3265
+ }
3266
+ responseMessage += ` Last updated: ${date}\n\n`;
3267
+ }
3268
+ responseMessage += 'Use `/workflow run <name>` to execute a workflow.';
3269
+ }
3270
+ }
3271
+ else if (wfSubCommand === 'run' || wfSubCommand === 'execute') {
3272
+ // Run a workflow
3273
+ const wfName = args.slice(1).join(' ').trim();
3274
+ if (!wfName) {
3275
+ responseMessage = 'Usage: /workflow run <name>\n\nUse /workflow list to see available workflows.';
3276
+ }
3277
+ else {
3278
+ // Cancel workflow learning if active before running a different workflow
3279
+ const runCancelMsg = this.cancelWorkflowLearning('Running another workflow');
3280
+ if (runCancelMsg && this.onDirectMessageCallback) {
3281
+ this.onDirectMessageCallback(runCancelMsg);
3282
+ }
3283
+ const workflow = workflowStorage.load(wfName);
3284
+ if (!workflow) {
3285
+ responseMessage = `❌ Workflow '${wfName}' not found.\n\nUse /workflow list to see available workflows.`;
3286
+ }
3287
+ else {
3288
+ // Start workflow execution
3289
+ responseMessage = `🚀 Starting workflow: **${workflow.name}**\n\n` +
3290
+ `${workflow.steps.length} steps to execute...\n\n` +
3291
+ workflowStorage.formatWorkflowForDisplay(workflow);
3292
+ // Send initial response
3293
+ if (this.onDirectMessageCallback) {
3294
+ this.onDirectMessageCallback(responseMessage);
3295
+ }
3296
+ // Trigger workflow execution by sending the workflow to the AI
3297
+ // Format the workflow as a prompt for the AI to execute
3298
+ const workflowPrompt = this.buildWorkflowPrompt(workflow);
3299
+ // Use setTimeout to allow the UI to update before starting execution
3300
+ setTimeout(() => {
3301
+ this.handleMessage(workflowPrompt, { isolatedWorkflow: true });
3302
+ }, 100);
3303
+ return;
3304
+ }
3305
+ }
3306
+ }
3307
+ else if (wfSubCommand === 'view' || wfSubCommand === 'show') {
3308
+ // View a workflow's steps
3309
+ const wfName = args.slice(1).join(' ').trim();
3310
+ if (!wfName) {
3311
+ responseMessage = 'Usage: /workflow view <name>';
3312
+ }
3313
+ else {
3314
+ const workflow = workflowStorage.load(wfName);
3315
+ if (!workflow) {
3316
+ responseMessage = `❌ Workflow '${wfName}' not found.\n\nUse /workflow list to see available workflows.`;
3317
+ }
3318
+ else {
3319
+ responseMessage = workflowStorage.formatWorkflowForDisplay(workflow);
3320
+ }
3321
+ }
3322
+ }
3323
+ else if (wfSubCommand === 'delete' || wfSubCommand === 'rm' || wfSubCommand === 'remove') {
3324
+ // Delete a workflow
3325
+ const wfName = args.slice(1).join(' ').trim();
3326
+ if (!wfName) {
3327
+ responseMessage = 'Usage: /workflow delete <name>';
3328
+ }
3329
+ else {
3330
+ const result = workflowStorage.delete(wfName);
3331
+ if (result.success) {
3332
+ responseMessage = `✅ Workflow '${wfName}' deleted successfully.`;
3333
+ }
3334
+ else {
3335
+ responseMessage = `❌ ${result.error}`;
3336
+ }
3337
+ }
3338
+ }
3339
+ else {
3340
+ responseMessage = `Unknown /workflow subcommand: ${wfSubCommand}\n\n` +
3341
+ 'Usage:\n' +
3342
+ ' /workflow new - Create a new workflow\n' +
3343
+ ' /workflow list - List saved workflows\n' +
3344
+ ' /workflow run <name> - Run a workflow\n' +
3345
+ ' /workflow view <name> - View workflow steps\n' +
3346
+ ' /workflow delete <name> - Delete a workflow';
3347
+ }
3348
+ break;
2795
3349
  case 'sync':
2796
3350
  // Sync local data to/from cloud
2797
3351
  if (!apiClient.isAuthenticated()) {
@@ -3312,7 +3866,7 @@ Then try /models local again.`;
3312
3866
  type: currentContext.type,
3313
3867
  connectionCommand: this.lastConnectionCommand,
3314
3868
  remoteCwd: currentContext.metadata.workingDirectory,
3315
- localCwdBeforeRemote: this.localCwdBeforeRemote || process.cwd(),
3869
+ localCwdBeforeRemote: this.cwdStack.length > 0 ? this.cwdStack[0] : process.cwd(),
3316
3870
  metadata: {
3317
3871
  hostname: currentContext.metadata.hostname,
3318
3872
  username: currentContext.metadata.username,
@@ -3322,9 +3876,9 @@ Then try /models local again.`;
3322
3876
  }
3323
3877
  };
3324
3878
  }
3325
- // Determine the local CWD to save (use localCwdBeforeRemote if in remote, otherwise current cwd)
3326
- const cwdToSave = currentContext.type !== 'local' && this.localCwdBeforeRemote
3327
- ? this.localCwdBeforeRemote
3879
+ // Determine the local CWD to save (use base of cwdStack if in remote, otherwise current cwd)
3880
+ const cwdToSave = currentContext.type !== 'local' && this.cwdStack.length > 0
3881
+ ? this.cwdStack[0]
3328
3882
  : this.cwd;
3329
3883
  try {
3330
3884
  localChatStorage.saveChat(this.currentChatId, storedMessages, storedUIMessages, cwdToSave, remoteContext);
@@ -3415,7 +3969,7 @@ Then try /models local again.`;
3415
3969
  // Pop context to return to local
3416
3970
  this.contextManager.popContext();
3417
3971
  // Clear remote context tracking
3418
- this.localCwdBeforeRemote = null;
3972
+ this.cwdStack = [];
3419
3973
  this.lastConnectionCommand = null;
3420
3974
  }
3421
3975
  // Load AI context
@@ -3443,8 +3997,8 @@ Then try /models local again.`;
3443
3997
  // Attempt to restore remote context if chat was saved while in remote environment
3444
3998
  if (chat.remoteContext) {
3445
3999
  const { type, connectionCommand, remoteCwd, localCwdBeforeRemote } = chat.remoteContext;
3446
- // Store local CWD for when user exits remote
3447
- this.localCwdBeforeRemote = localCwdBeforeRemote;
4000
+ // Store local CWD for when user exits remote (as base of stack)
4001
+ this.cwdStack = [localCwdBeforeRemote];
3448
4002
  this.lastConnectionCommand = connectionCommand;
3449
4003
  // Show reconnection notification
3450
4004
  if (this.onDirectMessageCallback) {
@@ -3489,7 +4043,7 @@ Then try /models local again.`;
3489
4043
  }
3490
4044
  catch (error) {
3491
4045
  // Connection failed - fall back to local mode
3492
- this.localCwdBeforeRemote = null;
4046
+ this.cwdStack = [];
3493
4047
  this.lastConnectionCommand = null;
3494
4048
  if (this.onConnectionStatusUpdate) {
3495
4049
  this.onConnectionStatusUpdate({
@@ -3552,7 +4106,7 @@ Then try /models local again.`;
3552
4106
  this.currentChatId = null;
3553
4107
  this.conversationStarted = false;
3554
4108
  this.uiMessageHistory = [];
3555
- this.localCwdBeforeRemote = null;
4109
+ this.cwdStack = [];
3556
4110
  this.lastConnectionCommand = null;
3557
4111
  // Reset context limit state
3558
4112
  if (this.contextLimitReached) {
@@ -3689,45 +4243,6 @@ Once the user approves the plan:
3689
4243
  }
3690
4244
  }
3691
4245
  }
3692
- /**
3693
- * Handle unexpected remote session disconnect
3694
- * Called when SSH/WSL/Docker connection is lost unexpectedly
3695
- */
3696
- handleRemoteDisconnect(connectionString, type, error) {
3697
- // Pop the remote context
3698
- this.contextManager.popContext();
3699
- // Restore local CWD
3700
- if (this.localCwdBeforeRemote) {
3701
- this.cwd = this.localCwdBeforeRemote;
3702
- this.contextManager.updateWorkingDirectory(this.localCwdBeforeRemote);
3703
- if (this.onCwdChange) {
3704
- this.onCwdChange(this.localCwdBeforeRemote);
3705
- }
3706
- }
3707
- // Clear remote context tracking
3708
- this.localCwdBeforeRemote = null;
3709
- this.lastConnectionCommand = null;
3710
- // Save chat with no remote context
3711
- this.saveCurrentChat();
3712
- // Notify UI of disconnection via connection status update
3713
- if (this.onConnectionStatusUpdate) {
3714
- this.onConnectionStatusUpdate({
3715
- type: type,
3716
- status: 'disconnected',
3717
- connectionString,
3718
- error: error
3719
- });
3720
- }
3721
- // If there's an active tool execution (shell running), mark it as error
3722
- if (this.onToolExecutionUpdate && this.currentInteractiveProcess) {
3723
- this.onToolExecutionUpdate({
3724
- toolName: 'execute_command',
3725
- status: 'error',
3726
- error: `Disconnected from ${type}: ${error || 'Connection lost'}`
3727
- });
3728
- this.currentInteractiveProcess = undefined;
3729
- }
3730
- }
3731
4246
  /**
3732
4247
  * Set the current interactive process
3733
4248
  * This is called by the execute_command tool when starting an interactive command
@@ -3769,20 +4284,32 @@ Once the user approves the plan:
3769
4284
  if (currentContext.handler) {
3770
4285
  await currentContext.handler.disconnect();
3771
4286
  }
3772
- // Pop context
4287
+ // Pop context - this returns us to the parent context
3773
4288
  this.contextManager.popContext();
3774
- // Restore local CWD that was saved before entering remote session
3775
- if (this.localCwdBeforeRemote) {
3776
- this.cwd = this.localCwdBeforeRemote;
3777
- this.contextManager.updateWorkingDirectory(this.localCwdBeforeRemote);
4289
+ // Check the NEW current context after popping
4290
+ const newContext = this.contextManager.getCurrentContext();
4291
+ // Pop the previous CWD from stack - this handles any nesting level
4292
+ const previousCwd = this.cwdStack.pop();
4293
+ if (previousCwd) {
4294
+ this.cwd = previousCwd;
4295
+ this.contextManager.updateWorkingDirectory(previousCwd);
4296
+ if (this.onCwdChange) {
4297
+ this.onCwdChange(previousCwd);
4298
+ }
4299
+ }
4300
+ else if (newContext.type !== 'local') {
4301
+ // Fallback: use parent context's CWD if no stack entry
4302
+ const parentCwd = newContext.metadata?.workingDirectory || '~';
4303
+ this.cwd = parentCwd;
3778
4304
  if (this.onCwdChange) {
3779
- this.onCwdChange(this.localCwdBeforeRemote);
4305
+ this.onCwdChange(parentCwd);
3780
4306
  }
3781
4307
  }
3782
- // Clear remote context tracking
3783
- this.localCwdBeforeRemote = null;
3784
- this.lastConnectionCommand = null;
3785
- // Save chat with no remote context - so resuming won't try to reconnect
4308
+ if (newContext.type === 'local') {
4309
+ // Clear tracking when back to local
4310
+ this.lastConnectionCommand = null;
4311
+ }
4312
+ // Save chat - include remote context info if still in remote
3786
4313
  this.saveCurrentChat();
3787
4314
  if (this.onResponseCallback) {
3788
4315
  this.onResponseCallback('✅ Exited subshell');
@@ -3790,80 +4317,19 @@ Once the user approves the plan:
3790
4317
  return;
3791
4318
  }
3792
4319
  }
3793
- // Detect subshell commands
4320
+ // WARPIFY MODE: SSH/WSL/Docker commands now run as NORMAL PTY commands
4321
+ // The old flow intercepted these commands and used the ssh2/etc libraries directly.
4322
+ // The new flow lets them run in focus mode, user enters password in terminal,
4323
+ // then presses Alt+E to "warpify" the session for AI context awareness.
4324
+ // The detection code is preserved below (commented) for reference.
4325
+ /*
4326
+ // Detect subshell commands (OLD FLOW - DISABLED FOR WARPIFY)
3794
4327
  const detection = this.commandDetector.detect(command);
3795
4328
  if (detection) {
3796
- // Build connection string for display (e.g., "rohan@localhost" for SSH)
3797
- let connectionString = '';
3798
- if (detection.handler.type === 'ssh') {
3799
- // Parse SSH command to get user@host
3800
- const sshMatch = command.match(/ssh\s+(?:(?:-\w+\s+)+)?(?:(\S+)@)?(\S+)/);
3801
- if (sshMatch) {
3802
- const user = sshMatch[1] || 'user';
3803
- const host = sshMatch[2] || 'remote';
3804
- connectionString = `${user}@${host}`;
3805
- }
3806
- }
3807
- else if (detection.handler.type === 'wsl') {
3808
- // Parse WSL command to get distribution name
3809
- const wslMatch = command.match(/wsl(?:\s+(?:-d|--distribution)\s+(\S+))?/);
3810
- connectionString = wslMatch?.[1] || 'Ubuntu';
3811
- }
3812
- else if (detection.handler.type === 'docker') {
3813
- // Parse Docker command to get container
3814
- const dockerMatch = command.match(/docker\s+exec\s+(?:(?:-\w+\s+)+)?(\S+)/);
3815
- connectionString = dockerMatch?.[1]?.substring(0, 12) || 'container';
3816
- }
3817
- // Show connecting message with spinner (dynamic)
3818
- if (this.onConnectionStatusUpdate) {
3819
- this.onConnectionStatusUpdate({
3820
- type: detection.handler.type,
3821
- status: 'connecting',
3822
- connectionString
3823
- });
3824
- }
3825
- // Update connection state
3826
- this.contextManager.updateConnectionState('connecting');
3827
- try {
3828
- // Save local CWD before entering remote session (for restoration when resuming chat)
3829
- if (this.contextManager.getCurrentContext().type === 'local') {
3830
- this.localCwdBeforeRemote = this.cwd;
3831
- }
3832
- this.lastConnectionCommand = command;
3833
- // Connect to subshell
3834
- const context = await detection.handler.connect(command, this.cwd);
3835
- this.contextManager.pushContext(context);
3836
- // Set up disconnect callback for SSH connections
3837
- if (detection.handler.type === 'ssh' && context.handler?.setDisconnectCallback) {
3838
- context.handler.setDisconnectCallback((error) => {
3839
- // Handle unexpected disconnect
3840
- this.handleRemoteDisconnect(connectionString, detection.handler.type, error);
3841
- });
3842
- }
3843
- // Show success message (replaces the spinner with static message)
3844
- if (this.onConnectionStatusUpdate) {
3845
- this.onConnectionStatusUpdate({
3846
- type: detection.handler.type,
3847
- status: 'connected',
3848
- connectionString
3849
- });
3850
- }
3851
- return;
3852
- }
3853
- catch (error) {
3854
- // Connection failed
3855
- this.contextManager.updateConnectionState('error');
3856
- if (this.onConnectionStatusUpdate) {
3857
- this.onConnectionStatusUpdate({
3858
- type: detection.handler.type,
3859
- status: 'error',
3860
- connectionString,
3861
- error: error.message
3862
- });
3863
- }
3864
- return;
3865
- }
4329
+ // ... old interception logic ...
4330
+ // This triggered password prompt BEFORE running the command
3866
4331
  }
4332
+ */
3867
4333
  // Special handling for cd command - change the actual working directory
3868
4334
  const cdMatch = command.match(/^cd\s+(.+)$/);
3869
4335
  if (cdMatch) {