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.
- package/dist/cli-adapter.d.ts +85 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +773 -31
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/mcp-config-manager.d.ts.map +1 -1
- package/dist/config/mcp-config-manager.js +9 -8
- package/dist/config/mcp-config-manager.js.map +1 -1
- package/dist/config/slash-commands.d.ts +2 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +31 -1
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/context/handlers/docker-handler.js.map +1 -1
- package/dist/context/handlers/ssh-handler.d.ts +16 -1
- package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
- package/dist/context/handlers/ssh-handler.js +57 -12
- package/dist/context/handlers/ssh-handler.js.map +1 -1
- package/dist/context/subshell-handler.d.ts +14 -0
- package/dist/context/subshell-handler.d.ts.map +1 -1
- package/dist/hooks/useTerminalDimensions.d.ts +41 -0
- package/dist/hooks/useTerminalDimensions.d.ts.map +1 -0
- package/dist/hooks/useTerminalDimensions.js +84 -0
- package/dist/hooks/useTerminalDimensions.js.map +1 -0
- package/dist/index.js +57 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
- package/dist/mcp/mcp-command-handler.js +3 -1
- package/dist/mcp/mcp-command-handler.js.map +1 -1
- package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
- package/dist/mcp/mcp-server-manager.js +5 -3
- package/dist/mcp/mcp-server-manager.js.map +1 -1
- package/dist/services/api-client.d.ts +24 -0
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +27 -0
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/auth-handler.js +1 -1
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/clipboard-service.d.ts +42 -0
- package/dist/services/clipboard-service.d.ts.map +1 -0
- package/dist/services/clipboard-service.js +217 -0
- package/dist/services/clipboard-service.js.map +1 -0
- package/dist/services/local-chat-storage.d.ts +154 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -0
- package/dist/services/local-chat-storage.js +258 -0
- package/dist/services/local-chat-storage.js.map +1 -0
- package/dist/tools/grep-search.d.ts +5 -0
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +68 -16
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/plan-mode.d.ts +57 -6
- package/dist/tools/plan-mode.d.ts.map +1 -1
- package/dist/tools/plan-mode.js +297 -46
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/read-binary-file.d.ts +10 -0
- package/dist/tools/read-binary-file.d.ts.map +1 -0
- package/dist/tools/read-binary-file.js +210 -0
- package/dist/tools/read-binary-file.js.map +1 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/components/App.d.ts +35 -0
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +622 -43
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/ClipboardImageAutocomplete.d.ts +14 -0
- package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +1 -0
- package/dist/ui/components/ClipboardImageAutocomplete.js +39 -0
- package/dist/ui/components/ClipboardImageAutocomplete.js.map +1 -0
- package/dist/ui/components/ConnectionStatusMessage.d.ts +1 -1
- package/dist/ui/components/ConnectionStatusMessage.d.ts.map +1 -1
- package/dist/ui/components/ConnectionStatusMessage.js +21 -0
- package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts +17 -0
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +1 -0
- package/dist/ui/components/DetailedPlanReviewScreen.js +110 -0
- package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -0
- package/dist/ui/components/InputBox.d.ts +4 -1
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +419 -30
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +20 -6
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts +6 -0
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +66 -3
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.d.ts +8 -0
- package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.js +26 -8
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.d.ts +3 -0
- package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +10 -6
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.js +4 -4
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
- package/dist/ui/components/TaskProgressIndicator.d.ts +18 -0
- package/dist/ui/components/TaskProgressIndicator.d.ts.map +1 -0
- package/dist/ui/components/TaskProgressIndicator.js +72 -0
- package/dist/ui/components/TaskProgressIndicator.js.map +1 -0
- package/dist/ui/components/ThinkingDisplay.d.ts +3 -0
- package/dist/ui/components/ThinkingDisplay.d.ts.map +1 -1
- package/dist/ui/components/ThinkingDisplay.js +6 -4
- package/dist/ui/components/ThinkingDisplay.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +85 -15
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.d.ts +1 -2
- package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.js +108 -27
- package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
- package/dist/utils/custom-commands-manager.d.ts +59 -0
- package/dist/utils/custom-commands-manager.d.ts.map +1 -0
- package/dist/utils/custom-commands-manager.js +142 -0
- package/dist/utils/custom-commands-manager.js.map +1 -0
- package/dist/utils/input-classifier.d.ts +10 -11
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +299 -75
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/terminal-output.d.ts.map +1 -1
- package/dist/utils/terminal-output.js +110 -14
- package/dist/utils/terminal-output.js.map +1 -1
- package/dist/utils/unicode-sanitizer.d.ts +44 -0
- package/dist/utils/unicode-sanitizer.d.ts.map +1 -0
- package/dist/utils/unicode-sanitizer.js +211 -0
- package/dist/utils/unicode-sanitizer.js.map +1 -0
- package/models-config.json +2 -3
- package/package.json +4 -1
package/dist/cli-adapter.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|
798
|
-
|
|
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 = `${
|
|
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
|
-
|
|
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
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
//
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
-
`/
|
|
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
|
-
|
|
1706
|
-
this.
|
|
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://
|
|
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) {
|