centaurus-cli 2.8.0 → 2.8.2

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 (98) hide show
  1. package/dist/cli-adapter.d.ts +8 -6
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +599 -182
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/slash-commands.d.ts.map +1 -1
  6. package/dist/config/slash-commands.js +1 -0
  7. package/dist/config/slash-commands.js.map +1 -1
  8. package/dist/context/context-manager.d.ts +4 -1
  9. package/dist/context/context-manager.d.ts.map +1 -1
  10. package/dist/context/context-manager.js +30 -7
  11. package/dist/context/context-manager.js.map +1 -1
  12. package/dist/context/handlers/wsl-handler.d.ts +10 -0
  13. package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
  14. package/dist/context/handlers/wsl-handler.js +29 -1
  15. package/dist/context/handlers/wsl-handler.js.map +1 -1
  16. package/dist/index.js +30 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/services/ai-service-client.d.ts +1 -0
  19. package/dist/services/ai-service-client.d.ts.map +1 -1
  20. package/dist/services/ai-service-client.js.map +1 -1
  21. package/dist/tools/command.d.ts.map +1 -1
  22. package/dist/tools/command.js +136 -21
  23. package/dist/tools/command.js.map +1 -1
  24. package/dist/tools/file-ops.d.ts +1 -0
  25. package/dist/tools/file-ops.d.ts.map +1 -1
  26. package/dist/tools/file-ops.js +131 -0
  27. package/dist/tools/file-ops.js.map +1 -1
  28. package/dist/tools/inspect-symbol.js +27 -27
  29. package/dist/tools/inspect-symbol.js.map +1 -1
  30. package/dist/tools/plan-mode.d.ts +55 -19
  31. package/dist/tools/plan-mode.d.ts.map +1 -1
  32. package/dist/tools/plan-mode.js +204 -123
  33. package/dist/tools/plan-mode.js.map +1 -1
  34. package/dist/tools/reproduce_issue.d.ts +2 -0
  35. package/dist/tools/reproduce_issue.d.ts.map +1 -0
  36. package/dist/tools/reproduce_issue.js +166 -0
  37. package/dist/tools/reproduce_issue.js.map +1 -0
  38. package/dist/tools/types.d.ts +1 -1
  39. package/dist/tools/types.d.ts.map +1 -1
  40. package/dist/tools/validation.d.ts.map +1 -1
  41. package/dist/tools/validation.js +4 -3
  42. package/dist/tools/validation.js.map +1 -1
  43. package/dist/types/index.d.ts +11 -1
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/ui/components/App.d.ts +6 -6
  46. package/dist/ui/components/App.d.ts.map +1 -1
  47. package/dist/ui/components/App.js +228 -62
  48. package/dist/ui/components/App.js.map +1 -1
  49. package/dist/ui/components/DiffViewer.d.ts.map +1 -1
  50. package/dist/ui/components/DiffViewer.js +69 -58
  51. package/dist/ui/components/DiffViewer.js.map +1 -1
  52. package/dist/ui/components/FileTagAutocomplete.d.ts +11 -0
  53. package/dist/ui/components/FileTagAutocomplete.d.ts.map +1 -0
  54. package/dist/ui/components/FileTagAutocomplete.js +27 -0
  55. package/dist/ui/components/FileTagAutocomplete.js.map +1 -0
  56. package/dist/ui/components/InputBox.d.ts.map +1 -1
  57. package/dist/ui/components/InputBox.js +209 -7
  58. package/dist/ui/components/InputBox.js.map +1 -1
  59. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  60. package/dist/ui/components/InteractiveShell.js +177 -13
  61. package/dist/ui/components/InteractiveShell.js.map +1 -1
  62. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  63. package/dist/ui/components/MessageDisplay.js +3 -3
  64. package/dist/ui/components/MessageDisplay.js.map +1 -1
  65. package/dist/ui/components/PlanAcceptedMessage.d.ts +12 -0
  66. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -0
  67. package/dist/ui/components/PlanAcceptedMessage.js +22 -0
  68. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -0
  69. package/dist/ui/components/PlanReviewScreen.d.ts +14 -0
  70. package/dist/ui/components/PlanReviewScreen.d.ts.map +1 -0
  71. package/dist/ui/components/PlanReviewScreen.js +52 -0
  72. package/dist/ui/components/PlanReviewScreen.js.map +1 -0
  73. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  74. package/dist/ui/components/StreamingMessageDisplay.js +3 -3
  75. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  76. package/dist/ui/components/TaskCompletedMessage.d.ts +14 -0
  77. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -0
  78. package/dist/ui/components/TaskCompletedMessage.js +25 -0
  79. package/dist/ui/components/TaskCompletedMessage.js.map +1 -0
  80. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  81. package/dist/ui/components/ToolExecutionMessage.js +207 -20
  82. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  83. package/dist/utils/conversation-logger.d.ts +127 -0
  84. package/dist/utils/conversation-logger.d.ts.map +1 -0
  85. package/dist/utils/conversation-logger.js +283 -0
  86. package/dist/utils/conversation-logger.js.map +1 -0
  87. package/dist/utils/editor-utils.d.ts +37 -0
  88. package/dist/utils/editor-utils.d.ts.map +1 -1
  89. package/dist/utils/editor-utils.js +212 -1
  90. package/dist/utils/editor-utils.js.map +1 -1
  91. package/dist/utils/input-classifier.d.ts.map +1 -1
  92. package/dist/utils/input-classifier.js +10 -3
  93. package/dist/utils/input-classifier.js.map +1 -1
  94. package/dist/utils/markdown-parser.d.ts.map +1 -1
  95. package/dist/utils/markdown-parser.js +4 -2
  96. package/dist/utils/markdown-parser.js.map +1 -1
  97. package/package.json +1 -1
  98. package/prompts/system-prompt-autonomous.md +0 -377
@@ -7,13 +7,13 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
8
  import { ConfigManager } from './config/manager.js';
9
9
  import { ToolRegistry } from './tools/registry.js';
10
- import { viewFileTool, writeToFileTool, editFileTool, listDirTool } from './tools/file-ops.js';
10
+ import { viewFileTool, writeToFileTool, editFileTool, listDirTool, multiEditFileTool } from './tools/file-ops.js';
11
11
  import { runCommandTool } from './tools/command.js';
12
12
  import { grepSearchTool } from './tools/grep-search.js';
13
13
  import { findFilesTool } from './tools/find-files.js';
14
14
  import { getDiffTool } from './tools/get-diff.js';
15
15
  import { inspectSymbolTool } from './tools/inspect-symbol.js';
16
- import { exitPlanModeTool, getPlanStatusTool, updatePlanStepTool } from './tools/plan-mode.js';
16
+ import { createPlanTool, markTaskCompleteTool, getCurrentPlan, clearPlan, approvePlan, getPlanContextForPrompt } from './tools/plan-mode.js';
17
17
  import { webSearchTool, fetchUrlTool } from './tools/web-search.js';
18
18
  import { taskCompleteTool } from './tools/task-complete.js';
19
19
  import { apiClient } from './services/api-client.js';
@@ -27,17 +27,18 @@ import { SSHHandler } from './context/handlers/ssh-handler.js';
27
27
  import { WSLHandler } from './context/handlers/wsl-handler.js';
28
28
  import { DockerHandler } from './context/handlers/docker-handler.js';
29
29
  import { AIContextInjector } from './services/ai-context-injector.js';
30
- import { environmentContextInjector } from './services/environment-context-injector.js';
31
30
  import { MCPConfigManager } from './config/mcp-config-manager.js';
32
31
  import { MCPServerManager } from './mcp/mcp-server-manager.js';
33
32
  import { MCPCommandHandler } from './mcp/mcp-command-handler.js';
34
- import { isInteractiveEditorCommand, isNanoEditor, getEditorName } from './utils/editor-utils.js';
33
+ import { isInteractiveEditorCommand, runWSLCommand, runDockerCommand, runSSHCommand } from './utils/editor-utils.js';
34
+ import { conversationLogger } from './utils/conversation-logger.js';
35
35
  export class CentaurusCLI {
36
36
  configManager;
37
37
  toolRegistry;
38
38
  conversationHistory = [];
39
39
  cwd;
40
40
  planMode = false;
41
+ pendingPlanRequest = null; // Stores original user request during planning phase
41
42
  commandMode = false;
42
43
  previousMode = 'execution';
43
44
  onResponseCallback;
@@ -54,6 +55,8 @@ export class CentaurusCLI {
54
55
  onToolStreamingOutput;
55
56
  onPlanModeChange;
56
57
  onPlanApprovalRequest;
58
+ onPlanCreated;
59
+ onTaskCompleted;
57
60
  onPasswordRequest;
58
61
  currentInteractiveProcess;
59
62
  conversationStarted = false;
@@ -118,6 +121,12 @@ export class CentaurusCLI {
118
121
  setOnPlanApprovalRequest(callback) {
119
122
  this.onPlanApprovalRequest = callback;
120
123
  }
124
+ setOnPlanCreated(callback) {
125
+ this.onPlanCreated = callback;
126
+ }
127
+ setOnTaskCompleted(callback) {
128
+ this.onTaskCompleted = callback;
129
+ }
121
130
  setOnCommandModeChange(callback) {
122
131
  this.onCommandModeChange = callback;
123
132
  }
@@ -261,15 +270,15 @@ export class CentaurusCLI {
261
270
  this.toolRegistry.register(viewFileTool);
262
271
  this.toolRegistry.register(writeToFileTool);
263
272
  this.toolRegistry.register(editFileTool);
273
+ this.toolRegistry.register(multiEditFileTool);
264
274
  this.toolRegistry.register(listDirTool);
265
275
  this.toolRegistry.register(runCommandTool);
266
276
  this.toolRegistry.register(grepSearchTool);
267
277
  this.toolRegistry.register(findFilesTool);
268
278
  this.toolRegistry.register(getDiffTool);
269
279
  this.toolRegistry.register(inspectSymbolTool);
270
- this.toolRegistry.register(exitPlanModeTool);
271
- this.toolRegistry.register(getPlanStatusTool);
272
- this.toolRegistry.register(updatePlanStepTool);
280
+ this.toolRegistry.register(createPlanTool);
281
+ this.toolRegistry.register(markTaskCompleteTool);
273
282
  this.toolRegistry.register(webSearchTool);
274
283
  this.toolRegistry.register(fetchUrlTool);
275
284
  this.toolRegistry.register(taskCompleteTool);
@@ -399,6 +408,10 @@ Press Enter to continue...
399
408
  if (!apiClient.isAuthenticated()) {
400
409
  throw new Error('Authentication required. Please sign in to use AI features.');
401
410
  }
411
+ // Store original request if in planning mode (for execution phase after approval)
412
+ if (this.planMode && !this.pendingPlanRequest) {
413
+ this.pendingPlanRequest = message;
414
+ }
402
415
  // Add user message to history
403
416
  this.conversationHistory.push({
404
417
  role: 'user',
@@ -406,6 +419,9 @@ Press Enter to continue...
406
419
  });
407
420
  // Save user message to backend
408
421
  await this.saveMessageToBackend('user', message);
422
+ // Start logging session and log user message
423
+ conversationLogger.startSession();
424
+ conversationLogger.logUserMessage(message);
409
425
  try {
410
426
  const tools = this.toolRegistry.getSchemas();
411
427
  const context = {
@@ -434,27 +450,10 @@ Press Enter to continue...
434
450
  const selectedModelConfig = ALL_MODELS.find(m => m.id === selectedModelId && m.name === config.modelName);
435
451
  const selectedModel = selectedModelId;
436
452
  const selectedModelThinkingConfig = selectedModelConfig?.thinkingConfig;
437
- // Create messages array and inject system prompt with environment context
438
- // Always use the autonomous prompt (optimized for agentic code generation)
439
- const systemPromptPath = path.join(__dirname, '..', 'prompts', 'system-prompt-autonomous.md');
440
- let systemPrompt = '';
441
- try {
442
- systemPrompt = fs.readFileSync(systemPromptPath, 'utf-8');
443
- }
444
- catch (error) {
445
- // Fallback to basic prompt if file not found
446
- systemPrompt = 'You are Centaurus, a Senior Site Reliability Engineer (SRE) and Full-Stack Architect embedded in a CLI.';
447
- }
448
- // Enhance system prompt with environment context
449
- const enhancedSystemPrompt = environmentContextInjector.getEnhancedSystemPrompt(systemPrompt);
450
- // Build messages array with enhanced system prompt at the beginning
451
- let messages = [
452
- {
453
- role: 'system',
454
- content: enhancedSystemPrompt,
455
- },
456
- ...this.conversationHistory
457
- ];
453
+ // Build messages array WITHOUT system prompt - backend will inject it
454
+ // The backend uses cli-system-prompt.md for CLI clients
455
+ // We pass environmentContext and mode separately so backend can inject them
456
+ let messages = [...this.conversationHistory];
458
457
  // Inject subshell context if in a subshell environment
459
458
  const currentContext = this.contextManager.getCurrentContext();
460
459
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
@@ -471,44 +470,61 @@ Press Enter to continue...
471
470
  let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
472
471
  let thoughtStartTime = null; // Track when thinking started
473
472
  let thoughtContent = ''; // Accumulate thought content
473
+ // ANTI-LOOP: Track duplicate tool calls to detect infinite loops
474
+ const MAX_DUPLICATE_CALLS = 2; // Max times same operation allowed on same target
475
+ const fileWriteTracker = new Map(); // Track writes per file
476
+ const recentToolCalls = [];
477
+ // ANTI-LOOP: Track ALL duplicate tool calls (not just file ops)
478
+ const toolCallTracker = new Map(); // Hash -> count
479
+ const MAX_IDENTICAL_TOOL_CALLS = 3; // Max times exact same tool call allowed
474
480
  // Create AbortController for this request
475
481
  this.currentAbortController = new AbortController();
476
482
  // Multi-turn tool execution loop
477
483
  while (turnCount < MAX_TURNS) {
478
484
  turnCount++;
479
485
  // Refresh environment context to capture any CWD changes from previous turns
486
+ // This is sent to backend which will inject it into the system prompt
480
487
  environmentContext = this.getEnvironmentContext();
481
- // Refresh system prompt with new CWD
482
- const refreshedSystemPrompt = environmentContextInjector.getEnhancedSystemPrompt(systemPrompt, this.cwd);
483
- // Update the system message in the messages array
484
- if (messages.length > 0 && messages[0].role === 'system') {
485
- messages[0].content = refreshedSystemPrompt;
486
- }
487
488
  let assistantMessage = '';
488
489
  let toolCalls = [];
490
+ // DEBUG: Log message history state before AI call
491
+ const messageStats = {
492
+ totalMessages: messages.length,
493
+ totalCharacters: messages.reduce((sum, m) => sum + (typeof m.content === 'string' ? m.content.length : 0), 0),
494
+ byRole: {
495
+ system: messages.filter(m => m.role === 'system').length,
496
+ user: messages.filter(m => m.role === 'user').length,
497
+ assistant: messages.filter(m => m.role === 'assistant').length,
498
+ tool: messages.filter(m => m.role === 'tool').length
499
+ },
500
+ assistantWithToolCalls: messages.filter(m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0).length
501
+ };
502
+ try {
503
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] === TURN ${turnCount} AI CALL ===\n`);
504
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Message history: ${messageStats.totalMessages} messages, ${messageStats.totalCharacters} chars\n`);
505
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] By role: system=${messageStats.byRole.system}, user=${messageStats.byRole.user}, assistant=${messageStats.byRole.assistant}, tool=${messageStats.byRole.tool}\n`);
506
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
507
+ }
508
+ catch (e) { }
489
509
  // Stream AI response from backend
490
510
  // Backend will inject system prompt automatically with environment context
491
511
  for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
492
512
  // Handle error chunks
493
513
  if (chunk.type === 'error') {
514
+ conversationLogger.logError('AI Stream', new Error(chunk.message));
494
515
  throw new Error(chunk.message);
495
516
  }
496
517
  // Handle thought chunks (internal reasoning)
497
518
  if (chunk.type === 'thought') {
498
- // Debug logging to file
499
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Received thought chunk: ${chunk.content.substring(0, 100)}\n`);
519
+ conversationLogger.logThoughtChunk(chunk.content);
500
520
  if (!thoughtStartTime) {
501
521
  thoughtStartTime = Date.now();
502
522
  }
503
523
  thoughtContent += chunk.content;
504
524
  // Send thought to UI callback if available
505
525
  if (this.onThoughtStreamCallback) {
506
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Calling onThoughtStreamCallback\n`);
507
526
  this.onThoughtStreamCallback(chunk.content);
508
527
  }
509
- else {
510
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] WARNING: onThoughtStreamCallback is not set!\n`);
511
- }
512
528
  continue;
513
529
  }
514
530
  // Handle text chunks
@@ -516,6 +532,7 @@ Press Enter to continue...
516
532
  // If we were thinking and now got text, finalize the thought
517
533
  if (thoughtStartTime) {
518
534
  const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
535
+ conversationLogger.logThoughtComplete(thinkingDuration);
519
536
  if (this.onThoughtCompleteCallback) {
520
537
  this.onThoughtCompleteCallback(thinkingDuration);
521
538
  }
@@ -528,6 +545,7 @@ Press Enter to continue...
528
545
  filteredContent = filteredContent.trim();
529
546
  if (filteredContent) {
530
547
  assistantMessage += filteredContent;
548
+ conversationLogger.logAITextChunk(filteredContent);
531
549
  // Send chunk to UI in real-time
532
550
  if (this.onResponseStreamCallback) {
533
551
  this.onResponseStreamCallback(filteredContent);
@@ -536,11 +554,17 @@ Press Enter to continue...
536
554
  }
537
555
  // Handle tool call chunks
538
556
  if (chunk.type === 'tool_call') {
539
- // Debug logging for tool calls
540
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Received tool_call chunk: ${JSON.stringify(chunk.toolCall?.name || 'unknown')}\n`);
557
+ const toolCall = chunk.toolCall;
558
+ // Debug: Log every tool_call chunk received
559
+ try {
560
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED: ${toolCall?.name || 'unknown'}, toolCalls.length before push: ${toolCalls.length}\n`);
561
+ }
562
+ catch (e) { }
563
+ conversationLogger.logToolCall(toolCall?.name || 'unknown', toolCall?.id || 'unknown', toolCall?.arguments || {});
541
564
  // If we were thinking and now got a tool call, finalize the thought
542
565
  if (thoughtStartTime) {
543
566
  const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
567
+ conversationLogger.logThoughtComplete(thinkingDuration);
544
568
  if (this.onThoughtCompleteCallback) {
545
569
  this.onThoughtCompleteCallback(thinkingDuration);
546
570
  }
@@ -548,34 +572,48 @@ Press Enter to continue...
548
572
  thoughtContent = '';
549
573
  }
550
574
  toolCalls.push(chunk.toolCall);
575
+ // IMMEDIATELY notify UI that a tool call was received (pending status)
576
+ // This shows tool calls in the UI as they stream in, not after the stream ends
577
+ if (this.onToolExecutionUpdate && toolCall) {
578
+ this.onToolExecutionUpdate({
579
+ toolName: toolCall.name,
580
+ status: 'pending', // 'pending' means received but not yet executing
581
+ arguments: toolCall.arguments
582
+ });
583
+ }
584
+ // Debug: Log after push
585
+ try {
586
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** TOOL_CALL PUSHED: ${toolCall?.name || 'unknown'}, toolCalls.length after push: ${toolCalls.length}\n`);
587
+ }
588
+ catch (e) { }
551
589
  }
552
590
  // Handle done chunk
553
591
  if (chunk.type === 'done') {
554
592
  // If we were thinking and stream ended, finalize the thought
555
593
  if (thoughtStartTime) {
556
594
  const thinkingDuration = Math.round((Date.now() - thoughtStartTime) / 1000);
557
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Stream done, finalizing thinking: ${thinkingDuration}s\n`);
595
+ conversationLogger.logThoughtComplete(thinkingDuration);
558
596
  if (this.onThoughtCompleteCallback) {
559
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Calling onThoughtCompleteCallback from done handler\n`);
560
597
  this.onThoughtCompleteCallback(thinkingDuration);
561
598
  }
562
- else {
563
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] WARNING: onThoughtCompleteCallback not set in done handler!\n`);
564
- }
565
599
  thoughtStartTime = null;
566
600
  thoughtContent = '';
567
601
  }
568
- else {
569
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Stream done but no thinking was in progress\n`);
570
- }
602
+ // Log AI text completion
603
+ conversationLogger.logAITextComplete();
604
+ conversationLogger.logStreamEnd('done');
571
605
  break;
572
606
  }
573
607
  } // End of stream loop
574
- // Debug logging: state after stream ends
575
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Stream ended. Turn ${turnCount}. toolCalls.length=${toolCalls.length}, assistantMessage.length=${assistantMessage.length}\n`);
576
- if (toolCalls.length > 0) {
577
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Tool calls: ${toolCalls.map(tc => tc.name).join(', ')}\n`);
578
- }
608
+ // Log loop state after stream ends
609
+ conversationLogger.logLoopState(turnCount, {
610
+ toolCallCount: toolCalls.length,
611
+ assistantMessageLength: assistantMessage.length,
612
+ hasToolCalls: toolCalls.length > 0,
613
+ willContinue: toolCalls.length > 0,
614
+ narrationAttempts,
615
+ completionAttempts,
616
+ });
579
617
  // If there are tool calls, execute them
580
618
  if (toolCalls.length > 0) {
581
619
  // CRITICAL: AI should ONLY communicate via reason_text and task_complete summary
@@ -590,16 +628,23 @@ Press Enter to continue...
590
628
  toolCalls = toolCalls.slice(0, MAX_TOOL_CALLS_PER_TURN);
591
629
  }
592
630
  const toolResults = [];
631
+ const handledToolCallIds = new Set(); // Track tool calls that were handled directly (e.g., create_plan, mark_task_complete)
593
632
  let userCancelledOperation = false;
594
633
  let taskCompleted = false;
595
634
  let taskCompleteSummary = '';
596
635
  for (let i = 0; i < toolCalls.length; i++) {
636
+ // Debug: Log which tool we're about to execute
637
+ try {
638
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** ABOUT TO EXECUTE TOOL [${i + 1}/${toolCalls.length}]: ${toolCalls[i].name}\n`);
639
+ }
640
+ catch (e) { }
597
641
  const toolCall = toolCalls[i];
598
642
  try {
599
643
  // Check if this is task_complete FIRST (before displaying anything)
600
644
  if (toolCall.name === 'task_complete') {
601
645
  taskCompleted = true;
602
646
  taskCompleteSummary = toolCall.arguments.summary || '';
647
+ conversationLogger.logTaskComplete(taskCompleteSummary);
603
648
  // CRITICAL: Suppress any text that came before task_complete in this turn
604
649
  // The AI should ONLY communicate through the task_complete summary
605
650
  assistantMessage = '';
@@ -609,19 +654,218 @@ Press Enter to continue...
609
654
  }
610
655
  // Execute the tool for proper result handling
611
656
  await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
657
+ // Clear the plan when task is complete
658
+ clearPlan();
612
659
  // Stop processing remaining tools
613
660
  break;
614
661
  }
662
+ if (toolCall.name === 'create_plan') {
663
+ // Execute the tool to create the plan
664
+ const execResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
665
+ // Extract the actual result string (toolRegistry.execute returns { success, result })
666
+ const result = execResult.success ? String(execResult.result) : `Error: ${execResult.error}`;
667
+ // Parse the PLAN_CREATED response to get the plan
668
+ if (typeof result === 'string' && result.startsWith('PLAN_CREATED:')) {
669
+ const planJson = result.substring('PLAN_CREATED:'.length);
670
+ try {
671
+ const plan = JSON.parse(planJson);
672
+ // Notify UI that a plan was created
673
+ if (this.onPlanCreated) {
674
+ this.onPlanCreated(plan);
675
+ }
676
+ // If we have approval callback, ask for approval
677
+ if (this.onPlanApprovalRequest) {
678
+ const approved = await this.onPlanApprovalRequest(plan);
679
+ if (approved) {
680
+ // Approve and activate the plan
681
+ approvePlan();
682
+ // Suppress any text output
683
+ assistantMessage = '';
684
+ // Switch out of plan mode to execution mode
685
+ this.planMode = false;
686
+ if (this.onPlanModeChange) {
687
+ this.onPlanModeChange(false);
688
+ }
689
+ // Add assistant message with plan tool call to history
690
+ this.conversationHistory.push({
691
+ role: 'assistant',
692
+ content: '',
693
+ tool_calls: [toolCall],
694
+ });
695
+ // Add plan approval response
696
+ this.conversationHistory.push({
697
+ role: 'tool',
698
+ tool_call_id: toolCall.id,
699
+ content: 'Plan approved by user. Now switching to execution mode.',
700
+ });
701
+ // Mark this tool call as handled so it's not duplicated
702
+ handledToolCallIds.add(toolCall.id);
703
+ // Add user message that includes plan context for execution phase
704
+ const planContext = getPlanContextForPrompt();
705
+ const originalRequest = this.pendingPlanRequest || message;
706
+ const executionPrompt = `${planContext}\n\nOriginal Request: ${originalRequest}\n\nProceed with task 1. After completing each task, call mark_task_complete with the task number.`;
707
+ this.conversationHistory.push({
708
+ role: 'user',
709
+ content: executionPrompt,
710
+ });
711
+ // Clear pending plan request
712
+ this.pendingPlanRequest = null;
713
+ // Update messages array for this turn
714
+ messages = [...this.conversationHistory];
715
+ // Continue the loop - AI will now execute with plan context
716
+ continue;
717
+ }
718
+ else {
719
+ // User wants to edit - stop the loop, they'll provide feedback
720
+ clearPlan();
721
+ finalAssistantMessage = 'Plan editing requested. Please provide your feedback or modifications.';
722
+ taskCompleted = true;
723
+ break;
724
+ }
725
+ }
726
+ else {
727
+ // No approval callback - add the tool result to history and wait for user response
728
+ // This ensures the AI doesn't get stuck in a silent loop
729
+ this.conversationHistory.push({
730
+ role: 'assistant',
731
+ content: '',
732
+ tool_calls: [toolCall],
733
+ });
734
+ this.conversationHistory.push({
735
+ role: 'tool',
736
+ tool_call_id: toolCall.id,
737
+ content: `Plan created: "${plan.title}" with ${plan.steps.length} tasks. Waiting for user approval.`,
738
+ });
739
+ // Mark this tool call as handled so it's not duplicated
740
+ handledToolCallIds.add(toolCall.id);
741
+ // Update messages for next iteration
742
+ messages = [...this.conversationHistory];
743
+ // Stop and wait for user to approve
744
+ finalAssistantMessage = `Plan created: ${plan.title}. Please approve to continue.`;
745
+ taskCompleted = true;
746
+ break;
747
+ }
748
+ }
749
+ catch (parseError) {
750
+ // Log error and add error result to history so AI knows
751
+ console.error('Failed to parse plan:', parseError);
752
+ // CRITICAL: Add tool result even on parse error to prevent silent loop
753
+ this.conversationHistory.push({
754
+ role: 'assistant',
755
+ content: '',
756
+ tool_calls: [toolCall],
757
+ });
758
+ this.conversationHistory.push({
759
+ role: 'tool',
760
+ tool_call_id: toolCall.id,
761
+ content: `Error parsing plan: ${parseError}. Please try again with valid plan format.`,
762
+ });
763
+ // Mark this tool call as handled so it's not duplicated
764
+ handledToolCallIds.add(toolCall.id);
765
+ messages = [...this.conversationHistory];
766
+ }
767
+ }
768
+ else {
769
+ // Tool returned non-PLAN_CREATED result - add it to history
770
+ this.conversationHistory.push({
771
+ role: 'assistant',
772
+ content: '',
773
+ tool_calls: [toolCall],
774
+ });
775
+ this.conversationHistory.push({
776
+ role: 'tool',
777
+ tool_call_id: toolCall.id,
778
+ content: result || 'create_plan executed but returned empty result.',
779
+ });
780
+ // Mark this tool call as handled so it's not duplicated
781
+ handledToolCallIds.add(toolCall.id);
782
+ messages = [...this.conversationHistory];
783
+ }
784
+ continue;
785
+ }
786
+ if (toolCall.name === 'mark_task_complete') {
787
+ // Execute the tool
788
+ const execResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
789
+ // Extract the actual result string (toolRegistry.execute returns { success, result })
790
+ const result = execResult.success ? String(execResult.result) : `Error: ${execResult.error}`;
791
+ // Parse the TASK_COMPLETED response
792
+ if (typeof result === 'string' && result.startsWith('TASK_COMPLETED:')) {
793
+ const completionJson = result.substring('TASK_COMPLETED:'.length);
794
+ try {
795
+ const completion = JSON.parse(completionJson);
796
+ const currentPlan = getCurrentPlan();
797
+ if (currentPlan && this.onTaskCompleted) {
798
+ const task = currentPlan.steps[completion.taskNumber - 1];
799
+ this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote);
800
+ }
801
+ // Notify UI about completed task
802
+ this.notifyToolStatus(toolCall.name, 'completed', toolCall.arguments, `Task ${completion.taskNumber} of ${completion.totalCount} completed: ${completion.taskDescription}`);
803
+ // Add to tool results
804
+ toolResults.push({
805
+ tool_call_id: toolCall.id,
806
+ name: toolCall.name,
807
+ result: completion.allComplete
808
+ ? 'All tasks completed! Call task_complete with a summary.'
809
+ : `Task ${completion.taskNumber} completed. Next: ${completion.nextTask}`,
810
+ });
811
+ // If all tasks are complete, prompt AI to call task_complete
812
+ if (completion.allComplete) {
813
+ toolResults[toolResults.length - 1].result =
814
+ 'All tasks in the plan are now completed! Call task_complete with a summary of what was accomplished.';
815
+ }
816
+ }
817
+ catch (parseError) {
818
+ console.error('Failed to parse task completion:', parseError);
819
+ }
820
+ }
821
+ continue;
822
+ }
823
+ // ANTI-LOOP: Only detect EXACT identical tool calls (same name + same args)
824
+ // This is a safety net - the backend fix (functionCall in messages) should prevent loops
825
+ // We use a HIGH threshold to avoid blocking legitimate multi-step operations
826
+ const toolArgsToTrack = { ...toolCall.arguments };
827
+ delete toolArgsToTrack.reason_text; // Ignore reason_text - only matters for params
828
+ const toolCallHash = `${toolCall.name}:${JSON.stringify(toolArgsToTrack)}`;
829
+ const toolCallCount = (toolCallTracker.get(toolCallHash) || 0) + 1;
830
+ toolCallTracker.set(toolCallHash, toolCallCount);
831
+ // Only stop after 5 IDENTICAL calls (same tool + same exact args)
832
+ if (toolCallCount > 5) {
833
+ // Log the loop detection
834
+ conversationLogger.logNarrationDetection('duplicate_tool_loop', {
835
+ toolName: toolCall.name,
836
+ callCount: toolCallCount,
837
+ maxAllowed: 5
838
+ });
839
+ // Force task completion with a helpful message
840
+ const loopMessage = `⚠️ **Loop Detected**: The AI called \`${toolCall.name}\` with identical parameters ${toolCallCount} times.\n\n` +
841
+ `The system has stopped to prevent an infinite loop.\n\n` +
842
+ `**Tip**: Try rephrasing your request or ask about a specific aspect of the task.`;
843
+ if (this.onResponseStreamCallback) {
844
+ this.onResponseStreamCallback(loopMessage);
845
+ }
846
+ // Set as completed and break
847
+ finalAssistantMessage = loopMessage;
848
+ taskCompleted = true;
849
+ break;
850
+ }
851
+ // NOTE: File-specific loop detection REMOVED
852
+ // The backend now includes functionCall parts in assistant messages,
853
+ // so the AI should properly remember its previous actions and not repeat them.
615
854
  // Extract and display reason_text if present (but skip for task_complete)
616
855
  const reasonText = toolCall.arguments.reason_text;
617
856
  if (reasonText && this.onResponseStreamCallback) {
618
857
  this.onResponseStreamCallback(reasonText + '\n\n');
619
858
  }
859
+ // Determine the effective CWD for this command (use remote context CWD if applicable)
860
+ const currentCtx = this.contextManager.getCurrentContext();
861
+ const effectiveCwd = currentCtx.type !== 'local'
862
+ ? currentCtx.metadata?.workingDirectory || '~'
863
+ : this.cwd;
620
864
  // Notify UI: tool is executing (send before execution starts)
621
865
  if (this.onToolExecutionUpdate) {
622
866
  // Add cwd to arguments for execute_command tool
623
867
  const toolArgs = toolCall.name === 'execute_command'
624
- ? { ...toolCall.arguments, cwd: this.cwd }
868
+ ? { ...toolCall.arguments, cwd: effectiveCwd }
625
869
  : toolCall.arguments;
626
870
  this.onToolExecutionUpdate({
627
871
  toolName: toolCall.name,
@@ -629,14 +873,18 @@ Press Enter to continue...
629
873
  arguments: toolArgs
630
874
  });
631
875
  }
876
+ // Log tool execution start
877
+ conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
632
878
  // Execute the tool (it will request approval if needed)
633
879
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
634
880
  if (result.success) {
881
+ // Log successful tool result
882
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
635
883
  // Notify UI: tool succeeded (send full result to UI)
636
884
  if (this.onToolExecutionUpdate) {
637
885
  // Add cwd to arguments for execute_command tool
638
886
  const toolArgs = toolCall.name === 'execute_command'
639
- ? { ...toolCall.arguments, cwd: this.cwd }
887
+ ? { ...toolCall.arguments, cwd: effectiveCwd }
640
888
  : toolCall.arguments;
641
889
  this.onToolExecutionUpdate({
642
890
  toolName: toolCall.name,
@@ -665,6 +913,8 @@ Press Enter to continue...
665
913
  });
666
914
  }
667
915
  else {
916
+ // Log failed tool result
917
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, result.error);
668
918
  // Check if operation was cancelled by user
669
919
  if (result.error && result.error.includes('Operation cancelled by user')) {
670
920
  userCancelledOperation = true;
@@ -673,7 +923,7 @@ Press Enter to continue...
673
923
  if (this.onToolExecutionUpdate) {
674
924
  // Add cwd to arguments for execute_command tool
675
925
  const toolArgs = toolCall.name === 'execute_command'
676
- ? { ...toolCall.arguments, cwd: this.cwd }
926
+ ? { ...toolCall.arguments, cwd: effectiveCwd }
677
927
  : toolCall.arguments;
678
928
  this.onToolExecutionUpdate({
679
929
  toolName: toolCall.name,
@@ -694,6 +944,8 @@ Press Enter to continue...
694
944
  }
695
945
  }
696
946
  catch (error) {
947
+ // Log tool execution error
948
+ conversationLogger.logError(`Tool execution: ${toolCall.name}`, error);
697
949
  // Check if operation was cancelled by user
698
950
  if (error.message && error.message.includes('Operation cancelled by user')) {
699
951
  userCancelledOperation = true;
@@ -738,14 +990,10 @@ Press Enter to continue...
738
990
  });
739
991
  // Add tool results to history
740
992
  for (const toolResult of toolResults) {
741
- const toolMessage = {
742
- tool_call_id: toolResult.tool_call_id,
743
- name: toolResult.name,
744
- result: toolResult.result,
745
- };
746
993
  this.conversationHistory.push({
747
994
  role: 'tool',
748
- content: JSON.stringify(toolMessage),
995
+ tool_call_id: toolResult.tool_call_id,
996
+ content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
749
997
  });
750
998
  }
751
999
  // Set final message indicating cancellation
@@ -758,35 +1006,42 @@ Press Enter to continue...
758
1006
  this.onResponseStreamCallback(assistantMessage);
759
1007
  }
760
1008
  // Add assistant message with tool calls to conversation history
761
- this.conversationHistory.push({
762
- role: 'assistant',
763
- content: assistantMessage || '',
764
- tool_calls: toolCalls, // Store tool calls for MaaS models
765
- });
1009
+ if (toolCalls && toolCalls.length > 0) {
1010
+ try {
1011
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Adapting assistant message: has toolCalls=${toolCalls.length}, first=${JSON.stringify(toolCalls[0])}\n`);
1012
+ }
1013
+ catch (e) { }
1014
+ }
1015
+ else {
1016
+ try {
1017
+ fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Adapting assistant message: NO toolCalls\n`);
1018
+ }
1019
+ catch (e) { }
1020
+ }
1021
+ // Filter out tool calls that were already handled directly (e.g., create_plan, mark_task_complete)
1022
+ const unhandledToolCalls = toolCalls.filter(tc => !handledToolCallIds.has(tc.id));
1023
+ // Only add assistant message if there are unhandled tool calls
1024
+ if (unhandledToolCalls.length > 0) {
1025
+ this.conversationHistory.push({
1026
+ role: 'assistant',
1027
+ content: assistantMessage || '',
1028
+ tool_calls: unhandledToolCalls, // Only include unhandled tool calls
1029
+ });
1030
+ }
766
1031
  // Add tool results to conversation history as tool messages
767
1032
  // Format: { tool_call_id, name, result: <object or string> }
768
- // Include system instruction to remind AI to continue if needed
769
1033
  for (const toolResult of toolResults) {
770
- const toolMessage = {
771
- tool_call_id: toolResult.tool_call_id,
772
- name: toolResult.name,
773
- result: toolResult.result,
774
- system_instruction: 'IMPORTANT: This tool executed successfully. If there are MORE steps needed to complete the task, continue executing the necessary tools. If the task is fully complete, call the task_complete tool with a comprehensive summary.'
775
- };
1034
+ // Add tool result to conversation history as tool message
1035
+ // IMPORTANT: tool_call_id must be a top-level property
776
1036
  this.conversationHistory.push({
777
1037
  role: 'tool',
778
- content: JSON.stringify(toolMessage),
1038
+ tool_call_id: toolResult.tool_call_id,
1039
+ content: typeof toolResult.result === 'string' ? toolResult.result : JSON.stringify(toolResult.result),
779
1040
  });
780
1041
  }
781
1042
  // Rebuild messages array with updated history
782
- // IMPORTANT: Re-add system prompt for each turn
783
- messages = [
784
- {
785
- role: 'system',
786
- content: enhancedSystemPrompt,
787
- },
788
- ...this.conversationHistory
789
- ];
1043
+ // Backend will inject system prompt
1044
+ messages = [...this.conversationHistory];
790
1045
  // Re-inject subshell context
791
1046
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
792
1047
  continue; // Loop back to AI service
@@ -798,12 +1053,17 @@ Press Enter to continue...
798
1053
  // No tool calls and no message - AI stopped silently
799
1054
  // This usually means the AI thinks it's done but didn't call task_complete
800
1055
  // Prompt it to either continue or complete
1056
+ conversationLogger.logNarrationDetection('silent_stop', {
1057
+ turn: turnCount,
1058
+ assistantMessageLength: 0,
1059
+ });
801
1060
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
802
1061
  '**This is not allowed.** You must either:\n' +
803
1062
  '1. Execute a tool call if more work is needed, OR\n' +
804
1063
  '2. Call task_complete() with a summary of what you accomplished\n\n' +
805
1064
  '**If you have completed the task**, call task_complete() NOW with a comprehensive summary.\n' +
806
1065
  '**If more work is needed**, execute the next tool call immediately.';
1066
+ conversationLogger.logSystemPrompt('silent_stop_prompt', silentStopPrompt);
807
1067
  this.conversationHistory.push({
808
1068
  role: 'user',
809
1069
  content: silentStopPrompt,
@@ -814,9 +1074,19 @@ Press Enter to continue...
814
1074
  const isNarration = /\b(I will|I'll|Let me|Let's|I need to|I'm going to|I should|I can)\b/i.test(assistantMessage);
815
1075
  if (isNarration) {
816
1076
  narrationAttempts++;
1077
+ conversationLogger.logNarrationDetection('narration', {
1078
+ turn: turnCount,
1079
+ narrationAttempts,
1080
+ maxAttempts: MAX_NARRATION_ATTEMPTS,
1081
+ messagePreview: assistantMessage.substring(0, 200),
1082
+ });
817
1083
  // If AI keeps narrating without executing, force completion immediately
818
1084
  if (narrationAttempts >= MAX_NARRATION_ATTEMPTS) {
819
1085
  // Force task completion with error message
1086
+ conversationLogger.logNarrationDetection('narration', {
1087
+ action: 'forced_completion',
1088
+ reason: 'max_narration_attempts_reached',
1089
+ });
820
1090
  finalAssistantMessage = '⚠️ **Task Incomplete**: The AI repeatedly described actions without executing them.\n\n' +
821
1091
  '**What happened**: The AI entered a narration loop, describing what it wanted to do instead of using tool calls.\n\n' +
822
1092
  '**Suggestions**:\n' +
@@ -871,9 +1141,18 @@ Press Enter to continue...
871
1141
  const isFinalAnswer = assistantMessage.length > 20;
872
1142
  if (isFinalAnswer) {
873
1143
  completionAttempts++;
1144
+ conversationLogger.logNarrationDetection('final_answer', {
1145
+ turn: turnCount,
1146
+ completionAttempts,
1147
+ messagePreview: assistantMessage.substring(0, 200),
1148
+ });
874
1149
  // If AI keeps providing text summaries without calling task_complete, accept the text and finish
875
1150
  // This prevents the infinite loop where the AI keeps summarizing in response to our prompt
876
1151
  if (completionAttempts > 1) {
1152
+ conversationLogger.logNarrationDetection('final_answer', {
1153
+ action: 'accepting_text_as_final',
1154
+ reason: 'multiple_completion_attempts',
1155
+ });
877
1156
  finalAssistantMessage = assistantMessage;
878
1157
  break;
879
1158
  }
@@ -898,7 +1177,8 @@ Press Enter to continue...
898
1177
  }
899
1178
  }
900
1179
  }
901
- // Rebuild messages array
1180
+ // Rebuild messages array with updated history
1181
+ // Backend will inject system prompt
902
1182
  messages = [...this.conversationHistory];
903
1183
  // Re-inject subshell context
904
1184
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
@@ -918,7 +1198,8 @@ Press Enter to continue...
918
1198
  role: 'user',
919
1199
  content: silentStopPrompt,
920
1200
  });
921
- // Rebuild messages array
1201
+ // Rebuild messages array with updated history
1202
+ // Backend will inject system prompt
922
1203
  messages = [...this.conversationHistory];
923
1204
  // Re-inject subshell context
924
1205
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
@@ -953,32 +1234,19 @@ Press Enter to continue...
953
1234
  // Save assistant message to backend
954
1235
  await this.saveMessageToBackend('assistant', finalMessage);
955
1236
  } // End of while loop
956
- // Parse response for plan mode
957
- if (this.planMode) {
958
- const planData = this.parsePlanResponse(finalAssistantMessage);
959
- if (planData && this.onPlanApprovalRequest) {
960
- // Ask user for approval
961
- const approved = await this.onPlanApprovalRequest(planData);
962
- if (approved) {
963
- // Switch to execution mode
964
- this.planMode = false;
965
- if (this.onPlanModeChange) {
966
- this.onPlanModeChange(false);
967
- }
968
- // Send message to start execution
969
- await this.handleMessage('Let\'s implement the plan we just discussed. Start with the first task.');
970
- return;
971
- }
972
- }
973
- }
1237
+ // Log session end
1238
+ conversationLogger.endSession(finalAssistantMessage, turnCount);
974
1239
  // Send response back to UI (only if there's a message)
975
1240
  if (this.onResponseCallback && finalAssistantMessage) {
976
1241
  this.onResponseCallback(finalAssistantMessage);
977
1242
  }
978
1243
  }
979
1244
  catch (error) {
1245
+ // Log the error
1246
+ conversationLogger.logError('handleMessage', error);
980
1247
  // Check if this was an abort/cancellation
981
1248
  if (error.name === 'AbortError' || error.message?.includes('aborted')) {
1249
+ conversationLogger.logError('handleMessage', new Error('Request cancelled by user'));
982
1250
  if (this.onResponseCallback) {
983
1251
  this.onResponseCallback('⚠️ Request cancelled by user.');
984
1252
  }
@@ -1005,16 +1273,20 @@ Press Enter to continue...
1005
1273
  `/config - View current configuration\n` +
1006
1274
  `/model - Select from available Google models\n` +
1007
1275
  `/plan - Toggle plan mode for complex implementations\n` +
1276
+ `/mcp - Manage configured MCP servers and tools\n` +
1277
+ `/docs - Open Centaurus documentation in browser\n` +
1008
1278
  `/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
1009
1279
  `/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
1010
1280
  `/sign-in - Sign in with Google (if not already signed in)\n` +
1011
1281
  `/logout - Sign out, clear session, and exit CLI\n` +
1012
1282
  `/exit - Exit the application\n\n` +
1013
1283
  `Keyboard Shortcuts:\n\n` +
1014
- `Ctrl+D - Toggle command mode (execute terminal commands directly)\n` +
1015
- `Tab - Autocomplete files/directories (in command mode)\n` +
1284
+ `Ctrl+D - Cycle modes (Agent Terminal → Auto)\n` +
1016
1285
  `Ctrl+T - Toggle auto-accept mode\n` +
1017
- `? - Show keyboard shortcuts help`;
1286
+ `Ctrl+C - Cancel operation / Exit (press twice)\n` +
1287
+ `Tab - Autocomplete files/directories (in command mode)\n` +
1288
+ `Ctrl+Z - Undo last input change\n` +
1289
+ `Ctrl+A - Select all text`;
1018
1290
  break;
1019
1291
  case 'init':
1020
1292
  try {
@@ -1402,6 +1674,22 @@ Start by listing the directory structure to understand what you're working with.
1402
1674
  responseMessage = '❌ MCP is not initialized. Please restart the CLI.';
1403
1675
  }
1404
1676
  break;
1677
+ case 'docs':
1678
+ // Open documentation URL in default browser
1679
+ const docsUrl = 'https://centaurus-web.vercel.app/docs';
1680
+ const { exec } = await import('child_process');
1681
+ const platform = process.platform;
1682
+ if (platform === 'win32') {
1683
+ exec(`start "" "${docsUrl}"`);
1684
+ }
1685
+ else if (platform === 'darwin') {
1686
+ exec(`open "${docsUrl}"`);
1687
+ }
1688
+ else {
1689
+ exec(`xdg-open "${docsUrl}"`);
1690
+ }
1691
+ responseMessage = `📖 Opening documentation in your browser...\n\n${docsUrl}`;
1692
+ break;
1405
1693
  case 'exit':
1406
1694
  process.exit(0);
1407
1695
  break;
@@ -1442,25 +1730,41 @@ Start by listing the directory structure to understand what you're working with.
1442
1730
  return 'default';
1443
1731
  }
1444
1732
  getPlanModeInstructions() {
1445
- return `\n\n## PLAN MODE ACTIVE\n\nYou are currently in PLAN MODE. In this mode, you should:\n\n1. **Explore the current directory** using list_directory and read_file tools to understand the codebase structure\n2. **Research the topic** using the web_search tool to gather best practices and implementation approaches\n3. **Create a detailed implementation plan** with ordered tasks\n\nWhen you've completed your planning, you MUST format your response in this EXACT format:\n\n<tasks>\n1. First task description\n2. Second task description\n3. Third task description\n...\n</tasks>\n\n<question>\nShall I proceed with implementing this plan?\n</question>\n\nIMPORTANT:\n- The <tasks> section must contain a numbered list of tasks in order\n- The <question> section must contain a yes/no question asking if you should proceed\n- Do NOT execute any implementation tasks in plan mode - only create the plan\n- Use tools to explore and research, but do not modify any files`;
1446
- }
1447
- parsePlanResponse(response) {
1448
- // Extract tasks
1449
- const tasksMatch = response.match(/<tasks>([\s\S]*?)<\/tasks>/);
1450
- if (!tasksMatch)
1451
- return null;
1452
- const tasksText = tasksMatch[1].trim();
1453
- const taskLines = tasksText.split('\n').filter(line => line.trim());
1454
- const tasks = taskLines.map(line => {
1455
- // Remove numbering (e.g., "1. ", "2. ")
1456
- return line.replace(/^\d+\.\s*/, '').trim();
1457
- }).filter(task => task);
1458
- // Extract question
1459
- const questionMatch = response.match(/<question>([\s\S]*?)<\/question>/);
1460
- if (!questionMatch)
1461
- return null;
1462
- const question = questionMatch[1].trim();
1463
- return { tasks, question };
1733
+ return `\n\n## PLAN MODE ACTIVE (MODE=plan)
1734
+
1735
+ You are currently in PLAN MODE. In this mode, you MUST:
1736
+
1737
+ 1. **DO NOT execute any implementation tools** (no write_to_file, edit_file, execute_command, etc.)
1738
+ 2. **Call the \`create_plan\` tool** to present a structured plan to the user FIRST
1739
+
1740
+ ### How to Create a Plan:
1741
+
1742
+ Analyze the user's request, then call \`create_plan\` with:
1743
+ - A clear title describing what will be accomplished
1744
+ - A brief summary of the approach
1745
+ - An ordered list of specific, actionable tasks
1746
+
1747
+ Example:
1748
+ \`\`\`
1749
+ create_plan(
1750
+ title: "Create Python CSV Filter Script",
1751
+ summary: "Build a Python script that reads a CSV file, filters rows based on criteria, and writes output",
1752
+ tasks: [
1753
+ { description: "Create sample input.csv with test data", complexity: "low" },
1754
+ { description: "Write csv_filter.py with read/filter/write logic", complexity: "medium" },
1755
+ { description: "Execute and verify the script works correctly", complexity: "low" }
1756
+ ]
1757
+ )
1758
+ \`\`\`
1759
+
1760
+ ### After Plan Approval:
1761
+
1762
+ Once the user approves the plan:
1763
+ 1. Execute each task in order
1764
+ 2. After completing each task, call \`mark_task_complete(task_number: N)\`
1765
+ 3. After all tasks are done, call \`task_complete\` with a summary
1766
+
1767
+ **CRITICAL: In plan mode, ALWAYS call create_plan FIRST before any other tools.**`;
1464
1768
  }
1465
1769
  /**
1466
1770
  * Toggle command mode on/off
@@ -1510,32 +1814,18 @@ Start by listing the directory structure to understand what you're working with.
1510
1814
  // These need full terminal control
1511
1815
  const currentContextForEditor = this.contextManager.getCurrentContext();
1512
1816
  if (isInteractiveEditorCommand(command)) {
1513
- if (currentContextForEditor.type === 'local' && this.onInteractiveEditorMode) {
1514
- // Local context: delegate to App for full terminal takeover
1515
- this.onInteractiveEditorMode(true, command, this.cwd);
1516
- return;
1517
- }
1518
- else if (currentContextForEditor.type !== 'local') {
1519
- // Remote context (SSH, WSL, Docker)
1520
- if (isNanoEditor(command) && this.onInteractiveEditorMode) {
1521
- // Nano is supported in remote contexts - pass remote context to App
1522
- this.onInteractiveEditorMode(true, command, this.cwd, currentContextForEditor);
1523
- return;
1817
+ if (this.onInteractiveEditorMode) {
1818
+ if (currentContextForEditor.type === 'local') {
1819
+ // Local context: delegate to App for full terminal takeover
1820
+ this.onInteractiveEditorMode(true, command, this.cwd);
1524
1821
  }
1525
1822
  else {
1526
- // Other editors show "coming soon" message
1527
- const editorName = getEditorName(command);
1528
- if (this.onResponseCallback) {
1529
- this.onResponseCallback(`ℹ️ ${editorName} integration coming soon!\n\n` +
1530
- `💡 Currently supported: nano\n` +
1531
- ` Try: nano <filename>\n\n` +
1532
- `📋 Alternatives:\n` +
1533
- ` • Type 'exit' to return to local mode, then run the editor\n` +
1534
- ` • Use 'code <filename>' to open in VS Code\n` +
1535
- ` • Use 'cat' or 'less' to view files`);
1536
- }
1537
- return;
1823
+ // Remote context (SSH, WSL, Docker) - pass remote context to App
1824
+ // Use the remote context's working directory, not the local Windows CWD
1825
+ const remoteCwd = currentContextForEditor.metadata?.workingDirectory || '~';
1826
+ this.onInteractiveEditorMode(true, command, remoteCwd, currentContextForEditor);
1538
1827
  }
1828
+ return;
1539
1829
  }
1540
1830
  }
1541
1831
  try {
@@ -1627,15 +1917,19 @@ Start by listing the directory structure to understand what you're working with.
1627
1917
  return;
1628
1918
  }
1629
1919
  }
1920
+ // Get current context to determine correct CWD for notification
1921
+ const currentContext = this.contextManager.getCurrentContext();
1922
+ const effectiveCwd = currentContext.type !== 'local'
1923
+ ? currentContext.metadata?.workingDirectory || '~'
1924
+ : this.cwd;
1630
1925
  // Notify UI that command is executing
1631
1926
  if (this.onToolExecutionUpdate) {
1632
1927
  this.onToolExecutionUpdate({
1633
1928
  toolName: 'execute_command',
1634
1929
  status: 'executing',
1635
- arguments: { command, cwd: this.cwd, isPty: shellUtils.isPtyAvailable() }
1930
+ arguments: { command, cwd: effectiveCwd, isPty: shellUtils.isPtyAvailable() }
1636
1931
  });
1637
1932
  }
1638
- const currentContext = this.contextManager.getCurrentContext();
1639
1933
  // Execute with streaming support for local commands
1640
1934
  if (currentContext.type === 'local') {
1641
1935
  // Use interactive execution to support stdin
@@ -1673,38 +1967,161 @@ Start by listing the directory structure to understand what you're working with.
1673
1967
  });
1674
1968
  });
1675
1969
  }
1676
- else {
1677
- // Subshell execution (no streaming yet)
1678
- const result = await this.contextManager.executeCommand(command);
1970
+ else if (currentContext.type === 'wsl') {
1971
+ // WSL execution with PTY for proper TTY handling (sudo, etc.)
1972
+ const remoteCwd = currentContext.metadata?.workingDirectory || '~';
1973
+ const distribution = currentContext.metadata?.distroName || 'Ubuntu';
1679
1974
  let output = '';
1680
- if (result.stdout && result.stdout.trim()) {
1681
- output += result.stdout;
1682
- }
1683
- if (result.stderr && result.stderr.trim()) {
1684
- if (output)
1685
- output += '\n';
1686
- output += result.stderr;
1687
- }
1688
- // Notify UI of completion
1689
- if (this.onToolExecutionUpdate) {
1690
- if (result.exitCode !== 0) {
1691
- this.onToolExecutionUpdate({
1692
- toolName: 'execute_command',
1693
- status: 'error',
1694
- result: output,
1695
- error: `Exit Code: ${result.exitCode}`,
1696
- arguments: { command, cwd: this.cwd }
1697
- });
1698
- }
1699
- else {
1700
- this.onToolExecutionUpdate({
1701
- toolName: 'execute_command',
1702
- status: 'completed',
1703
- result: output || 'Command executed successfully',
1704
- arguments: { command, cwd: this.cwd }
1705
- });
1706
- }
1975
+ await new Promise((resolve) => {
1976
+ const wslPty = runWSLCommand(distribution, command, remoteCwd, (data) => {
1977
+ // Stream output to UI
1978
+ output += data;
1979
+ if (this.onToolStreamingOutput) {
1980
+ this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
1981
+ }
1982
+ }, (exitCode) => {
1983
+ // Notify UI of completion
1984
+ if (this.onToolExecutionUpdate) {
1985
+ if (exitCode !== 0) {
1986
+ this.onToolExecutionUpdate({
1987
+ toolName: 'execute_command',
1988
+ status: 'error',
1989
+ result: output,
1990
+ error: `Exit Code: ${exitCode}`,
1991
+ arguments: { command, cwd: remoteCwd }
1992
+ });
1993
+ }
1994
+ else {
1995
+ this.onToolExecutionUpdate({
1996
+ toolName: 'execute_command',
1997
+ status: 'completed',
1998
+ result: output || 'Command executed successfully',
1999
+ arguments: { command, cwd: remoteCwd }
2000
+ });
2001
+ }
2002
+ }
2003
+ this.currentInteractiveProcess = undefined;
2004
+ resolve();
2005
+ });
2006
+ // Set up interactive process for stdin
2007
+ this.currentInteractiveProcess = {
2008
+ process: null,
2009
+ write: (data) => wslPty.write(data),
2010
+ kill: () => wslPty.kill(),
2011
+ signal: (sig) => {
2012
+ if (sig === 'SIGINT') {
2013
+ wslPty.write('\x03'); // Ctrl+C
2014
+ }
2015
+ },
2016
+ resize: (cols, rows) => wslPty.resize(cols, rows),
2017
+ isPty: true
2018
+ };
2019
+ });
2020
+ }
2021
+ else if (currentContext.type === 'docker') {
2022
+ // Docker execution with PTY for proper TTY handling
2023
+ const remoteCwd = currentContext.metadata?.workingDirectory || '~';
2024
+ const containerId = currentContext.metadata?.containerId || '';
2025
+ let output = '';
2026
+ await new Promise((resolve) => {
2027
+ const dockerPty = runDockerCommand(containerId, command, remoteCwd, (data) => {
2028
+ // Stream output to UI
2029
+ output += data;
2030
+ if (this.onToolStreamingOutput) {
2031
+ this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
2032
+ }
2033
+ }, (exitCode) => {
2034
+ // Notify UI of completion
2035
+ if (this.onToolExecutionUpdate) {
2036
+ if (exitCode !== 0) {
2037
+ this.onToolExecutionUpdate({
2038
+ toolName: 'execute_command',
2039
+ status: 'error',
2040
+ result: output,
2041
+ error: `Exit Code: ${exitCode}`,
2042
+ arguments: { command, cwd: remoteCwd }
2043
+ });
2044
+ }
2045
+ else {
2046
+ this.onToolExecutionUpdate({
2047
+ toolName: 'execute_command',
2048
+ status: 'completed',
2049
+ result: output || 'Command executed successfully',
2050
+ arguments: { command, cwd: remoteCwd }
2051
+ });
2052
+ }
2053
+ }
2054
+ this.currentInteractiveProcess = undefined;
2055
+ resolve();
2056
+ });
2057
+ // Set up interactive process for stdin
2058
+ this.currentInteractiveProcess = {
2059
+ process: null,
2060
+ write: (data) => dockerPty.write(data),
2061
+ kill: () => dockerPty.kill(),
2062
+ signal: (sig) => {
2063
+ if (sig === 'SIGINT') {
2064
+ dockerPty.write('\x03'); // Ctrl+C
2065
+ }
2066
+ },
2067
+ resize: (cols, rows) => dockerPty.resize(cols, rows),
2068
+ isPty: true
2069
+ };
2070
+ });
2071
+ }
2072
+ else if (currentContext.type === 'ssh') {
2073
+ // SSH execution with PTY for proper TTY handling
2074
+ const remoteCwd = currentContext.metadata?.workingDirectory || '~';
2075
+ const sshClient = currentContext.handler?.client;
2076
+ if (!sshClient) {
2077
+ throw new Error('SSH client not available');
1707
2078
  }
2079
+ let output = '';
2080
+ await new Promise((resolve) => {
2081
+ const sshPty = runSSHCommand(sshClient, command, remoteCwd, (data) => {
2082
+ // Stream output to UI
2083
+ output += data;
2084
+ if (this.onToolStreamingOutput) {
2085
+ this.onToolStreamingOutput({ toolName: 'execute_command', chunk: data, type: 'stdout' });
2086
+ }
2087
+ }, (exitCode) => {
2088
+ // Notify UI of completion
2089
+ if (this.onToolExecutionUpdate) {
2090
+ if (exitCode !== 0) {
2091
+ this.onToolExecutionUpdate({
2092
+ toolName: 'execute_command',
2093
+ status: 'error',
2094
+ result: output,
2095
+ error: `Exit Code: ${exitCode}`,
2096
+ arguments: { command, cwd: remoteCwd }
2097
+ });
2098
+ }
2099
+ else {
2100
+ this.onToolExecutionUpdate({
2101
+ toolName: 'execute_command',
2102
+ status: 'completed',
2103
+ result: output || 'Command executed successfully',
2104
+ arguments: { command, cwd: remoteCwd }
2105
+ });
2106
+ }
2107
+ }
2108
+ this.currentInteractiveProcess = undefined;
2109
+ resolve();
2110
+ });
2111
+ // Set up interactive process for stdin
2112
+ this.currentInteractiveProcess = {
2113
+ process: null,
2114
+ write: (data) => sshPty.write(data),
2115
+ kill: () => sshPty.kill(),
2116
+ signal: (sig) => {
2117
+ if (sig === 'SIGINT') {
2118
+ sshPty.write('\x03'); // Ctrl+C
2119
+ }
2120
+ },
2121
+ resize: (cols, rows) => sshPty.resize(cols, rows),
2122
+ isPty: true
2123
+ };
2124
+ });
1708
2125
  }
1709
2126
  }
1710
2127
  catch (error) {