centaurus-cli 2.8.5 → 2.8.7

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