centaurus-cli 2.8.6 → 2.8.8

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 (128) hide show
  1. package/dist/cli-adapter.d.ts +85 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +773 -31
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  6. package/dist/config/mcp-config-manager.js +9 -8
  7. package/dist/config/mcp-config-manager.js.map +1 -1
  8. package/dist/config/slash-commands.d.ts +2 -0
  9. package/dist/config/slash-commands.d.ts.map +1 -1
  10. package/dist/config/slash-commands.js +31 -1
  11. package/dist/config/slash-commands.js.map +1 -1
  12. package/dist/context/handlers/docker-handler.js.map +1 -1
  13. package/dist/context/handlers/ssh-handler.d.ts +16 -1
  14. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  15. package/dist/context/handlers/ssh-handler.js +57 -12
  16. package/dist/context/handlers/ssh-handler.js.map +1 -1
  17. package/dist/context/subshell-handler.d.ts +14 -0
  18. package/dist/context/subshell-handler.d.ts.map +1 -1
  19. package/dist/hooks/useTerminalDimensions.d.ts +41 -0
  20. package/dist/hooks/useTerminalDimensions.d.ts.map +1 -0
  21. package/dist/hooks/useTerminalDimensions.js +84 -0
  22. package/dist/hooks/useTerminalDimensions.js.map +1 -0
  23. package/dist/index.js +57 -5
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  26. package/dist/mcp/mcp-command-handler.js +3 -1
  27. package/dist/mcp/mcp-command-handler.js.map +1 -1
  28. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  29. package/dist/mcp/mcp-server-manager.js +5 -3
  30. package/dist/mcp/mcp-server-manager.js.map +1 -1
  31. package/dist/services/api-client.d.ts +24 -0
  32. package/dist/services/api-client.d.ts.map +1 -1
  33. package/dist/services/api-client.js +27 -0
  34. package/dist/services/api-client.js.map +1 -1
  35. package/dist/services/auth-handler.js +1 -1
  36. package/dist/services/auth-handler.js.map +1 -1
  37. package/dist/services/clipboard-service.d.ts +42 -0
  38. package/dist/services/clipboard-service.d.ts.map +1 -0
  39. package/dist/services/clipboard-service.js +217 -0
  40. package/dist/services/clipboard-service.js.map +1 -0
  41. package/dist/services/local-chat-storage.d.ts +154 -0
  42. package/dist/services/local-chat-storage.d.ts.map +1 -0
  43. package/dist/services/local-chat-storage.js +258 -0
  44. package/dist/services/local-chat-storage.js.map +1 -0
  45. package/dist/tools/grep-search.d.ts +5 -0
  46. package/dist/tools/grep-search.d.ts.map +1 -1
  47. package/dist/tools/grep-search.js +68 -16
  48. package/dist/tools/grep-search.js.map +1 -1
  49. package/dist/tools/plan-mode.d.ts +57 -6
  50. package/dist/tools/plan-mode.d.ts.map +1 -1
  51. package/dist/tools/plan-mode.js +297 -46
  52. package/dist/tools/plan-mode.js.map +1 -1
  53. package/dist/tools/read-binary-file.d.ts +10 -0
  54. package/dist/tools/read-binary-file.d.ts.map +1 -0
  55. package/dist/tools/read-binary-file.js +210 -0
  56. package/dist/tools/read-binary-file.js.map +1 -0
  57. package/dist/types/index.d.ts +7 -1
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/ui/components/App.d.ts +35 -0
  60. package/dist/ui/components/App.d.ts.map +1 -1
  61. package/dist/ui/components/App.js +622 -43
  62. package/dist/ui/components/App.js.map +1 -1
  63. package/dist/ui/components/ClipboardImageAutocomplete.d.ts +14 -0
  64. package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +1 -0
  65. package/dist/ui/components/ClipboardImageAutocomplete.js +39 -0
  66. package/dist/ui/components/ClipboardImageAutocomplete.js.map +1 -0
  67. package/dist/ui/components/ConnectionStatusMessage.d.ts +1 -1
  68. package/dist/ui/components/ConnectionStatusMessage.d.ts.map +1 -1
  69. package/dist/ui/components/ConnectionStatusMessage.js +21 -0
  70. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  71. package/dist/ui/components/DetailedPlanReviewScreen.d.ts +17 -0
  72. package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +1 -0
  73. package/dist/ui/components/DetailedPlanReviewScreen.js +110 -0
  74. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -0
  75. package/dist/ui/components/InputBox.d.ts +4 -1
  76. package/dist/ui/components/InputBox.d.ts.map +1 -1
  77. package/dist/ui/components/InputBox.js +419 -30
  78. package/dist/ui/components/InputBox.js.map +1 -1
  79. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  80. package/dist/ui/components/InteractiveShell.js +20 -6
  81. package/dist/ui/components/InteractiveShell.js.map +1 -1
  82. package/dist/ui/components/MessageDisplay.d.ts +6 -0
  83. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  84. package/dist/ui/components/MessageDisplay.js +66 -3
  85. package/dist/ui/components/MessageDisplay.js.map +1 -1
  86. package/dist/ui/components/PlanAcceptedMessage.d.ts +8 -0
  87. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -1
  88. package/dist/ui/components/PlanAcceptedMessage.js +26 -8
  89. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  90. package/dist/ui/components/StreamingMessageDisplay.d.ts +3 -0
  91. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  92. package/dist/ui/components/StreamingMessageDisplay.js +10 -6
  93. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  94. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -1
  95. package/dist/ui/components/TaskCompletedMessage.js +4 -4
  96. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  97. package/dist/ui/components/TaskProgressIndicator.d.ts +18 -0
  98. package/dist/ui/components/TaskProgressIndicator.d.ts.map +1 -0
  99. package/dist/ui/components/TaskProgressIndicator.js +72 -0
  100. package/dist/ui/components/TaskProgressIndicator.js.map +1 -0
  101. package/dist/ui/components/ThinkingDisplay.d.ts +3 -0
  102. package/dist/ui/components/ThinkingDisplay.d.ts.map +1 -1
  103. package/dist/ui/components/ThinkingDisplay.js +6 -4
  104. package/dist/ui/components/ThinkingDisplay.js.map +1 -1
  105. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  106. package/dist/ui/components/ToolExecutionMessage.js +85 -15
  107. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  108. package/dist/ui/components/VersionUpdatePrompt.d.ts +1 -2
  109. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  110. package/dist/ui/components/VersionUpdatePrompt.js +108 -27
  111. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  112. package/dist/utils/custom-commands-manager.d.ts +59 -0
  113. package/dist/utils/custom-commands-manager.d.ts.map +1 -0
  114. package/dist/utils/custom-commands-manager.js +142 -0
  115. package/dist/utils/custom-commands-manager.js.map +1 -0
  116. package/dist/utils/input-classifier.d.ts +10 -11
  117. package/dist/utils/input-classifier.d.ts.map +1 -1
  118. package/dist/utils/input-classifier.js +299 -75
  119. package/dist/utils/input-classifier.js.map +1 -1
  120. package/dist/utils/terminal-output.d.ts.map +1 -1
  121. package/dist/utils/terminal-output.js +110 -14
  122. package/dist/utils/terminal-output.js.map +1 -1
  123. package/dist/utils/unicode-sanitizer.d.ts +44 -0
  124. package/dist/utils/unicode-sanitizer.d.ts.map +1 -0
  125. package/dist/utils/unicode-sanitizer.js +211 -0
  126. package/dist/utils/unicode-sanitizer.js.map +1 -0
  127. package/models-config.json +2 -3
  128. package/package.json +4 -1
@@ -13,9 +13,10 @@ 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 { createPlanTool, markTaskCompleteTool, getCurrentPlan, clearPlan, approvePlan, getPlanContextForPrompt } from './tools/plan-mode.js';
16
+ import { createPlanTool, markTaskCompleteTool, getCurrentPlan, clearPlan, approvePlan, getPhaseContextForPrompt, getCurrentPhase } 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
+ import { readBinaryFileTool } from './tools/read-binary-file.js';
19
20
  import { apiClient } from './services/api-client.js';
20
21
  import { conversationManager } from './services/conversation-manager.js';
21
22
  import { aiServiceClient } from './services/ai-service-client.js';
@@ -32,6 +33,8 @@ import { MCPServerManager } from './mcp/mcp-server-manager.js';
32
33
  import { MCPCommandHandler } from './mcp/mcp-command-handler.js';
33
34
  import { isInteractiveEditorCommand, runWSLCommand, runDockerCommand, runSSHCommand } from './utils/editor-utils.js';
34
35
  import { conversationLogger } from './utils/conversation-logger.js';
36
+ import { localChatStorage } from './services/local-chat-storage.js';
37
+ import { logWarning } from './utils/logger.js';
35
38
  export class CentaurusCLI {
36
39
  configManager;
37
40
  toolRegistry;
@@ -69,6 +72,15 @@ export class CentaurusCLI {
69
72
  mcpCommandHandler;
70
73
  onInteractiveEditorMode;
71
74
  onConnectionStatusUpdate;
75
+ currentChatId = null;
76
+ onShowChatPickerCallback;
77
+ onShowChatDeletePickerCallback;
78
+ onShowChatListCallback;
79
+ onShowChatRenamePickerCallback;
80
+ onRestoreMessagesCallback;
81
+ uiMessageHistory = []; // Mirror of App.tsx's messageHistory for saving
82
+ localCwdBeforeRemote = null; // Track local CWD before entering remote session
83
+ lastConnectionCommand = null; // Track the command used to connect to remote
72
84
  constructor() {
73
85
  this.configManager = new ConfigManager();
74
86
  this.toolRegistry = new ToolRegistry();
@@ -163,7 +175,7 @@ export class CentaurusCLI {
163
175
  await this.mcpCommandHandler.initializeMCP();
164
176
  }
165
177
  catch (error) {
166
- console.error('Failed to initialize MCP:', error);
178
+ logWarning(`Failed to initialize MCP: ${error?.message || error}`);
167
179
  }
168
180
  }
169
181
  writeToShellStdin(input) {
@@ -209,6 +221,12 @@ export class CentaurusCLI {
209
221
  getCurrentSubshellContext() {
210
222
  return this.contextManager.getCurrentContext();
211
223
  }
224
+ /**
225
+ * Get the current conversation ID for file uploads
226
+ */
227
+ getCurrentConversationId() {
228
+ return conversationManager.getCurrentConversationId();
229
+ }
212
230
  async handlePickerSelection(selection, pickerType) {
213
231
  try {
214
232
  // Selection is the index of the model in ALL_MODELS array
@@ -316,6 +334,7 @@ export class CentaurusCLI {
316
334
  this.toolRegistry.register(webSearchTool);
317
335
  this.toolRegistry.register(fetchUrlTool);
318
336
  this.toolRegistry.register(taskCompleteTool);
337
+ this.toolRegistry.register(readBinaryFileTool);
319
338
  // Load configuration
320
339
  const config = this.configManager.load();
321
340
  // Enable backend sync if authenticated
@@ -382,6 +401,12 @@ Press Enter to continue...
382
401
  if (this.conversationStarted || !apiClient.isAuthenticated()) {
383
402
  return;
384
403
  }
404
+ // Check if conversationManager already has a conversation (e.g., created by App.tsx for image upload)
405
+ // This prevents duplicate conversation creation
406
+ if (conversationManager.getCurrentConversationId()) {
407
+ this.conversationStarted = true;
408
+ return;
409
+ }
385
410
  try {
386
411
  const config = this.configManager.load();
387
412
  const title = `Conversation ${new Date().toLocaleString()}`;
@@ -492,12 +517,32 @@ Press Enter to continue...
492
517
  if (this.planMode && !this.pendingPlanRequest) {
493
518
  this.pendingPlanRequest = message;
494
519
  }
520
+ // Build the user message content - inject plan mode instructions if active
521
+ let userMessageContent = message;
522
+ // When plan mode is active, explicitly inject planning instructions into the user message
523
+ // This ensures the AI creates a plan even when toggled mid-conversation
524
+ if (this.planMode && !getCurrentPlan()?.isActive) {
525
+ const planModePrefix = `[PLAN MODE ACTIVE - You MUST call create_plan first before any implementation tools]
526
+
527
+ User Request: ${message}
528
+
529
+ CRITICAL INSTRUCTIONS:
530
+ 1. You are in PLANNING MODE - DO NOT implement anything directly
531
+ 2. First explore the codebase using view_file, list_dir, grep_search to understand the context
532
+ 3. Then call create_plan with a detailed plan including:
533
+ - designSummary: Your understanding of the codebase
534
+ - tasks: Hierarchical tasks with subtasks
535
+ 4. Wait for user approval before implementing
536
+
537
+ DO NOT use write_to_file, edit_file, or execute_command until the plan is approved.`;
538
+ userMessageContent = planModePrefix;
539
+ }
495
540
  // Add user message to history
496
541
  this.conversationHistory.push({
497
542
  role: 'user',
498
- content: message,
543
+ content: userMessageContent,
499
544
  });
500
- // Save user message to backend
545
+ // Save user message to backend (save original message for display, not the prefixed version)
501
546
  await this.saveMessageToBackend('user', message);
502
547
  // Start logging session and log user message
503
548
  conversationLogger.startSession();
@@ -712,7 +757,10 @@ Press Enter to continue...
712
757
  if (toolCalls.length > 0) {
713
758
  // CRITICAL: AI should ONLY communicate via reason_text and task_complete summary
714
759
  // Any text output alongside tool calls should be suppressed
715
- if (assistantMessage && assistantMessage.trim()) {
760
+ // EXCEPTION: If task_complete is in the tool calls, preserve assistantMessage
761
+ // because task_complete can have an empty summary and rely on streamed text
762
+ const hasTaskComplete = toolCalls.some(tc => tc.name === 'task_complete');
763
+ if (assistantMessage && assistantMessage.trim() && !hasTaskComplete) {
716
764
  // Suppress text output - AI should only use reason_text
717
765
  assistantMessage = ''; // Clear ALL text output - AI should only use reason_text
718
766
  }
@@ -739,13 +787,17 @@ Press Enter to continue...
739
787
  taskCompleted = true;
740
788
  taskCompleteSummary = toolCall.arguments.summary || '';
741
789
  conversationLogger.logTaskComplete(taskCompleteSummary);
742
- // CRITICAL: Suppress any text that came before task_complete in this turn
743
- // The AI should ONLY communicate through the task_complete summary
744
- assistantMessage = '';
745
- // Stream the summary to UI so it's visible
746
- if (taskCompleteSummary && this.onResponseStreamCallback) {
747
- this.onResponseStreamCallback(taskCompleteSummary);
790
+ // If task_complete has a summary, use that as the final message
791
+ // If no summary provided, PRESERVE the already-streamed assistantMessage text
792
+ if (taskCompleteSummary) {
793
+ // AI provided a summary - suppress any other text and use the summary
794
+ assistantMessage = '';
795
+ // Stream the summary to UI so it's visible
796
+ if (this.onResponseStreamCallback) {
797
+ this.onResponseStreamCallback(taskCompleteSummary);
798
+ }
748
799
  }
800
+ // If no summary, keep assistantMessage as-is (it was already streamed to UI)
749
801
  // Execute the tool for proper result handling
750
802
  await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
751
803
  // Clear the plan when task is complete
@@ -794,10 +846,11 @@ Press Enter to continue...
794
846
  });
795
847
  // Mark this tool call as handled so it's not duplicated
796
848
  handledToolCallIds.add(toolCall.id);
797
- // Add user message that includes plan context for execution phase
798
- const planContext = getPlanContextForPrompt();
849
+ // Add user message that includes ONLY current phase context for execution
850
+ // This implements phased execution - AI only sees one task at a time
851
+ const phaseContext = getPhaseContextForPrompt();
799
852
  const originalRequest = this.pendingPlanRequest || message;
800
- const executionPrompt = `${planContext}\n\nOriginal Request: ${originalRequest}\n\nProceed with task 1. After completing each task, call mark_task_complete with the task number.`;
853
+ const executionPrompt = `${phaseContext}\n\nOriginal Request: ${originalRequest}\n\nComplete the current task. After finishing each subtask, call mark_task_complete with the subtask number (e.g., "1.1"). When all subtasks are done, the main task will automatically complete and you'll receive the next task.`;
801
854
  this.conversationHistory.push({
802
855
  role: 'user',
803
856
  content: executionPrompt,
@@ -842,7 +895,7 @@ Press Enter to continue...
842
895
  }
843
896
  catch (parseError) {
844
897
  // Log error and add error result to history so AI knows
845
- console.error('Failed to parse plan:', parseError);
898
+ logWarning(`Failed to parse plan: ${parseError?.message || parseError}`);
846
899
  // CRITICAL: Add tool result even on parse error to prevent silent loop
847
900
  this.conversationHistory.push({
848
901
  role: 'assistant',
@@ -887,20 +940,66 @@ Press Enter to continue...
887
940
  const completionJson = result.substring('TASK_COMPLETED:'.length);
888
941
  try {
889
942
  const completion = JSON.parse(completionJson);
890
- const currentPlan = getCurrentPlan();
891
- if (currentPlan && this.onTaskCompleted) {
892
- const task = currentPlan.steps[completion.taskNumber - 1];
893
- this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote);
943
+ const currentPlanData = getCurrentPlan();
944
+ if (currentPlanData && this.onTaskCompleted) {
945
+ // Handle both main task completion and subtask completion
946
+ const taskNumParts = String(completion.taskNumber).split('.');
947
+ const mainTaskNum = parseInt(taskNumParts[0], 10) - 1;
948
+ const task = currentPlanData.steps[mainTaskNum];
949
+ if (task) {
950
+ this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote);
951
+ }
952
+ }
953
+ // Notify UI about completed task/subtask
954
+ const displayType = completion.type === 'subtask' ? 'Subtask' : 'Task';
955
+ this.notifyToolStatus(toolCall.name, 'completed', toolCall.arguments, `${displayType} ${completion.taskNumber} of ${completion.totalCount} completed: ${completion.taskDescription}`);
956
+ // Check if we need to advance to next phase (when main task is complete)
957
+ if (completion.mainTaskComplete || completion.type === 'task') {
958
+ // A main task is complete - check if there's a next phase
959
+ const nextPhase = getCurrentPhase();
960
+ if (nextPhase && !completion.allComplete) {
961
+ // Inject next phase context for the AI
962
+ const phaseContext = getPhaseContextForPrompt();
963
+ // Add tool result for current task
964
+ toolResults.push({
965
+ tool_call_id: toolCall.id,
966
+ name: toolCall.name,
967
+ result: `Task completed. Moving to Task ${nextPhase.taskNumber}: ${nextPhase.task.description}`,
968
+ });
969
+ // Add the tool call and result to history
970
+ this.conversationHistory.push({
971
+ role: 'assistant',
972
+ content: '',
973
+ tool_calls: [toolCall],
974
+ });
975
+ this.conversationHistory.push({
976
+ role: 'tool',
977
+ tool_call_id: toolCall.id,
978
+ content: `Task completed. Now starting Task ${nextPhase.taskNumber}.`,
979
+ });
980
+ // Add next phase instructions
981
+ this.conversationHistory.push({
982
+ role: 'user',
983
+ content: `${phaseContext}\n\nContinue with the next task. Complete each subtask and call mark_task_complete for each one.`,
984
+ });
985
+ // Mark as handled
986
+ handledToolCallIds.add(toolCall.id);
987
+ // Update messages and continue
988
+ messages = [...this.conversationHistory];
989
+ continue;
990
+ }
894
991
  }
895
- // Notify UI about completed task
896
- this.notifyToolStatus(toolCall.name, 'completed', toolCall.arguments, `Task ${completion.taskNumber} of ${completion.totalCount} completed: ${completion.taskDescription}`);
897
- // Add to tool results
992
+ // Add to tool results for subtask completion or final completion
898
993
  toolResults.push({
899
994
  tool_call_id: toolCall.id,
900
995
  name: toolCall.name,
901
996
  result: completion.allComplete
902
- ? 'All tasks completed! Call task_complete with a summary.'
903
- : `Task ${completion.taskNumber} completed. Next: ${completion.nextTask}`,
997
+ ? 'All tasks completed! Call task_complete with a summary of what was accomplished.'
998
+ : completion.nextSubtask
999
+ ? `Subtask ${completion.taskNumber} completed. Next subtask: ${completion.nextSubtask}`
1000
+ : completion.nextTask
1001
+ ? `Task ${completion.taskNumber} completed. Next: ${completion.nextTask}`
1002
+ : `Task ${completion.taskNumber} completed.`,
904
1003
  });
905
1004
  // If all tasks are complete, prompt AI to call task_complete
906
1005
  if (completion.allComplete) {
@@ -909,7 +1008,7 @@ Press Enter to continue...
909
1008
  }
910
1009
  }
911
1010
  catch (parseError) {
912
- console.error('Failed to parse task completion:', parseError);
1011
+ logWarning(`Failed to parse task completion: ${parseError?.message || parseError}`);
913
1012
  }
914
1013
  }
915
1014
  continue;
@@ -1099,8 +1198,8 @@ Press Enter to continue...
1099
1198
  }
1100
1199
  // If task_complete was called, stop the agentic loop immediately
1101
1200
  if (taskCompleted) {
1102
- // Set the final message to the summary from task_complete
1103
- finalAssistantMessage = taskCompleteSummary;
1201
+ // Set the final message: use summary if provided, otherwise use the streamed assistantMessage
1202
+ finalAssistantMessage = taskCompleteSummary || assistantMessage;
1104
1203
  break;
1105
1204
  }
1106
1205
  // If user cancelled an operation, stop the agentic loop immediately
@@ -1361,6 +1460,8 @@ Press Enter to continue...
1361
1460
  // Save assistant message to backend
1362
1461
  await this.saveMessageToBackend('assistant', finalMessage);
1363
1462
  } // End of while loop
1463
+ // Auto-save conversation to local storage
1464
+ this.saveCurrentChat();
1364
1465
  // Log session end
1365
1466
  conversationLogger.endSession(finalAssistantMessage, turnCount);
1366
1467
  // Send response back to UI (only if there's a message)
@@ -1396,7 +1497,8 @@ Press Enter to continue...
1396
1497
  responseMessage = `Available Commands:\n\n` +
1397
1498
  `/help - Show this help message\n` +
1398
1499
  `/init - Analyze project and create/load centaurus.md context file\n` +
1399
- `/clear - Clear conversation history\n` +
1500
+ `/chat - Manage chat sessions (resume previous chats)\n` +
1501
+ `/clear - Clear conversation and start a new chat\n` +
1400
1502
  `/config - View current configuration\n` +
1401
1503
  `/model - Select from available Google models\n` +
1402
1504
  `/plan - Toggle plan mode for complex implementations\n` +
@@ -1702,8 +1804,8 @@ Start by listing the directory structure to understand what you're working with.
1702
1804
  '• Manual completion detection';
1703
1805
  break;
1704
1806
  case 'clear':
1705
- this.conversationHistory = [];
1706
- this.conversationStarted = false;
1807
+ // Start a new chat session (clears history and generates new chat ID)
1808
+ this.startNewChat();
1707
1809
  // Don't send any response message - the UI will handle clearing
1708
1810
  return;
1709
1811
  case 'config':
@@ -1803,7 +1905,7 @@ Start by listing the directory structure to understand what you're working with.
1803
1905
  break;
1804
1906
  case 'docs':
1805
1907
  // Open documentation URL in default browser
1806
- const docsUrl = 'https://centaurus-web.vercel.app/docs';
1908
+ const docsUrl = 'https://www.centauruslabs.in/docs';
1807
1909
  const { exec } = await import('child_process');
1808
1910
  const platform = process.platform;
1809
1911
  if (platform === 'win32') {
@@ -1817,9 +1919,171 @@ Start by listing the directory structure to understand what you're working with.
1817
1919
  }
1818
1920
  responseMessage = `📖 Opening documentation in your browser...\n\n${docsUrl}`;
1819
1921
  break;
1922
+ case 'chat':
1923
+ // Chat management commands
1924
+ const chatSubCommand = args[0]?.toLowerCase();
1925
+ if (chatSubCommand === 'resume' || !chatSubCommand) {
1926
+ // Show list of saved chats for resuming (interactive picker)
1927
+ const chats = localChatStorage.listChats();
1928
+ if (chats.length === 0) {
1929
+ responseMessage = '📭 No saved chats found.\\n\\nStart a new conversation and it will be automatically saved!';
1930
+ }
1931
+ else {
1932
+ // Format chat list for picker display
1933
+ if (this.onShowChatPickerCallback) {
1934
+ this.onShowChatPickerCallback(chats, this.currentChatId);
1935
+ return; // Don't send a text response, picker will handle it
1936
+ }
1937
+ else {
1938
+ // Fallback: show as text if no picker callback
1939
+ responseMessage = '📚 Saved Chats:\\n\\n' +
1940
+ chats.slice(0, 10).map((chat, i) => {
1941
+ const date = new Date(chat.updatedAt).toLocaleDateString();
1942
+ const time = new Date(chat.updatedAt).toLocaleTimeString();
1943
+ return `${i + 1}. ${chat.title}\\n 📅 ${date} ${time} | 💬 ${chat.messageCount} messages`;
1944
+ }).join('\\n\\n') +
1945
+ (chats.length > 10 ? `\\n\\n...and ${chats.length - 10} more chats` : '');
1946
+ }
1947
+ }
1948
+ }
1949
+ else if (chatSubCommand === 'list') {
1950
+ // Show read-only list of saved chats (no selection)
1951
+ const chats = localChatStorage.listChats();
1952
+ if (chats.length === 0) {
1953
+ responseMessage = '📭 No saved chats found.\\n\\nStart a new conversation and it will be automatically saved!';
1954
+ }
1955
+ else {
1956
+ // Format chat list for read-only display
1957
+ if (this.onShowChatListCallback) {
1958
+ this.onShowChatListCallback(chats, this.currentChatId);
1959
+ return; // Don't send a text response, UI will handle it
1960
+ }
1961
+ else {
1962
+ // Fallback: show as text
1963
+ responseMessage = '📚 Saved Chats:\\n\\n' +
1964
+ chats.slice(0, 10).map((chat, i) => {
1965
+ const date = new Date(chat.updatedAt).toLocaleDateString();
1966
+ const time = new Date(chat.updatedAt).toLocaleTimeString();
1967
+ const isCurrent = chat.id === this.currentChatId ? ' (current)' : '';
1968
+ return `${i + 1}. ${chat.title}${isCurrent}\\n 📅 ${date} ${time} | 💬 ${chat.messageCount} messages`;
1969
+ }).join('\\n\\n') +
1970
+ (chats.length > 10 ? `\\n\\n...and ${chats.length - 10} more chats` : '');
1971
+ }
1972
+ }
1973
+ }
1974
+ else if (chatSubCommand === 'delete') {
1975
+ // Show list of saved chats for deletion
1976
+ const chats = localChatStorage.listChats();
1977
+ if (chats.length === 0) {
1978
+ responseMessage = '📭 No saved chats to delete.';
1979
+ }
1980
+ else {
1981
+ // Format chat list for delete picker display
1982
+ if (this.onShowChatDeletePickerCallback) {
1983
+ this.onShowChatDeletePickerCallback(chats, this.currentChatId);
1984
+ return; // Don't send a text response, picker will handle it
1985
+ }
1986
+ else {
1987
+ responseMessage = '❌ Delete picker not available. Please use the UI to delete chats.';
1988
+ }
1989
+ }
1990
+ }
1991
+ else if (chatSubCommand === 'new') {
1992
+ // Start a new chat session (old one is already saved)
1993
+ this.startNewChat();
1994
+ // Clear UI messages
1995
+ if (this.onRestoreMessagesCallback) {
1996
+ this.onRestoreMessagesCallback([]);
1997
+ }
1998
+ responseMessage = '🆕 Started a new chat session!\n\nYour previous conversation has been saved and can be resumed with /chat resume.';
1999
+ }
2000
+ else if (chatSubCommand === 'rename') {
2001
+ // Show list of saved chats for renaming
2002
+ const chats = localChatStorage.listChats();
2003
+ if (chats.length === 0) {
2004
+ responseMessage = '📭 No saved chats to rename.';
2005
+ }
2006
+ else {
2007
+ // Format chat list for rename picker display
2008
+ if (this.onShowChatRenamePickerCallback) {
2009
+ this.onShowChatRenamePickerCallback(chats, this.currentChatId);
2010
+ return; // Don't send a text response, picker will handle it
2011
+ }
2012
+ else {
2013
+ responseMessage = '❌ Rename picker not available. Please use the UI to rename chats.';
2014
+ }
2015
+ }
2016
+ }
2017
+ else {
2018
+ responseMessage = `Unknown /chat subcommand: ${chatSubCommand}\\n\\nUsage:\\n /chat resume - Resume a previous chat session\\n /chat list - List all saved chats\\n /chat delete - Delete a saved chat\\n /chat new - Start a new chat session\\n /chat rename - Rename a saved chat`;
2019
+ }
2020
+ break;
1820
2021
  case 'exit':
1821
2022
  process.exit(0);
1822
2023
  break;
2024
+ case 'add-command':
2025
+ case 'add-command-auto-detect':
2026
+ // Handle custom command auto-detect management
2027
+ const { CustomCommandsManager } = await import('./utils/custom-commands-manager.js');
2028
+ const customCmdManager = CustomCommandsManager.getInstance();
2029
+ await customCmdManager.initialize();
2030
+ const addCmdSubCommand = args[0]?.toLowerCase();
2031
+ if (!addCmdSubCommand || addCmdSubCommand === 'list') {
2032
+ // List all custom commands
2033
+ const commands = customCmdManager.listCommands();
2034
+ if (commands.length === 0) {
2035
+ responseMessage = '📋 No custom commands configured.\n\n' +
2036
+ 'Add commands with: /add-command add <command>\n' +
2037
+ 'These commands will be detected as terminal commands in Auto mode.';
2038
+ }
2039
+ else {
2040
+ responseMessage = `📋 Custom Terminal Commands (${commands.length}):\n\n` +
2041
+ commands.map(cmd => ` • ${cmd}`).join('\n') +
2042
+ '\n\nUsage:\n' +
2043
+ ' /add-command add <command> - Add a command\n' +
2044
+ ' /add-command delete <command> - Delete a command';
2045
+ }
2046
+ }
2047
+ else if (addCmdSubCommand === 'add') {
2048
+ // Add a custom command
2049
+ const cmdToAdd = args.slice(1).join(' ').trim();
2050
+ if (!cmdToAdd) {
2051
+ responseMessage = '❌ Please specify a command to add.\n\nUsage: /add-command add <command>';
2052
+ }
2053
+ else {
2054
+ const result = await customCmdManager.addCommand(cmdToAdd);
2055
+ if (result.success) {
2056
+ responseMessage = `✅ ${result.message}\n\nThis word will now be detected as a terminal command in Auto mode.`;
2057
+ }
2058
+ else {
2059
+ responseMessage = `❌ ${result.message}`;
2060
+ }
2061
+ }
2062
+ }
2063
+ else if (addCmdSubCommand === 'delete') {
2064
+ // Delete a custom command
2065
+ const cmdToDelete = args.slice(1).join(' ').trim();
2066
+ if (!cmdToDelete) {
2067
+ responseMessage = '❌ Please specify a command to delete.\n\nUsage: /add-command delete <command>';
2068
+ }
2069
+ else {
2070
+ const result = await customCmdManager.deleteCommand(cmdToDelete);
2071
+ if (result.success) {
2072
+ responseMessage = `✅ ${result.message}`;
2073
+ }
2074
+ else {
2075
+ responseMessage = `❌ ${result.message}`;
2076
+ }
2077
+ }
2078
+ }
2079
+ else {
2080
+ responseMessage = `Unknown /add-command subcommand: ${addCmdSubCommand}\n\n` +
2081
+ 'Usage:\n' +
2082
+ ' /add-command list - List all custom commands\n' +
2083
+ ' /add-command add <command> - Add a custom command\n' +
2084
+ ' /add-command delete <command> - Delete a custom command';
2085
+ }
2086
+ break;
1823
2087
  default:
1824
2088
  responseMessage = `Unknown command: /${cmd}\nType /help for available commands.`;
1825
2089
  }
@@ -1828,6 +2092,409 @@ Start by listing the directory structure to understand what you're working with.
1828
2092
  this.onDirectMessageCallback(responseMessage);
1829
2093
  }
1830
2094
  }
2095
+ /**
2096
+ * Set callback for showing chat picker
2097
+ */
2098
+ setOnShowChatPickerCallback(callback) {
2099
+ this.onShowChatPickerCallback = callback;
2100
+ }
2101
+ /**
2102
+ * Set callback for showing chat delete picker
2103
+ */
2104
+ setOnShowChatDeletePickerCallback(callback) {
2105
+ this.onShowChatDeletePickerCallback = callback;
2106
+ }
2107
+ /**
2108
+ * Set callback for showing chat list (read-only view)
2109
+ */
2110
+ setOnShowChatListCallback(callback) {
2111
+ this.onShowChatListCallback = callback;
2112
+ }
2113
+ /**
2114
+ * Set callback for showing chat rename picker
2115
+ */
2116
+ setOnShowChatRenamePickerCallback(callback) {
2117
+ this.onShowChatRenamePickerCallback = callback;
2118
+ }
2119
+ /**
2120
+ * Handle chat rename operation
2121
+ */
2122
+ handleChatRename(chatId, newTitle) {
2123
+ const chat = localChatStorage.loadChat(chatId);
2124
+ const oldTitle = chat?.title || 'Unknown chat';
2125
+ const success = localChatStorage.renameChat(chatId, newTitle);
2126
+ if (success) {
2127
+ if (this.onDirectMessageCallback) {
2128
+ this.onDirectMessageCallback(`✏️ Renamed chat: "${oldTitle}" → "${newTitle}"`);
2129
+ }
2130
+ }
2131
+ else {
2132
+ if (this.onResponseCallback) {
2133
+ this.onResponseCallback('❌ Failed to rename the chat.');
2134
+ }
2135
+ }
2136
+ }
2137
+ /**
2138
+ * Handle chat delete picker selection
2139
+ */
2140
+ async handleChatDeleteSelection(chatId) {
2141
+ const chat = localChatStorage.loadChat(chatId);
2142
+ const chatTitle = chat?.title || 'Unknown chat';
2143
+ const isDeletingCurrentChat = this.currentChatId === chatId;
2144
+ // Get the backend conversation ID (UUID) if available for file deletion
2145
+ // Files are stored with the backend UUID, not the local chat ID
2146
+ const fileDeleteId = chat?.backendConversationId || chatId;
2147
+ const success = localChatStorage.deleteChat(chatId);
2148
+ if (success) {
2149
+ // Clean up any files associated with this conversation (images in GCS)
2150
+ // Use backendConversationId (UUID) if available, as that's what files are stored under
2151
+ // This is a fire-and-forget operation - don't block on it
2152
+ if (apiClient.isAuthenticated()) {
2153
+ apiClient.deleteConversationFiles(fileDeleteId).catch(() => {
2154
+ // Silently ignore - files might not exist or backend error
2155
+ });
2156
+ }
2157
+ // If we deleted the current chat, start a new chat
2158
+ if (isDeletingCurrentChat) {
2159
+ this.startNewChat();
2160
+ if (this.onDirectMessageCallback) {
2161
+ this.onDirectMessageCallback(`🗑️ Deleted current chat: "${chatTitle}"\n\n🆕 Started a new chat session.`);
2162
+ }
2163
+ // Clear UI messages via restore callback with empty array
2164
+ if (this.onRestoreMessagesCallback) {
2165
+ this.onRestoreMessagesCallback([]);
2166
+ }
2167
+ }
2168
+ else {
2169
+ if (this.onDirectMessageCallback) {
2170
+ this.onDirectMessageCallback(`🗑️ Deleted chat: "${chatTitle}"`);
2171
+ }
2172
+ }
2173
+ }
2174
+ else {
2175
+ if (this.onResponseCallback) {
2176
+ this.onResponseCallback('❌ Failed to delete the selected chat.');
2177
+ }
2178
+ }
2179
+ }
2180
+ /**
2181
+ * Record a user shell command and its output to conversation history
2182
+ * This allows the AI to see what commands the user ran in command mode
2183
+ */
2184
+ recordShellCommandToHistory(command, output, cwd, exitCode) {
2185
+ // Add as a user message so AI knows what the user did
2186
+ const shellCommandInfo = exitCode !== undefined && exitCode !== 0
2187
+ ? `[User ran shell command in ${cwd}]\nCommand: ${command}\nExit Code: ${exitCode}\nOutput:\n${output}`
2188
+ : `[User ran shell command in ${cwd}]\nCommand: ${command}\nOutput:\n${output || '(no output)'}`;
2189
+ this.conversationHistory.push({
2190
+ role: 'user',
2191
+ content: shellCommandInfo
2192
+ });
2193
+ }
2194
+ /**
2195
+ * Save current conversation to local storage
2196
+ */
2197
+ saveCurrentChat() {
2198
+ // Only save if there are messages (AI conversation or shell commands)
2199
+ if (this.conversationHistory.length === 0) {
2200
+ return;
2201
+ }
2202
+ // Generate chat ID if not exists
2203
+ if (!this.currentChatId) {
2204
+ this.currentChatId = localChatStorage.generateChatId();
2205
+ }
2206
+ // Convert to StoredMessage format (AI context)
2207
+ const storedMessages = this.conversationHistory.map(msg => ({
2208
+ role: msg.role,
2209
+ content: msg.content,
2210
+ tool_calls: msg.tool_calls,
2211
+ tool_call_id: msg.tool_call_id,
2212
+ }));
2213
+ // Convert UI messages to serializable format
2214
+ const storedUIMessages = this.uiMessageHistory.map(msg => ({
2215
+ id: msg.id,
2216
+ role: msg.role,
2217
+ content: msg.content,
2218
+ timestamp: msg.timestamp?.toISOString(),
2219
+ toolExecution: msg.toolExecution ? {
2220
+ toolName: msg.toolExecution.toolName,
2221
+ status: msg.toolExecution.status,
2222
+ result: msg.toolExecution.result,
2223
+ error: msg.toolExecution.error,
2224
+ arguments: msg.toolExecution.arguments,
2225
+ } : undefined,
2226
+ shouldStream: msg.shouldStream,
2227
+ isCommandMode: msg.isCommandMode,
2228
+ tool_call_id: msg.tool_call_id,
2229
+ tool_calls: msg.tool_calls,
2230
+ thinkingDuration: msg.thinkingDuration,
2231
+ taskCompletion: msg.taskCompletion,
2232
+ planAccepted: msg.planAccepted,
2233
+ connectionStatus: msg.connectionStatus, // For SSH/WSL/Docker connection status boxes
2234
+ }));
2235
+ // Capture remote context if in remote environment (SSH/WSL/Docker)
2236
+ // Use null to explicitly clear remote context when not in remote (so resuming won't reconnect)
2237
+ let remoteContext = null;
2238
+ const currentContext = this.contextManager.getCurrentContext();
2239
+ if (currentContext.type !== 'local' && this.lastConnectionCommand) {
2240
+ remoteContext = {
2241
+ type: currentContext.type,
2242
+ connectionCommand: this.lastConnectionCommand,
2243
+ remoteCwd: currentContext.metadata.workingDirectory,
2244
+ localCwdBeforeRemote: this.localCwdBeforeRemote || process.cwd(),
2245
+ metadata: {
2246
+ hostname: currentContext.metadata.hostname,
2247
+ username: currentContext.metadata.username,
2248
+ distroName: currentContext.metadata.distroName,
2249
+ containerId: currentContext.metadata.containerId,
2250
+ port: currentContext.metadata.port,
2251
+ }
2252
+ };
2253
+ }
2254
+ // Determine the local CWD to save (use localCwdBeforeRemote if in remote, otherwise current cwd)
2255
+ const cwdToSave = currentContext.type !== 'local' && this.localCwdBeforeRemote
2256
+ ? this.localCwdBeforeRemote
2257
+ : this.cwd;
2258
+ try {
2259
+ localChatStorage.saveChat(this.currentChatId, storedMessages, storedUIMessages, cwdToSave, remoteContext);
2260
+ // Also store the backend conversation ID (UUID) for file deletion
2261
+ // This is the ID used for GCS file storage, not the local chat ID
2262
+ const backendId = conversationManager.getCurrentConversationId();
2263
+ if (backendId && backendId !== this.currentChatId) {
2264
+ localChatStorage.setBackendConversationId(this.currentChatId, backendId);
2265
+ }
2266
+ }
2267
+ catch (error) {
2268
+ // Silently ignore save errors - don't interrupt the user
2269
+ }
2270
+ }
2271
+ /**
2272
+ * Load a chat from local storage and restore it
2273
+ */
2274
+ loadChat(chatId) {
2275
+ const chat = localChatStorage.loadChat(chatId);
2276
+ if (!chat) {
2277
+ return false;
2278
+ }
2279
+ // Clear current history and load the saved chat
2280
+ this.conversationHistory = chat.messages.map(msg => ({
2281
+ role: msg.role,
2282
+ content: msg.content,
2283
+ tool_calls: msg.tool_calls,
2284
+ tool_call_id: msg.tool_call_id,
2285
+ }));
2286
+ // Set the current chat ID to continue the conversation
2287
+ this.currentChatId = chatId;
2288
+ this.conversationStarted = true;
2289
+ // Restore CWD if saved (important for commands to run in correct directory)
2290
+ if (chat.cwd) {
2291
+ this.cwd = chat.cwd;
2292
+ // Update context manager if available
2293
+ if (this.contextManager) {
2294
+ this.contextManager.updateWorkingDirectory(chat.cwd);
2295
+ }
2296
+ // Notify UI of CWD change
2297
+ if (this.onCwdChange) {
2298
+ this.onCwdChange(chat.cwd);
2299
+ }
2300
+ }
2301
+ return true;
2302
+ }
2303
+ /**
2304
+ * Handle chat picker selection
2305
+ */
2306
+ async handleChatPickerSelection(chatId) {
2307
+ const chat = localChatStorage.loadChat(chatId);
2308
+ if (!chat) {
2309
+ if (this.onResponseCallback) {
2310
+ this.onResponseCallback('❌ Failed to load the selected chat.');
2311
+ }
2312
+ return;
2313
+ }
2314
+ // IMPORTANT: Clean up current remote session before loading new chat
2315
+ // This ensures that switching from a remote chat to a local chat properly resets the state
2316
+ const currentContext = this.contextManager.getCurrentContext();
2317
+ if (currentContext.type !== 'local') {
2318
+ // Disconnect from current remote session
2319
+ if (currentContext.handler) {
2320
+ try {
2321
+ await currentContext.handler.disconnect();
2322
+ }
2323
+ catch (error) {
2324
+ // Ignore disconnect errors - we're switching chats anyway
2325
+ }
2326
+ }
2327
+ // Pop context to return to local
2328
+ this.contextManager.popContext();
2329
+ // Clear remote context tracking
2330
+ this.localCwdBeforeRemote = null;
2331
+ this.lastConnectionCommand = null;
2332
+ }
2333
+ // Load AI context
2334
+ this.loadChat(chatId);
2335
+ // Restore UI messages if available
2336
+ if (chat.uiMessages && chat.uiMessages.length > 0 && this.onRestoreMessagesCallback) {
2337
+ // Convert StoredUIMessage back to Message format
2338
+ const restoredMessages = chat.uiMessages.map(msg => ({
2339
+ id: msg.id,
2340
+ role: msg.role,
2341
+ content: msg.content,
2342
+ timestamp: msg.timestamp ? new Date(msg.timestamp) : undefined,
2343
+ toolExecution: msg.toolExecution,
2344
+ shouldStream: false, // Don't stream restored messages
2345
+ isCommandMode: msg.isCommandMode,
2346
+ tool_call_id: msg.tool_call_id,
2347
+ tool_calls: msg.tool_calls,
2348
+ thinkingDuration: msg.thinkingDuration,
2349
+ taskCompletion: msg.taskCompletion,
2350
+ planAccepted: msg.planAccepted,
2351
+ connectionStatus: msg.connectionStatus, // For SSH/WSL/Docker connection status boxes
2352
+ }));
2353
+ this.onRestoreMessagesCallback(restoredMessages);
2354
+ }
2355
+ // Attempt to restore remote context if chat was saved while in remote environment
2356
+ if (chat.remoteContext) {
2357
+ const { type, connectionCommand, remoteCwd, localCwdBeforeRemote } = chat.remoteContext;
2358
+ // Store local CWD for when user exits remote
2359
+ this.localCwdBeforeRemote = localCwdBeforeRemote;
2360
+ this.lastConnectionCommand = connectionCommand;
2361
+ // Show reconnection notification
2362
+ if (this.onDirectMessageCallback) {
2363
+ this.onDirectMessageCallback(`🔄 Reconnecting to ${type.toUpperCase()} session...`);
2364
+ }
2365
+ // Show connecting status
2366
+ if (this.onConnectionStatusUpdate) {
2367
+ this.onConnectionStatusUpdate({
2368
+ type: type,
2369
+ status: 'connecting',
2370
+ connectionString: this.buildConnectionString(type, chat.remoteContext.metadata)
2371
+ });
2372
+ }
2373
+ try {
2374
+ // Detect and connect using the saved command
2375
+ const detection = this.commandDetector.detect(connectionCommand);
2376
+ if (detection) {
2377
+ const context = await detection.handler.connect(connectionCommand, localCwdBeforeRemote);
2378
+ this.contextManager.pushContext(context);
2379
+ // Navigate to saved remote CWD
2380
+ if (remoteCwd && remoteCwd !== context.metadata.workingDirectory) {
2381
+ try {
2382
+ await this.contextManager.executeCommand(`cd "${remoteCwd}"`);
2383
+ }
2384
+ catch (cdError) {
2385
+ // Failed to cd to saved path - warn but continue
2386
+ if (this.onDirectMessageCallback) {
2387
+ this.onDirectMessageCallback(`⚠️ Could not restore remote directory: ${remoteCwd}`);
2388
+ }
2389
+ }
2390
+ }
2391
+ // Show success
2392
+ if (this.onConnectionStatusUpdate) {
2393
+ this.onConnectionStatusUpdate({
2394
+ type: type,
2395
+ status: 'connected',
2396
+ connectionString: this.buildConnectionString(type, chat.remoteContext.metadata)
2397
+ });
2398
+ }
2399
+ return;
2400
+ }
2401
+ }
2402
+ catch (error) {
2403
+ // Connection failed - fall back to local mode
2404
+ this.localCwdBeforeRemote = null;
2405
+ this.lastConnectionCommand = null;
2406
+ if (this.onConnectionStatusUpdate) {
2407
+ this.onConnectionStatusUpdate({
2408
+ type: type,
2409
+ status: 'error',
2410
+ connectionString: this.buildConnectionString(type, chat.remoteContext.metadata),
2411
+ error: error.message
2412
+ });
2413
+ }
2414
+ if (this.onDirectMessageCallback) {
2415
+ this.onDirectMessageCallback(`⚠️ Loaded chat: "${chat.title}"\n\n❌ Could not reconnect to ${type.toUpperCase()}: ${error.message}\n\n📁 Restored to local directory: ${localCwdBeforeRemote}`);
2416
+ }
2417
+ return;
2418
+ }
2419
+ }
2420
+ // No remote context - show regular confirmation
2421
+ // No remote context - show regular confirmation and restore CWD
2422
+ if (chat.cwd && !chat.remoteContext) {
2423
+ if (fs.existsSync(chat.cwd)) {
2424
+ this.cwd = chat.cwd;
2425
+ this.contextManager.updateWorkingDirectory(chat.cwd);
2426
+ if (this.onCwdChange) {
2427
+ this.onCwdChange(chat.cwd);
2428
+ }
2429
+ }
2430
+ }
2431
+ if (this.onDirectMessageCallback) {
2432
+ const responseMessage = `✅ Loaded chat: "${chat.title}"\n\nYou have ${chat.messageCount} messages in AI context. Continue your conversation!`;
2433
+ this.onDirectMessageCallback(responseMessage);
2434
+ }
2435
+ }
2436
+ /**
2437
+ * Build a connection string for display based on remote context metadata
2438
+ */
2439
+ buildConnectionString(type, metadata) {
2440
+ if (!metadata)
2441
+ return type;
2442
+ if (type === 'ssh' && metadata.hostname) {
2443
+ return `${metadata.username || 'user'}@${metadata.hostname}`;
2444
+ }
2445
+ else if (type === 'wsl' && metadata.distroName) {
2446
+ return metadata.distroName;
2447
+ }
2448
+ else if (type === 'docker' && metadata.containerId) {
2449
+ return metadata.containerId.substring(0, 12);
2450
+ }
2451
+ return type;
2452
+ }
2453
+ /**
2454
+ * Get the current chat ID
2455
+ */
2456
+ getCurrentChatId() {
2457
+ return this.currentChatId;
2458
+ }
2459
+ /**
2460
+ * Start a new chat session
2461
+ */
2462
+ startNewChat() {
2463
+ this.conversationHistory = [];
2464
+ this.currentChatId = null;
2465
+ this.conversationStarted = false;
2466
+ this.uiMessageHistory = [];
2467
+ this.localCwdBeforeRemote = null;
2468
+ this.lastConnectionCommand = null;
2469
+ }
2470
+ /**
2471
+ * Update UI message history (called from App.tsx via callback)
2472
+ * Also triggers auto-save to ensure the latest messages are persisted
2473
+ *
2474
+ * Note: Only saves when there's real user/AI content, not just slash command system messages.
2475
+ * This prevents empty chats from being created when user only runs /chat commands.
2476
+ */
2477
+ updateUIMessageHistory(messages) {
2478
+ this.uiMessageHistory = messages;
2479
+ // Auto-save when UI history changes, but only if there's real content
2480
+ // Real content is defined as: user or assistant messages (not just system slash command messages)
2481
+ // This ensures:
2482
+ // - /chat rename, /chat new, etc. don't create new chats on their own
2483
+ // - System messages from slash commands aren't saved permanently
2484
+ // - Only actual AI conversation content triggers saves
2485
+ const hasRealContent = this.conversationHistory.length > 0;
2486
+ // Only save if there's actual AI conversation history
2487
+ // The conversationHistory only contains real user/assistant/tool messages from AI interactions
2488
+ if (messages.length > 0 && hasRealContent) {
2489
+ this.saveCurrentChat();
2490
+ }
2491
+ }
2492
+ /**
2493
+ * Set callback for restoring UI messages
2494
+ */
2495
+ setOnRestoreMessagesCallback(callback) {
2496
+ this.onRestoreMessagesCallback = callback;
2497
+ }
1831
2498
  /**
1832
2499
  * Get environment context for backend
1833
2500
  * Returns structured environment information to be sent to backend
@@ -1923,6 +2590,45 @@ Once the user approves the plan:
1923
2590
  }
1924
2591
  }
1925
2592
  }
2593
+ /**
2594
+ * Handle unexpected remote session disconnect
2595
+ * Called when SSH/WSL/Docker connection is lost unexpectedly
2596
+ */
2597
+ handleRemoteDisconnect(connectionString, type, error) {
2598
+ // Pop the remote context
2599
+ this.contextManager.popContext();
2600
+ // Restore local CWD
2601
+ if (this.localCwdBeforeRemote) {
2602
+ this.cwd = this.localCwdBeforeRemote;
2603
+ this.contextManager.updateWorkingDirectory(this.localCwdBeforeRemote);
2604
+ if (this.onCwdChange) {
2605
+ this.onCwdChange(this.localCwdBeforeRemote);
2606
+ }
2607
+ }
2608
+ // Clear remote context tracking
2609
+ this.localCwdBeforeRemote = null;
2610
+ this.lastConnectionCommand = null;
2611
+ // Save chat with no remote context
2612
+ this.saveCurrentChat();
2613
+ // Notify UI of disconnection via connection status update
2614
+ if (this.onConnectionStatusUpdate) {
2615
+ this.onConnectionStatusUpdate({
2616
+ type: type,
2617
+ status: 'disconnected',
2618
+ connectionString,
2619
+ error: error
2620
+ });
2621
+ }
2622
+ // If there's an active tool execution (shell running), mark it as error
2623
+ if (this.onToolExecutionUpdate && this.currentInteractiveProcess) {
2624
+ this.onToolExecutionUpdate({
2625
+ toolName: 'execute_command',
2626
+ status: 'error',
2627
+ error: `Disconnected from ${type}: ${error || 'Connection lost'}`
2628
+ });
2629
+ this.currentInteractiveProcess = undefined;
2630
+ }
2631
+ }
1926
2632
  /**
1927
2633
  * Set the current interactive process
1928
2634
  * This is called by the execute_command tool when starting an interactive command
@@ -1966,6 +2672,19 @@ Once the user approves the plan:
1966
2672
  }
1967
2673
  // Pop context
1968
2674
  this.contextManager.popContext();
2675
+ // Restore local CWD that was saved before entering remote session
2676
+ if (this.localCwdBeforeRemote) {
2677
+ this.cwd = this.localCwdBeforeRemote;
2678
+ this.contextManager.updateWorkingDirectory(this.localCwdBeforeRemote);
2679
+ if (this.onCwdChange) {
2680
+ this.onCwdChange(this.localCwdBeforeRemote);
2681
+ }
2682
+ }
2683
+ // Clear remote context tracking
2684
+ this.localCwdBeforeRemote = null;
2685
+ this.lastConnectionCommand = null;
2686
+ // Save chat with no remote context - so resuming won't try to reconnect
2687
+ this.saveCurrentChat();
1969
2688
  if (this.onResponseCallback) {
1970
2689
  this.onResponseCallback('✅ Exited subshell');
1971
2690
  }
@@ -2007,9 +2726,21 @@ Once the user approves the plan:
2007
2726
  // Update connection state
2008
2727
  this.contextManager.updateConnectionState('connecting');
2009
2728
  try {
2729
+ // Save local CWD before entering remote session (for restoration when resuming chat)
2730
+ if (this.contextManager.getCurrentContext().type === 'local') {
2731
+ this.localCwdBeforeRemote = this.cwd;
2732
+ }
2733
+ this.lastConnectionCommand = command;
2010
2734
  // Connect to subshell
2011
2735
  const context = await detection.handler.connect(command, this.cwd);
2012
2736
  this.contextManager.pushContext(context);
2737
+ // Set up disconnect callback for SSH connections
2738
+ if (detection.handler.type === 'ssh' && context.handler?.setDisconnectCallback) {
2739
+ context.handler.setDisconnectCallback((error) => {
2740
+ // Handle unexpected disconnect
2741
+ this.handleRemoteDisconnect(connectionString, detection.handler.type, error);
2742
+ });
2743
+ }
2013
2744
  // Show success message (replaces the spinner with static message)
2014
2745
  if (this.onConnectionStatusUpdate) {
2015
2746
  this.onConnectionStatusUpdate({
@@ -2135,6 +2866,8 @@ Once the user approves the plan:
2135
2866
  });
2136
2867
  }
2137
2868
  }
2869
+ // Record shell command to AI conversation history
2870
+ this.recordShellCommandToHistory(command, output, this.cwd, exitCode);
2138
2871
  this.currentInteractiveProcess = undefined;
2139
2872
  resolve();
2140
2873
  });
@@ -2173,6 +2906,8 @@ Once the user approves the plan:
2173
2906
  });
2174
2907
  }
2175
2908
  }
2909
+ // Record shell command to AI conversation history
2910
+ this.recordShellCommandToHistory(command, output, remoteCwd, exitCode);
2176
2911
  this.currentInteractiveProcess = undefined;
2177
2912
  resolve();
2178
2913
  });
@@ -2224,6 +2959,8 @@ Once the user approves the plan:
2224
2959
  });
2225
2960
  }
2226
2961
  }
2962
+ // Record shell command to AI conversation history
2963
+ this.recordShellCommandToHistory(command, output, remoteCwd, exitCode);
2227
2964
  this.currentInteractiveProcess = undefined;
2228
2965
  resolve();
2229
2966
  });
@@ -2278,6 +3015,8 @@ Once the user approves the plan:
2278
3015
  });
2279
3016
  }
2280
3017
  }
3018
+ // Record shell command to AI conversation history
3019
+ this.recordShellCommandToHistory(command, output, remoteCwd, exitCode);
2281
3020
  this.currentInteractiveProcess = undefined;
2282
3021
  resolve();
2283
3022
  });
@@ -2296,6 +3035,9 @@ Once the user approves the plan:
2296
3035
  };
2297
3036
  });
2298
3037
  }
3038
+ // Save conversation after user shell command execution
3039
+ // This ensures shell commands are saved to chat history like AI messages
3040
+ this.saveCurrentChat();
2299
3041
  }
2300
3042
  catch (error) {
2301
3043
  if (this.onResponseCallback) {