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.
- package/dist/cli-adapter.d.ts +85 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +769 -28
- package/dist/cli-adapter.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 +27 -0
- package/dist/index.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 +608 -16
- 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 +2 -1
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +399 -28
- 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/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 +7 -3
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,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:
|
|
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
|
-
|
|
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
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|
798
|
-
|
|
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 = `${
|
|
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
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
//
|
|
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
|
-
:
|
|
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
|
|
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
|
-
`/
|
|
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
|
-
|
|
1706
|
-
this.
|
|
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://
|
|
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) {
|