centaurus-cli 2.8.8 ā 2.9.0
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 +82 -2
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +622 -94
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/ConfigManager.d.ts.map +1 -1
- package/dist/config/ConfigManager.js +6 -5
- package/dist/config/ConfigManager.js.map +1 -1
- package/dist/config/build-config.d.ts +42 -0
- package/dist/config/build-config.d.ts.map +1 -0
- package/dist/config/build-config.js +44 -0
- package/dist/config/build-config.js.map +1 -0
- package/dist/config/manager.d.ts +2 -2
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +9 -12
- package/dist/config/manager.js.map +1 -1
- package/dist/config/mcp-config-manager.d.ts +5 -0
- package/dist/config/mcp-config-manager.d.ts.map +1 -1
- package/dist/config/mcp-config-manager.js +8 -0
- package/dist/config/mcp-config-manager.js.map +1 -1
- package/dist/config/models.d.ts +48 -42
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +148 -133
- package/dist/config/models.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 +34 -1
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/context/context-manager.d.ts.map +1 -1
- package/dist/context/context-manager.js +6 -6
- package/dist/context/context-manager.js.map +1 -1
- package/dist/hooks/useConnectivity.d.ts +2 -0
- package/dist/hooks/useConnectivity.d.ts.map +1 -0
- package/dist/hooks/useConnectivity.js +12 -0
- package/dist/hooks/useConnectivity.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -23
- 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 +2 -5
- package/dist/mcp/mcp-command-handler.js.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.js +8 -0
- package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
- package/dist/services/ai-service-client.d.ts +1 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +8 -6
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.d.ts +46 -34
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +47 -37
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/background-task-manager.d.ts +114 -0
- package/dist/services/background-task-manager.d.ts.map +1 -0
- package/dist/services/background-task-manager.js +301 -0
- package/dist/services/background-task-manager.js.map +1 -0
- package/dist/services/connectivity-manager.d.ts +18 -0
- package/dist/services/connectivity-manager.d.ts.map +1 -0
- package/dist/services/connectivity-manager.js +72 -0
- package/dist/services/connectivity-manager.js.map +1 -0
- package/dist/services/local-chat-storage.d.ts +5 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -1
- package/dist/services/local-chat-storage.js +38 -4
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/tools/background-command.d.ts +11 -0
- package/dist/tools/background-command.d.ts.map +1 -0
- package/dist/tools/background-command.js +162 -0
- package/dist/tools/background-command.js.map +1 -0
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +6 -3
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/create-image.d.ts +10 -0
- package/dist/tools/create-image.d.ts.map +1 -0
- package/dist/tools/create-image.js +189 -0
- package/dist/tools/create-image.js.map +1 -0
- package/dist/tools/web-search.d.ts.map +1 -1
- package/dist/tools/web-search.js +8 -6
- package/dist/tools/web-search.js.map +1 -1
- package/dist/ui/components/App.d.ts +34 -2
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +403 -67
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/ContextWindowIndicator.d.ts.map +1 -1
- package/dist/ui/components/ContextWindowIndicator.js +43 -22
- package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
- package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/ui/components/ErrorBoundary.js +2 -1
- package/dist/ui/components/ErrorBoundary.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +4 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +289 -207
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +8 -15
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/SlashCommandAutocomplete.d.ts +2 -0
- package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -1
- package/dist/ui/components/SlashCommandAutocomplete.js +19 -10
- package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
- package/dist/ui/components/StatusBar.d.ts.map +1 -1
- package/dist/ui/components/StatusBar.js +4 -0
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +155 -41
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.js +1 -0
- package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
- package/dist/utils/chat-formatter.d.ts +12 -0
- package/dist/utils/chat-formatter.d.ts.map +1 -0
- package/dist/utils/chat-formatter.js +326 -0
- package/dist/utils/chat-formatter.js.map +1 -0
- package/dist/utils/command-history.d.ts.map +1 -1
- package/dist/utils/command-history.js +2 -1
- package/dist/utils/command-history.js.map +1 -1
- package/dist/utils/conversation-logger.d.ts +15 -0
- package/dist/utils/conversation-logger.d.ts.map +1 -1
- package/dist/utils/conversation-logger.js +56 -2
- package/dist/utils/conversation-logger.js.map +1 -1
- package/dist/utils/editor-utils.d.ts.map +1 -1
- package/dist/utils/editor-utils.js +3 -2
- package/dist/utils/editor-utils.js.map +1 -1
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +140 -20
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +31 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/text-clipboard.d.ts +12 -0
- package/dist/utils/text-clipboard.d.ts.map +1 -0
- package/dist/utils/text-clipboard.js +63 -0
- package/dist/utils/text-clipboard.js.map +1 -0
- package/package.json +1 -2
- package/models-config.json +0 -126
package/dist/cli-adapter.js
CHANGED
|
@@ -17,10 +17,12 @@ import { createPlanTool, markTaskCompleteTool, getCurrentPlan, clearPlan, approv
|
|
|
17
17
|
import { webSearchTool, fetchUrlTool } from './tools/web-search.js';
|
|
18
18
|
import { taskCompleteTool } from './tools/task-complete.js';
|
|
19
19
|
import { readBinaryFileTool } from './tools/read-binary-file.js';
|
|
20
|
+
import { createImageTool } from './tools/create-image.js';
|
|
21
|
+
import { backgroundCommandTool } from './tools/background-command.js';
|
|
20
22
|
import { apiClient } from './services/api-client.js';
|
|
21
23
|
import { conversationManager } from './services/conversation-manager.js';
|
|
22
24
|
import { aiServiceClient } from './services/ai-service-client.js';
|
|
23
|
-
import { isValidModel, getInvalidModelError } from './config/models.js';
|
|
25
|
+
import { isValidModel, getInvalidModelError, fetchModelsConfig, getModelConfigByIdAndName } from './config/models.js';
|
|
24
26
|
import { authenticateWithGoogle } from './services/auth-handler.js';
|
|
25
27
|
import { ContextManager } from './context/context-manager.js';
|
|
26
28
|
import { CommandDetector } from './context/command-detector.js';
|
|
@@ -32,9 +34,10 @@ import { MCPConfigManager } from './config/mcp-config-manager.js';
|
|
|
32
34
|
import { MCPServerManager } from './mcp/mcp-server-manager.js';
|
|
33
35
|
import { MCPCommandHandler } from './mcp/mcp-command-handler.js';
|
|
34
36
|
import { isInteractiveEditorCommand, runWSLCommand, runDockerCommand, runSSHCommand } from './utils/editor-utils.js';
|
|
35
|
-
import { conversationLogger } from './utils/conversation-logger.js';
|
|
37
|
+
import { conversationLogger, quickLog } from './utils/conversation-logger.js';
|
|
36
38
|
import { localChatStorage } from './services/local-chat-storage.js';
|
|
37
39
|
import { logWarning } from './utils/logger.js';
|
|
40
|
+
import { BackgroundTaskManager } from './services/background-task-manager.js';
|
|
38
41
|
export class CentaurusCLI {
|
|
39
42
|
configManager;
|
|
40
43
|
toolRegistry;
|
|
@@ -43,6 +46,7 @@ export class CentaurusCLI {
|
|
|
43
46
|
planMode = false;
|
|
44
47
|
pendingPlanRequest = null; // Stores original user request during planning phase
|
|
45
48
|
commandMode = false;
|
|
49
|
+
backgroundMode = false; // Background shell mode for running commands in background
|
|
46
50
|
previousMode = 'execution';
|
|
47
51
|
onResponseCallback;
|
|
48
52
|
onDirectMessageCallback; // For slash commands - adds directly to history
|
|
@@ -81,6 +85,13 @@ export class CentaurusCLI {
|
|
|
81
85
|
uiMessageHistory = []; // Mirror of App.tsx's messageHistory for saving
|
|
82
86
|
localCwdBeforeRemote = null; // Track local CWD before entering remote session
|
|
83
87
|
lastConnectionCommand = null; // Track the command used to connect to remote
|
|
88
|
+
onBackgroundModeChange;
|
|
89
|
+
onBackgroundTaskCountChange;
|
|
90
|
+
onSetAutoMode;
|
|
91
|
+
onShowBackgroundTaskPickerCallback;
|
|
92
|
+
onShowBackgroundTaskCancelPickerCallback;
|
|
93
|
+
onBackgroundTaskViewCallback;
|
|
94
|
+
onTokenCountUpdate; // Report actual AI context token count to UI
|
|
84
95
|
constructor() {
|
|
85
96
|
this.configManager = new ConfigManager();
|
|
86
97
|
this.toolRegistry = new ToolRegistry();
|
|
@@ -166,6 +177,9 @@ export class CentaurusCLI {
|
|
|
166
177
|
setOnConnectionStatusUpdate(callback) {
|
|
167
178
|
this.onConnectionStatusUpdate = callback;
|
|
168
179
|
}
|
|
180
|
+
setOnTokenCountUpdate(callback) {
|
|
181
|
+
this.onTokenCountUpdate = callback;
|
|
182
|
+
}
|
|
169
183
|
async initializeMCP() {
|
|
170
184
|
try {
|
|
171
185
|
const mcpConfigManager = new MCPConfigManager();
|
|
@@ -229,20 +243,20 @@ export class CentaurusCLI {
|
|
|
229
243
|
}
|
|
230
244
|
async handlePickerSelection(selection, pickerType) {
|
|
231
245
|
try {
|
|
232
|
-
// Selection is the index of the model in
|
|
233
|
-
const
|
|
246
|
+
// Selection is the index of the model in models array from backend
|
|
247
|
+
const modelsConfig = await fetchModelsConfig();
|
|
234
248
|
const modelIndex = parseInt(selection, 10);
|
|
235
|
-
if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >=
|
|
249
|
+
if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >= modelsConfig.models.length) {
|
|
236
250
|
throw new Error('Invalid model selection');
|
|
237
251
|
}
|
|
238
|
-
const selectedModel =
|
|
252
|
+
const selectedModel = modelsConfig.models[modelIndex];
|
|
239
253
|
// Store only the model ID and name (not the full config with thinkingConfig)
|
|
240
254
|
// This prevents caching issues when we update model configs
|
|
241
255
|
this.configManager.set('model', selectedModel.id);
|
|
242
256
|
this.configManager.set('modelName', selectedModel.name);
|
|
243
|
-
// Notify UI of model name change
|
|
257
|
+
// Notify UI of model name change and contextWindow
|
|
244
258
|
if (this.onModelChange) {
|
|
245
|
-
this.onModelChange(selectedModel.name);
|
|
259
|
+
this.onModelChange(selectedModel.name, selectedModel.contextWindow);
|
|
246
260
|
}
|
|
247
261
|
const responseMessage = `Model changed to: ${selectedModel.name}`;
|
|
248
262
|
// Send response back to UI
|
|
@@ -335,6 +349,8 @@ export class CentaurusCLI {
|
|
|
335
349
|
this.toolRegistry.register(fetchUrlTool);
|
|
336
350
|
this.toolRegistry.register(taskCompleteTool);
|
|
337
351
|
this.toolRegistry.register(readBinaryFileTool);
|
|
352
|
+
this.toolRegistry.register(createImageTool);
|
|
353
|
+
this.toolRegistry.register(backgroundCommandTool);
|
|
338
354
|
// Load configuration
|
|
339
355
|
const config = this.configManager.load();
|
|
340
356
|
// Enable backend sync if authenticated
|
|
@@ -396,47 +412,24 @@ Press Enter to continue...
|
|
|
396
412
|
}
|
|
397
413
|
/**
|
|
398
414
|
* Start a new conversation in the backend
|
|
415
|
+
* @deprecated Backend conversation creation is no longer needed since messages are stored locally
|
|
416
|
+
* This function is kept as a no-op for compatibility
|
|
399
417
|
*/
|
|
400
418
|
async ensureConversationStarted() {
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
}
|
|
410
|
-
try {
|
|
411
|
-
const config = this.configManager.load();
|
|
412
|
-
const title = `Conversation ${new Date().toLocaleString()}`;
|
|
413
|
-
await conversationManager.startNewConversation(title, config.model || 'gemini-2.5-flash', 'google', // Always use Google provider now
|
|
414
|
-
this.cwd);
|
|
415
|
-
this.conversationStarted = true;
|
|
416
|
-
}
|
|
417
|
-
catch (error) {
|
|
418
|
-
// Silently continue without backend persistence
|
|
419
|
-
}
|
|
419
|
+
// No-op: Backend conversation creation has been disabled
|
|
420
|
+
// Conversations are now managed locally via local-chat-storage.ts
|
|
421
|
+
return;
|
|
420
422
|
}
|
|
421
423
|
/**
|
|
422
424
|
* Save a message to the backend
|
|
425
|
+
* @deprecated Messages are now stored locally only via saveCurrentChat()
|
|
426
|
+
* This function is kept as a no-op for compatibility
|
|
423
427
|
*/
|
|
424
428
|
async saveMessageToBackend(role, content) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
await this.ensureConversationStarted();
|
|
430
|
-
await conversationManager.addMessage({
|
|
431
|
-
role,
|
|
432
|
-
content,
|
|
433
|
-
contentType: 'text',
|
|
434
|
-
metadata: {},
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
catch (error) {
|
|
438
|
-
// Silently continue without backend persistence
|
|
439
|
-
}
|
|
429
|
+
// No-op: Messages are stored locally only
|
|
430
|
+
// Backend message storage has been disabled to avoid "failed to fetch" errors
|
|
431
|
+
// All conversation history is persisted via local-chat-storage.ts
|
|
432
|
+
return;
|
|
440
433
|
}
|
|
441
434
|
getModel() {
|
|
442
435
|
const config = this.configManager.load();
|
|
@@ -485,19 +478,45 @@ Press Enter to continue...
|
|
|
485
478
|
if (toolCallIds.size > 0) {
|
|
486
479
|
// Log the cleanup for debugging
|
|
487
480
|
try {
|
|
488
|
-
|
|
481
|
+
quickLog(`[${new Date().toISOString()}] [CLI] Cleaning up orphaned tool_calls: ${Array.from(toolCallIds).join(', ')}\n`);
|
|
489
482
|
}
|
|
490
483
|
catch (e) { }
|
|
491
484
|
// Remove the orphaned assistant message and any partial tool results after it
|
|
492
485
|
this.conversationHistory.splice(lastAssistantWithToolCallsIndex);
|
|
493
486
|
}
|
|
494
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Strip thinking blocks from ALL assistant messages in the conversation history.
|
|
490
|
+
*
|
|
491
|
+
* This is called at the START of a new user request to clear thinking from
|
|
492
|
+
* previous tasks. During the current agent loop, thinking is preserved for
|
|
493
|
+
* all turns to help the AI maintain reasoning context.
|
|
494
|
+
*
|
|
495
|
+
* Pattern:
|
|
496
|
+
* - New user request starts ā strip ALL thinking from history
|
|
497
|
+
* - During agent loop (multi-turn tool execution) ā keep ALL thinking
|
|
498
|
+
* - This allows AI to remember reasoning for current task but not old tasks
|
|
499
|
+
*/
|
|
500
|
+
stripThinkingFromHistory() {
|
|
501
|
+
for (let i = 0; i < this.conversationHistory.length; i++) {
|
|
502
|
+
const msg = this.conversationHistory[i];
|
|
503
|
+
if (msg.role === 'assistant' && msg.thinking) {
|
|
504
|
+
// Remove thinking from the message in place
|
|
505
|
+
delete msg.thinking;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
495
509
|
async handleMessage(message) {
|
|
496
510
|
// Handle command mode - execute commands directly
|
|
497
511
|
if (this.commandMode) {
|
|
498
512
|
await this.handleCommandModeExecution(message);
|
|
499
513
|
return;
|
|
500
514
|
}
|
|
515
|
+
// Handle background mode - execute commands in background
|
|
516
|
+
if (this.backgroundMode) {
|
|
517
|
+
this.handleBackgroundModeExecution(message);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
501
520
|
// Handle slash commands
|
|
502
521
|
if (message.startsWith('/')) {
|
|
503
522
|
await this.handleSlashCommand(message);
|
|
@@ -537,13 +556,17 @@ CRITICAL INSTRUCTIONS:
|
|
|
537
556
|
DO NOT use write_to_file, edit_file, or execute_command until the plan is approved.`;
|
|
538
557
|
userMessageContent = planModePrefix;
|
|
539
558
|
}
|
|
559
|
+
// NEW USER REQUEST: Strip thinking from previous tasks
|
|
560
|
+
// This clears thinking from the previous agent loop but thinking will be
|
|
561
|
+
// preserved for all turns within the current agent loop
|
|
562
|
+
this.stripThinkingFromHistory();
|
|
540
563
|
// Add user message to history
|
|
541
564
|
this.conversationHistory.push({
|
|
542
565
|
role: 'user',
|
|
543
566
|
content: userMessageContent,
|
|
544
567
|
});
|
|
545
|
-
//
|
|
546
|
-
|
|
568
|
+
// Messages are stored locally only - no backend persistence needed
|
|
569
|
+
// Local storage is handled by saveCurrentChat() which saves to ~/.centaurus/chats/
|
|
547
570
|
// Start logging session and log user message
|
|
548
571
|
conversationLogger.startSession();
|
|
549
572
|
conversationLogger.logUserMessage(message);
|
|
@@ -569,10 +592,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
569
592
|
// Get selected model ID from config, then look up full config from models-config.json
|
|
570
593
|
const config = this.configManager.load();
|
|
571
594
|
const selectedModelId = config.model || 'gemini-2.5-flash';
|
|
572
|
-
// Look up the full model config (including thinkingConfig) from
|
|
573
|
-
// This ensures we always use the latest config
|
|
574
|
-
const
|
|
575
|
-
const selectedModelConfig = ALL_MODELS.find(m => m.id === selectedModelId && m.name === config.modelName);
|
|
595
|
+
// Look up the full model config (including thinkingConfig) from backend
|
|
596
|
+
// This ensures we always use the latest config
|
|
597
|
+
const selectedModelConfig = await getModelConfigByIdAndName(selectedModelId, config.modelName || '');
|
|
576
598
|
const selectedModel = selectedModelId;
|
|
577
599
|
const selectedModelThinkingConfig = selectedModelConfig?.thinkingConfig;
|
|
578
600
|
// Build messages array WITHOUT system prompt - backend will inject it
|
|
@@ -594,7 +616,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
594
616
|
let narrationAttempts = 0; // Track how many times AI narrated without executing
|
|
595
617
|
let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
|
|
596
618
|
let thoughtStartTime = null; // Track when thinking started
|
|
597
|
-
let thoughtContent = ''; // Accumulate thought content
|
|
619
|
+
let thoughtContent = ''; // Accumulate thought content during streaming
|
|
620
|
+
let currentTurnThinking = ''; // Persist thinking for the current turn to attach to assistant message
|
|
598
621
|
// ANTI-LOOP: Track duplicate tool calls to detect infinite loops
|
|
599
622
|
const MAX_DUPLICATE_CALLS = 2; // Max times same operation allowed on same target
|
|
600
623
|
const fileWriteTracker = new Map(); // Track writes per file
|
|
@@ -619,7 +642,28 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
619
642
|
// DEBUG: Log message history state before AI call
|
|
620
643
|
const messageStats = {
|
|
621
644
|
totalMessages: messages.length,
|
|
622
|
-
totalCharacters: messages.reduce((sum, m) =>
|
|
645
|
+
totalCharacters: messages.reduce((sum, m) => {
|
|
646
|
+
let len = typeof m.content === 'string' ? m.content.length : 0;
|
|
647
|
+
// Include thinking content (internal reasoning)
|
|
648
|
+
if (m.thinking) {
|
|
649
|
+
len += m.thinking.length;
|
|
650
|
+
}
|
|
651
|
+
// Include tool calls (name + arguments)
|
|
652
|
+
if (m.tool_calls) {
|
|
653
|
+
m.tool_calls.forEach(tc => {
|
|
654
|
+
len += tc.name.length;
|
|
655
|
+
// Arguments are JSON stringified in the payload
|
|
656
|
+
if (tc.arguments) {
|
|
657
|
+
len += JSON.stringify(tc.arguments).length;
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
// Include tool_call_id for tool messages
|
|
662
|
+
if (m.role === 'tool' && m.tool_call_id) {
|
|
663
|
+
len += m.tool_call_id.length;
|
|
664
|
+
}
|
|
665
|
+
return sum + len;
|
|
666
|
+
}, 0),
|
|
623
667
|
byRole: {
|
|
624
668
|
system: messages.filter(m => m.role === 'system').length,
|
|
625
669
|
user: messages.filter(m => m.role === 'user').length,
|
|
@@ -629,12 +673,19 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
629
673
|
assistantWithToolCalls: messages.filter(m => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0).length
|
|
630
674
|
};
|
|
631
675
|
try {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
676
|
+
quickLog(`[${new Date().toISOString()}] [CLI] === TURN ${turnCount} AI CALL ===\n`);
|
|
677
|
+
quickLog(`[${new Date().toISOString()}] [CLI] Message history: ${messageStats.totalMessages} messages, ${messageStats.totalCharacters} chars\n`);
|
|
678
|
+
quickLog(`[${new Date().toISOString()}] [CLI] By role: system=${messageStats.byRole.system}, user=${messageStats.byRole.user}, assistant=${messageStats.byRole.assistant}, tool=${messageStats.byRole.tool}\n`);
|
|
679
|
+
quickLog(`[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
|
|
636
680
|
}
|
|
637
681
|
catch (e) { }
|
|
682
|
+
// Report actual token count to UI (estimate: 1 token ā 4 chars)
|
|
683
|
+
// Include system prompt estimate (~14000 chars based on backend logs)
|
|
684
|
+
const SYSTEM_PROMPT_ESTIMATE = 14000; // Backend injects ~14K char system prompt
|
|
685
|
+
const estimatedTokens = Math.ceil((messageStats.totalCharacters + SYSTEM_PROMPT_ESTIMATE) / 4);
|
|
686
|
+
if (this.onTokenCountUpdate) {
|
|
687
|
+
this.onTokenCountUpdate(estimatedTokens);
|
|
688
|
+
}
|
|
638
689
|
// Stream AI response from backend
|
|
639
690
|
// Backend will inject system prompt automatically with environment context
|
|
640
691
|
for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
|
|
@@ -675,6 +726,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
675
726
|
if (this.onThoughtCompleteCallback) {
|
|
676
727
|
this.onThoughtCompleteCallback(thinkingDuration);
|
|
677
728
|
}
|
|
729
|
+
// Capture thinking for this turn before clearing
|
|
730
|
+
currentTurnThinking = thoughtContent;
|
|
678
731
|
thoughtStartTime = null;
|
|
679
732
|
thoughtContent = '';
|
|
680
733
|
}
|
|
@@ -696,7 +749,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
696
749
|
const toolCall = chunk.toolCall;
|
|
697
750
|
// Debug: Log every tool_call chunk received
|
|
698
751
|
try {
|
|
699
|
-
|
|
752
|
+
quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED: ${toolCall?.name || 'unknown'}, toolCalls.length before push: ${toolCalls.length}\n`);
|
|
700
753
|
}
|
|
701
754
|
catch (e) { }
|
|
702
755
|
conversationLogger.logToolCall(toolCall?.name || 'unknown', toolCall?.id || 'unknown', toolCall?.arguments || {});
|
|
@@ -707,6 +760,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
707
760
|
if (this.onThoughtCompleteCallback) {
|
|
708
761
|
this.onThoughtCompleteCallback(thinkingDuration);
|
|
709
762
|
}
|
|
763
|
+
// Capture thinking for this turn before clearing
|
|
764
|
+
currentTurnThinking = thoughtContent;
|
|
710
765
|
thoughtStartTime = null;
|
|
711
766
|
thoughtContent = '';
|
|
712
767
|
}
|
|
@@ -722,7 +777,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
722
777
|
}
|
|
723
778
|
// Debug: Log after push
|
|
724
779
|
try {
|
|
725
|
-
|
|
780
|
+
quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL PUSHED: ${toolCall?.name || 'unknown'}, toolCalls.length after push: ${toolCalls.length}\n`);
|
|
726
781
|
}
|
|
727
782
|
catch (e) { }
|
|
728
783
|
}
|
|
@@ -735,6 +790,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
735
790
|
if (this.onThoughtCompleteCallback) {
|
|
736
791
|
this.onThoughtCompleteCallback(thinkingDuration);
|
|
737
792
|
}
|
|
793
|
+
// Capture thinking for this turn before clearing
|
|
794
|
+
currentTurnThinking = thoughtContent;
|
|
738
795
|
thoughtStartTime = null;
|
|
739
796
|
thoughtContent = '';
|
|
740
797
|
}
|
|
@@ -777,7 +834,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
777
834
|
for (let i = 0; i < toolCalls.length; i++) {
|
|
778
835
|
// Debug: Log which tool we're about to execute
|
|
779
836
|
try {
|
|
780
|
-
|
|
837
|
+
quickLog(`[${new Date().toISOString()}] [CLI] *** ABOUT TO EXECUTE TOOL [${i + 1}/${toolCalls.length}]: ${toolCalls[i].name}\n`);
|
|
781
838
|
}
|
|
782
839
|
catch (e) { }
|
|
783
840
|
const toolCall = toolCalls[i];
|
|
@@ -833,11 +890,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
833
890
|
this.onPlanModeChange(false);
|
|
834
891
|
}
|
|
835
892
|
// Add assistant message with plan tool call to history
|
|
836
|
-
|
|
893
|
+
const planAssistantMsg = {
|
|
837
894
|
role: 'assistant',
|
|
838
895
|
content: '',
|
|
839
896
|
tool_calls: [toolCall],
|
|
840
|
-
}
|
|
897
|
+
};
|
|
898
|
+
if (currentTurnThinking) {
|
|
899
|
+
planAssistantMsg.thinking = currentTurnThinking;
|
|
900
|
+
}
|
|
901
|
+
this.conversationHistory.push(planAssistantMsg);
|
|
841
902
|
// Add plan approval response
|
|
842
903
|
this.conversationHistory.push({
|
|
843
904
|
role: 'tool',
|
|
@@ -873,11 +934,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
873
934
|
else {
|
|
874
935
|
// No approval callback - add the tool result to history and wait for user response
|
|
875
936
|
// This ensures the AI doesn't get stuck in a silent loop
|
|
876
|
-
|
|
937
|
+
const planAssistantMsg = {
|
|
877
938
|
role: 'assistant',
|
|
878
939
|
content: '',
|
|
879
940
|
tool_calls: [toolCall],
|
|
880
|
-
}
|
|
941
|
+
};
|
|
942
|
+
if (currentTurnThinking) {
|
|
943
|
+
planAssistantMsg.thinking = currentTurnThinking;
|
|
944
|
+
}
|
|
945
|
+
this.conversationHistory.push(planAssistantMsg);
|
|
881
946
|
this.conversationHistory.push({
|
|
882
947
|
role: 'tool',
|
|
883
948
|
tool_call_id: toolCall.id,
|
|
@@ -897,11 +962,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
897
962
|
// Log error and add error result to history so AI knows
|
|
898
963
|
logWarning(`Failed to parse plan: ${parseError?.message || parseError}`);
|
|
899
964
|
// CRITICAL: Add tool result even on parse error to prevent silent loop
|
|
900
|
-
|
|
965
|
+
const errorAssistantMsg = {
|
|
901
966
|
role: 'assistant',
|
|
902
967
|
content: '',
|
|
903
968
|
tool_calls: [toolCall],
|
|
904
|
-
}
|
|
969
|
+
};
|
|
970
|
+
if (currentTurnThinking) {
|
|
971
|
+
errorAssistantMsg.thinking = currentTurnThinking;
|
|
972
|
+
}
|
|
973
|
+
this.conversationHistory.push(errorAssistantMsg);
|
|
905
974
|
this.conversationHistory.push({
|
|
906
975
|
role: 'tool',
|
|
907
976
|
tool_call_id: toolCall.id,
|
|
@@ -914,11 +983,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
914
983
|
}
|
|
915
984
|
else {
|
|
916
985
|
// Tool returned non-PLAN_CREATED result - add it to history
|
|
917
|
-
|
|
986
|
+
const resultAssistantMsg = {
|
|
918
987
|
role: 'assistant',
|
|
919
988
|
content: '',
|
|
920
989
|
tool_calls: [toolCall],
|
|
921
|
-
}
|
|
990
|
+
};
|
|
991
|
+
if (currentTurnThinking) {
|
|
992
|
+
resultAssistantMsg.thinking = currentTurnThinking;
|
|
993
|
+
}
|
|
994
|
+
this.conversationHistory.push(resultAssistantMsg);
|
|
922
995
|
this.conversationHistory.push({
|
|
923
996
|
role: 'tool',
|
|
924
997
|
tool_call_id: toolCall.id,
|
|
@@ -947,7 +1020,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
947
1020
|
const mainTaskNum = parseInt(taskNumParts[0], 10) - 1;
|
|
948
1021
|
const task = currentPlanData.steps[mainTaskNum];
|
|
949
1022
|
if (task) {
|
|
950
|
-
this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote
|
|
1023
|
+
this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote, completion.taskDescription // Pass the actual task/subtask description
|
|
1024
|
+
);
|
|
951
1025
|
}
|
|
952
1026
|
}
|
|
953
1027
|
// Notify UI about completed task/subtask
|
|
@@ -967,11 +1041,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
967
1041
|
result: `Task completed. Moving to Task ${nextPhase.taskNumber}: ${nextPhase.task.description}`,
|
|
968
1042
|
});
|
|
969
1043
|
// Add the tool call and result to history
|
|
970
|
-
|
|
1044
|
+
const nextPhaseAssistantMsg = {
|
|
971
1045
|
role: 'assistant',
|
|
972
1046
|
content: '',
|
|
973
1047
|
tool_calls: [toolCall],
|
|
974
|
-
}
|
|
1048
|
+
};
|
|
1049
|
+
if (currentTurnThinking) {
|
|
1050
|
+
nextPhaseAssistantMsg.thinking = currentTurnThinking;
|
|
1051
|
+
}
|
|
1052
|
+
this.conversationHistory.push(nextPhaseAssistantMsg);
|
|
975
1053
|
this.conversationHistory.push({
|
|
976
1054
|
role: 'tool',
|
|
977
1055
|
tool_call_id: toolCall.id,
|
|
@@ -1204,12 +1282,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1204
1282
|
}
|
|
1205
1283
|
// If user cancelled an operation, stop the agentic loop immediately
|
|
1206
1284
|
if (userCancelledOperation) {
|
|
1207
|
-
// Add assistant message to history
|
|
1208
|
-
|
|
1285
|
+
// Add assistant message to history with thinking if available
|
|
1286
|
+
const cancelledAssistantMsg = {
|
|
1209
1287
|
role: 'assistant',
|
|
1210
1288
|
content: assistantMessage || '',
|
|
1211
1289
|
tool_calls: toolCalls, // Store tool calls for MaaS models
|
|
1212
|
-
}
|
|
1290
|
+
};
|
|
1291
|
+
if (currentTurnThinking) {
|
|
1292
|
+
cancelledAssistantMsg.thinking = currentTurnThinking;
|
|
1293
|
+
}
|
|
1294
|
+
this.conversationHistory.push(cancelledAssistantMsg);
|
|
1213
1295
|
// Add tool results to history
|
|
1214
1296
|
for (const toolResult of toolResults) {
|
|
1215
1297
|
this.conversationHistory.push({
|
|
@@ -1230,13 +1312,13 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1230
1312
|
// Add assistant message with tool calls to conversation history
|
|
1231
1313
|
if (toolCalls && toolCalls.length > 0) {
|
|
1232
1314
|
try {
|
|
1233
|
-
|
|
1315
|
+
quickLog(`[${new Date().toISOString()}] [CLI] Adapting assistant message: has toolCalls=${toolCalls.length}, first=${JSON.stringify(toolCalls[0])}\n`);
|
|
1234
1316
|
}
|
|
1235
1317
|
catch (e) { }
|
|
1236
1318
|
}
|
|
1237
1319
|
else {
|
|
1238
1320
|
try {
|
|
1239
|
-
|
|
1321
|
+
quickLog(`[${new Date().toISOString()}] [CLI] Adapting assistant message: NO toolCalls\n`);
|
|
1240
1322
|
}
|
|
1241
1323
|
catch (e) { }
|
|
1242
1324
|
}
|
|
@@ -1247,11 +1329,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1247
1329
|
const unhandledToolResults = toolResults.filter(tr => !handledToolCallIds.has(tr.tool_call_id));
|
|
1248
1330
|
// Only add assistant message if there are unhandled tool calls
|
|
1249
1331
|
if (unhandledToolCalls.length > 0) {
|
|
1250
|
-
|
|
1332
|
+
const assistantHistoryMsg = {
|
|
1251
1333
|
role: 'assistant',
|
|
1252
1334
|
content: assistantMessage || '',
|
|
1253
1335
|
tool_calls: unhandledToolCalls, // Only include unhandled tool calls
|
|
1254
|
-
}
|
|
1336
|
+
};
|
|
1337
|
+
// Include thinking from this turn if available (Extended Thinking pattern)
|
|
1338
|
+
if (currentTurnThinking) {
|
|
1339
|
+
assistantHistoryMsg.thinking = currentTurnThinking;
|
|
1340
|
+
}
|
|
1341
|
+
this.conversationHistory.push(assistantHistoryMsg);
|
|
1255
1342
|
}
|
|
1256
1343
|
// Add tool results to conversation history as tool messages
|
|
1257
1344
|
// Format: { tool_call_id, name, result: <object or string> }
|
|
@@ -1266,8 +1353,10 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1266
1353
|
});
|
|
1267
1354
|
}
|
|
1268
1355
|
// Rebuild messages array with updated history
|
|
1269
|
-
//
|
|
1356
|
+
// During agent loop: keep ALL thinking for current task
|
|
1357
|
+
// (Thinking from previous tasks was already stripped at request start)
|
|
1270
1358
|
messages = [...this.conversationHistory];
|
|
1359
|
+
// No need to reset currentTurnThinking - keep accumulating for the task
|
|
1271
1360
|
// Re-inject subshell context
|
|
1272
1361
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
1273
1362
|
continue; // Loop back to AI service
|
|
@@ -1457,8 +1546,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1457
1546
|
role: 'assistant',
|
|
1458
1547
|
content: finalMessage,
|
|
1459
1548
|
});
|
|
1460
|
-
//
|
|
1461
|
-
await this.saveMessageToBackend('assistant', finalMessage);
|
|
1549
|
+
// Messages are stored locally only via saveCurrentChat() below
|
|
1462
1550
|
} // End of while loop
|
|
1463
1551
|
// Auto-save conversation to local storage
|
|
1464
1552
|
this.saveCurrentChat();
|
|
@@ -1499,11 +1587,13 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1499
1587
|
`/init - Analyze project and create/load centaurus.md context file\n` +
|
|
1500
1588
|
`/chat - Manage chat sessions (resume previous chats)\n` +
|
|
1501
1589
|
`/clear - Clear conversation and start a new chat\n` +
|
|
1590
|
+
`/sync - Sync data to/from cloud (upload/restore)\n` +
|
|
1502
1591
|
`/config - View current configuration\n` +
|
|
1503
1592
|
`/model - Select from available Google models\n` +
|
|
1504
1593
|
`/plan - Toggle plan mode for complex implementations\n` +
|
|
1505
1594
|
`/mcp - Manage configured MCP servers and tools\n` +
|
|
1506
1595
|
`/docs - Open Centaurus documentation in browser\n` +
|
|
1596
|
+
`/copy-chat-context - Copy chat as readable text to clipboard\n` +
|
|
1507
1597
|
`/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
|
|
1508
1598
|
`/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
|
|
1509
1599
|
`/sign-in - Sign in with Google (if not already signed in)\n` +
|
|
@@ -1865,12 +1955,12 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1865
1955
|
if (this.onShowPickerCallback) {
|
|
1866
1956
|
const config = this.configManager.load();
|
|
1867
1957
|
const currentModelName = config.modelName || '';
|
|
1868
|
-
//
|
|
1869
|
-
const
|
|
1958
|
+
// Fetch models from backend
|
|
1959
|
+
const modelsConfig = await fetchModelsConfig();
|
|
1870
1960
|
this.onShowPickerCallback({
|
|
1871
1961
|
message: 'Select Model',
|
|
1872
1962
|
type: 'model',
|
|
1873
|
-
choices:
|
|
1963
|
+
choices: modelsConfig.models.map((modelConfig, index) => ({
|
|
1874
1964
|
label: `${modelConfig.name} - ${modelConfig.description}${currentModelName === modelConfig.name ? ' [CURRENT]' : ''}`,
|
|
1875
1965
|
value: `${index}` // Use index as unique identifier
|
|
1876
1966
|
}))
|
|
@@ -1881,9 +1971,9 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1881
1971
|
else {
|
|
1882
1972
|
// Direct set via command argument
|
|
1883
1973
|
const newModel = args.join(' ');
|
|
1884
|
-
// Validate model
|
|
1885
|
-
if (!isValidModel(newModel)) {
|
|
1886
|
-
responseMessage = `ā ${getInvalidModelError(newModel)}`;
|
|
1974
|
+
// Validate model (async)
|
|
1975
|
+
if (!(await isValidModel(newModel))) {
|
|
1976
|
+
responseMessage = `ā ${await getInvalidModelError(newModel)}`;
|
|
1887
1977
|
break;
|
|
1888
1978
|
}
|
|
1889
1979
|
try {
|
|
@@ -1926,7 +2016,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1926
2016
|
// Show list of saved chats for resuming (interactive picker)
|
|
1927
2017
|
const chats = localChatStorage.listChats();
|
|
1928
2018
|
if (chats.length === 0) {
|
|
1929
|
-
responseMessage = '
|
|
2019
|
+
responseMessage = 'No saved chats found.\n\nStart a new conversation and it will be automatically saved!';
|
|
1930
2020
|
}
|
|
1931
2021
|
else {
|
|
1932
2022
|
// Format chat list for picker display
|
|
@@ -1936,13 +2026,13 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1936
2026
|
}
|
|
1937
2027
|
else {
|
|
1938
2028
|
// Fallback: show as text if no picker callback
|
|
1939
|
-
responseMessage = '
|
|
2029
|
+
responseMessage = 'Saved Chats:\n\n' +
|
|
1940
2030
|
chats.slice(0, 10).map((chat, i) => {
|
|
1941
2031
|
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
1942
2032
|
const time = new Date(chat.updatedAt).toLocaleTimeString();
|
|
1943
|
-
return `${i + 1}. ${chat.title}
|
|
1944
|
-
}).join('
|
|
1945
|
-
(chats.length > 10 ?
|
|
2033
|
+
return `${i + 1}. ${chat.title}\n ${date} ${time} | ${chat.messageCount} messages`;
|
|
2034
|
+
}).join('\n\n') +
|
|
2035
|
+
(chats.length > 10 ? `\n\n...and ${chats.length - 10} more chats` : '');
|
|
1946
2036
|
}
|
|
1947
2037
|
}
|
|
1948
2038
|
}
|
|
@@ -1950,7 +2040,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1950
2040
|
// Show read-only list of saved chats (no selection)
|
|
1951
2041
|
const chats = localChatStorage.listChats();
|
|
1952
2042
|
if (chats.length === 0) {
|
|
1953
|
-
responseMessage = '
|
|
2043
|
+
responseMessage = 'No saved chats found.\n\nStart a new conversation and it will be automatically saved!';
|
|
1954
2044
|
}
|
|
1955
2045
|
else {
|
|
1956
2046
|
// Format chat list for read-only display
|
|
@@ -1975,7 +2065,7 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1975
2065
|
// Show list of saved chats for deletion
|
|
1976
2066
|
const chats = localChatStorage.listChats();
|
|
1977
2067
|
if (chats.length === 0) {
|
|
1978
|
-
responseMessage = '
|
|
2068
|
+
responseMessage = 'No saved chats to delete.';
|
|
1979
2069
|
}
|
|
1980
2070
|
else {
|
|
1981
2071
|
// Format chat list for delete picker display
|
|
@@ -1995,13 +2085,13 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
1995
2085
|
if (this.onRestoreMessagesCallback) {
|
|
1996
2086
|
this.onRestoreMessagesCallback([]);
|
|
1997
2087
|
}
|
|
1998
|
-
responseMessage = '
|
|
2088
|
+
responseMessage = 'ā
Started a new chat session!\n\nYour previous conversation has been saved and can be resumed with /chat resume.';
|
|
1999
2089
|
}
|
|
2000
2090
|
else if (chatSubCommand === 'rename') {
|
|
2001
2091
|
// Show list of saved chats for renaming
|
|
2002
2092
|
const chats = localChatStorage.listChats();
|
|
2003
2093
|
if (chats.length === 0) {
|
|
2004
|
-
responseMessage = '
|
|
2094
|
+
responseMessage = 'No saved chats to rename.';
|
|
2005
2095
|
}
|
|
2006
2096
|
else {
|
|
2007
2097
|
// Format chat list for rename picker display
|
|
@@ -2018,6 +2108,30 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2018
2108
|
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
2109
|
}
|
|
2020
2110
|
break;
|
|
2111
|
+
case 'copy-chat-context':
|
|
2112
|
+
try {
|
|
2113
|
+
const { formatChatForClipboard } = await import('./utils/chat-formatter.js');
|
|
2114
|
+
const { copyTextToClipboard } = await import('./utils/text-clipboard.js');
|
|
2115
|
+
if (this.uiMessageHistory.length === 0) {
|
|
2116
|
+
responseMessage = 'No messages in current chat to copy.';
|
|
2117
|
+
}
|
|
2118
|
+
else {
|
|
2119
|
+
const formattedChat = formatChatForClipboard(this.uiMessageHistory);
|
|
2120
|
+
const success = await copyTextToClipboard(formattedChat);
|
|
2121
|
+
if (success) {
|
|
2122
|
+
responseMessage = 'ā
Chat content copied to clipboard!';
|
|
2123
|
+
}
|
|
2124
|
+
else {
|
|
2125
|
+
responseMessage = 'ā Failed to copy to clipboard.\n\n' +
|
|
2126
|
+
'This might happen if clipboard access is not available in your environment.\n' +
|
|
2127
|
+
'Try running the CLI in a terminal with clipboard access.';
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
catch (error) {
|
|
2132
|
+
responseMessage = `ā Error copying chat content: ${error.message}`;
|
|
2133
|
+
}
|
|
2134
|
+
break;
|
|
2021
2135
|
case 'exit':
|
|
2022
2136
|
process.exit(0);
|
|
2023
2137
|
break;
|
|
@@ -2084,6 +2198,189 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2084
2198
|
' /add-command delete <command> - Delete a custom command';
|
|
2085
2199
|
}
|
|
2086
2200
|
break;
|
|
2201
|
+
case 'background-task':
|
|
2202
|
+
case 'bkg':
|
|
2203
|
+
case 'bg-task':
|
|
2204
|
+
// Background task management commands
|
|
2205
|
+
const bkgSubCommand = args[0]?.toLowerCase();
|
|
2206
|
+
if (bkgSubCommand === 'list' || !bkgSubCommand) {
|
|
2207
|
+
// Show list of running background tasks
|
|
2208
|
+
const runningTasks = BackgroundTaskManager.getRunningTasks();
|
|
2209
|
+
if (runningTasks.length === 0) {
|
|
2210
|
+
responseMessage = 'š No background tasks running.\n\nSwitch to Background mode (Ctrl+D) to run commands in the background.';
|
|
2211
|
+
}
|
|
2212
|
+
else {
|
|
2213
|
+
// Show picker for task selection
|
|
2214
|
+
if (this.onShowBackgroundTaskPickerCallback) {
|
|
2215
|
+
this.onShowBackgroundTaskPickerCallback(runningTasks);
|
|
2216
|
+
return; // Don't send text response, picker will handle it
|
|
2217
|
+
}
|
|
2218
|
+
else {
|
|
2219
|
+
// Fallback: show as text
|
|
2220
|
+
responseMessage = 'š Running Background Tasks:\n\n' +
|
|
2221
|
+
runningTasks.map((task, i) => {
|
|
2222
|
+
const durationSec = Math.round(task.durationMs / 1000);
|
|
2223
|
+
return `${i + 1}. ${task.command}\n š ${task.cwd} | ā±ļø ${durationSec}s running`;
|
|
2224
|
+
}).join('\n\n');
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
else if (bkgSubCommand === 'cancel') {
|
|
2229
|
+
// Show list of running tasks for cancellation
|
|
2230
|
+
const runningTasks = BackgroundTaskManager.getRunningTasks();
|
|
2231
|
+
if (runningTasks.length === 0) {
|
|
2232
|
+
responseMessage = 'š No background tasks to cancel.';
|
|
2233
|
+
}
|
|
2234
|
+
else {
|
|
2235
|
+
// Show picker for task cancellation
|
|
2236
|
+
if (this.onShowBackgroundTaskCancelPickerCallback) {
|
|
2237
|
+
this.onShowBackgroundTaskCancelPickerCallback(runningTasks);
|
|
2238
|
+
return; // Don't send text response, picker will handle it
|
|
2239
|
+
}
|
|
2240
|
+
else {
|
|
2241
|
+
responseMessage = 'ā Cancel picker not available. Use /background-task list to see tasks.';
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
else {
|
|
2246
|
+
responseMessage = `Unknown /background-task subcommand: ${bkgSubCommand}\n\n` +
|
|
2247
|
+
'Usage:\n' +
|
|
2248
|
+
' /background-task list - List running background tasks\n' +
|
|
2249
|
+
' /background-task cancel - Cancel a running background task';
|
|
2250
|
+
}
|
|
2251
|
+
break;
|
|
2252
|
+
case 'sync':
|
|
2253
|
+
// Sync local data to/from cloud
|
|
2254
|
+
if (!apiClient.isAuthenticated()) {
|
|
2255
|
+
responseMessage = 'ā You must be signed in to sync data.\n\nUse /sign-in to authenticate first.';
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2258
|
+
// Parse subcommand
|
|
2259
|
+
const syncSubcommand = args[0]?.toLowerCase();
|
|
2260
|
+
if (!syncSubcommand) {
|
|
2261
|
+
// Show sync help
|
|
2262
|
+
responseMessage = 'āļø Sync Commands:\n\n' +
|
|
2263
|
+
' /sync upload - Upload local data to cloud (overwrites cloud data)\n' +
|
|
2264
|
+
' /sync restore - Download cloud data and restore locally (overwrites local data)\n\n' +
|
|
2265
|
+
'Use these commands to backup or restore your chat history and settings.';
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
if (syncSubcommand === 'upload') {
|
|
2269
|
+
try {
|
|
2270
|
+
responseMessage = 'āļø Uploading data to cloud...';
|
|
2271
|
+
// Send initial response
|
|
2272
|
+
if (this.onDirectMessageCallback) {
|
|
2273
|
+
this.onDirectMessageCallback(responseMessage);
|
|
2274
|
+
}
|
|
2275
|
+
// Gather all local data
|
|
2276
|
+
const config = this.configManager.load();
|
|
2277
|
+
const chats = localChatStorage.listChats();
|
|
2278
|
+
// Load full chat data for each chat
|
|
2279
|
+
const fullChats = chats.map(chatMeta => {
|
|
2280
|
+
const fullChat = localChatStorage.loadChat(chatMeta.id);
|
|
2281
|
+
return fullChat;
|
|
2282
|
+
}).filter(chat => chat !== null);
|
|
2283
|
+
// Create sync data structure
|
|
2284
|
+
const syncData = {
|
|
2285
|
+
version: 1,
|
|
2286
|
+
exportedAt: new Date().toISOString(),
|
|
2287
|
+
config: {
|
|
2288
|
+
model: config.model,
|
|
2289
|
+
modelName: config.modelName,
|
|
2290
|
+
enhancedQuality: config.enhancedQuality,
|
|
2291
|
+
autonomousMode: config.autonomousMode,
|
|
2292
|
+
},
|
|
2293
|
+
chats: fullChats,
|
|
2294
|
+
metadata: {
|
|
2295
|
+
totalChats: fullChats.length,
|
|
2296
|
+
totalMessages: fullChats.reduce((sum, chat) => sum + (chat?.messageCount || 0), 0),
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
// Upload to backend
|
|
2300
|
+
const result = await apiClient.uploadSyncData(syncData);
|
|
2301
|
+
responseMessage = `ā
Data uploaded successfully!\n\n` +
|
|
2302
|
+
`š Upload Summary:\n` +
|
|
2303
|
+
` ⢠Chats: ${fullChats.length}\n` +
|
|
2304
|
+
` ⢠Messages: ${syncData.metadata.totalMessages}\n` +
|
|
2305
|
+
` ⢠Version: ${result.version}\n` +
|
|
2306
|
+
` ⢠Updated: ${new Date(result.updatedAt).toLocaleString()}\n\n` +
|
|
2307
|
+
`Your local data is now backed up to the cloud.`;
|
|
2308
|
+
}
|
|
2309
|
+
catch (error) {
|
|
2310
|
+
responseMessage = `ā Upload failed: ${error.message}\n\nPlease try again later.`;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
else if (syncSubcommand === 'restore') {
|
|
2314
|
+
try {
|
|
2315
|
+
responseMessage = 'āļø Downloading data from cloud...';
|
|
2316
|
+
// Send initial response
|
|
2317
|
+
if (this.onDirectMessageCallback) {
|
|
2318
|
+
this.onDirectMessageCallback(responseMessage);
|
|
2319
|
+
}
|
|
2320
|
+
// Get sync data from backend
|
|
2321
|
+
const cloudData = await apiClient.getSyncData();
|
|
2322
|
+
if (!cloudData) {
|
|
2323
|
+
responseMessage = 'ā No cloud data found.\n\nUse /sync upload first to backup your data.';
|
|
2324
|
+
break;
|
|
2325
|
+
}
|
|
2326
|
+
const syncData = cloudData.syncData;
|
|
2327
|
+
// Validate sync data structure
|
|
2328
|
+
if (!syncData || typeof syncData !== 'object') {
|
|
2329
|
+
responseMessage = 'ā Invalid cloud data format.\n\nPlease try uploading again with /sync upload.';
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
// Restore config if present
|
|
2333
|
+
if (syncData.config) {
|
|
2334
|
+
const currentConfig = this.configManager.load();
|
|
2335
|
+
if (syncData.config.model) {
|
|
2336
|
+
currentConfig.model = syncData.config.model;
|
|
2337
|
+
}
|
|
2338
|
+
if (syncData.config.modelName) {
|
|
2339
|
+
currentConfig.modelName = syncData.config.modelName;
|
|
2340
|
+
}
|
|
2341
|
+
if (typeof syncData.config.enhancedQuality === 'boolean') {
|
|
2342
|
+
currentConfig.enhancedQuality = syncData.config.enhancedQuality;
|
|
2343
|
+
}
|
|
2344
|
+
if (typeof syncData.config.autonomousMode === 'boolean') {
|
|
2345
|
+
currentConfig.autonomousMode = syncData.config.autonomousMode;
|
|
2346
|
+
}
|
|
2347
|
+
this.configManager.save(currentConfig);
|
|
2348
|
+
}
|
|
2349
|
+
// Restore chats if present
|
|
2350
|
+
let restoredChats = 0;
|
|
2351
|
+
let restoredMessages = 0;
|
|
2352
|
+
if (syncData.chats && Array.isArray(syncData.chats)) {
|
|
2353
|
+
for (const chat of syncData.chats) {
|
|
2354
|
+
if (chat && chat.id) {
|
|
2355
|
+
// Import each chat to local storage
|
|
2356
|
+
const success = localChatStorage.importChat(chat);
|
|
2357
|
+
if (success) {
|
|
2358
|
+
restoredChats++;
|
|
2359
|
+
restoredMessages += chat.messageCount || chat.messages?.length || 0;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
responseMessage = `ā
Data restored successfully!\n\n` +
|
|
2365
|
+
`š Restore Summary:\n` +
|
|
2366
|
+
` ⢠Chats restored: ${restoredChats}\n` +
|
|
2367
|
+
` ⢠Messages restored: ${restoredMessages}\n` +
|
|
2368
|
+
` ⢠Cloud version: ${cloudData.version}\n` +
|
|
2369
|
+
` ⢠Last updated: ${new Date(cloudData.updatedAt).toLocaleString()}\n\n` +
|
|
2370
|
+
`Your local data has been restored from the cloud.\n` +
|
|
2371
|
+
`Note: Existing local chats with the same ID were overwritten.`;
|
|
2372
|
+
}
|
|
2373
|
+
catch (error) {
|
|
2374
|
+
responseMessage = `ā Restore failed: ${error.message}\n\nPlease try again later.`;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
else {
|
|
2378
|
+
responseMessage = `ā Unknown sync subcommand: ${syncSubcommand}\n\n` +
|
|
2379
|
+
'Available subcommands:\n' +
|
|
2380
|
+
' /sync upload - Upload local data to cloud\n' +
|
|
2381
|
+
' /sync restore - Download and restore cloud data locally';
|
|
2382
|
+
}
|
|
2383
|
+
break;
|
|
2087
2384
|
default:
|
|
2088
2385
|
responseMessage = `Unknown command: /${cmd}\nType /help for available commands.`;
|
|
2089
2386
|
}
|
|
@@ -2116,6 +2413,139 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2116
2413
|
setOnShowChatRenamePickerCallback(callback) {
|
|
2117
2414
|
this.onShowChatRenamePickerCallback = callback;
|
|
2118
2415
|
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Set callback for background mode change
|
|
2418
|
+
*/
|
|
2419
|
+
setOnBackgroundModeChange(callback) {
|
|
2420
|
+
this.onBackgroundModeChange = callback;
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Set callback for background task count change
|
|
2424
|
+
*/
|
|
2425
|
+
setOnBackgroundTaskCountChange(callback) {
|
|
2426
|
+
this.onBackgroundTaskCountChange = callback;
|
|
2427
|
+
// Subscribe to BackgroundTaskManager count changes
|
|
2428
|
+
BackgroundTaskManager.on('countChanged', (count) => {
|
|
2429
|
+
this.onBackgroundTaskCountChange?.(count);
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Set callback for setting Auto mode (used after background task starts)
|
|
2434
|
+
*/
|
|
2435
|
+
setOnSetAutoMode(callback) {
|
|
2436
|
+
this.onSetAutoMode = callback;
|
|
2437
|
+
}
|
|
2438
|
+
/**
|
|
2439
|
+
* Set callback for showing background task picker
|
|
2440
|
+
*/
|
|
2441
|
+
setOnShowBackgroundTaskPickerCallback(callback) {
|
|
2442
|
+
this.onShowBackgroundTaskPickerCallback = callback;
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Set callback for showing background task cancel picker
|
|
2446
|
+
*/
|
|
2447
|
+
setOnShowBackgroundTaskCancelPickerCallback(callback) {
|
|
2448
|
+
this.onShowBackgroundTaskCancelPickerCallback = callback;
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Toggle background mode
|
|
2452
|
+
*/
|
|
2453
|
+
toggleBackgroundMode() {
|
|
2454
|
+
this.backgroundMode = !this.backgroundMode;
|
|
2455
|
+
if (this.onBackgroundModeChange) {
|
|
2456
|
+
this.onBackgroundModeChange(this.backgroundMode);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
/**
|
|
2460
|
+
* Get current background mode state
|
|
2461
|
+
*/
|
|
2462
|
+
getBackgroundMode() {
|
|
2463
|
+
return this.backgroundMode;
|
|
2464
|
+
}
|
|
2465
|
+
/**
|
|
2466
|
+
* Handle background task cancellation
|
|
2467
|
+
*/
|
|
2468
|
+
handleBackgroundTaskCancel(taskId) {
|
|
2469
|
+
const task = BackgroundTaskManager.getTask(taskId);
|
|
2470
|
+
const success = BackgroundTaskManager.cancelTask(taskId);
|
|
2471
|
+
if (success) {
|
|
2472
|
+
if (this.onDirectMessageCallback) {
|
|
2473
|
+
this.onDirectMessageCallback(`š Cancelled background task: ${task?.command || taskId}`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
else {
|
|
2477
|
+
if (this.onDirectMessageCallback) {
|
|
2478
|
+
this.onDirectMessageCallback(`ā Failed to cancel task. It may have already completed.`);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Handle background task selection - show task output in focus mode
|
|
2484
|
+
*/
|
|
2485
|
+
handleBackgroundTaskSelection(taskId) {
|
|
2486
|
+
const task = BackgroundTaskManager.getTask(taskId);
|
|
2487
|
+
if (!task) {
|
|
2488
|
+
if (this.onDirectMessageCallback) {
|
|
2489
|
+
this.onDirectMessageCallback(`ā Task not found: ${taskId}`);
|
|
2490
|
+
}
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
// Get the full output from the task
|
|
2494
|
+
const output = BackgroundTaskManager.getTaskOutput(taskId);
|
|
2495
|
+
// If we have the view callback, use it to show in focus mode (InteractiveShell)
|
|
2496
|
+
if (this.onBackgroundTaskViewCallback) {
|
|
2497
|
+
// Create event handlers for the App.tsx useEffect to subscribe to
|
|
2498
|
+
let onOutputHandler;
|
|
2499
|
+
let onCompleteHandler;
|
|
2500
|
+
// Subscribe to future output updates for this task
|
|
2501
|
+
const outputListener = (taskIdEvent, chunk) => {
|
|
2502
|
+
if (taskIdEvent === taskId && onOutputHandler) {
|
|
2503
|
+
onOutputHandler(chunk);
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
// Subscribe to task completion
|
|
2507
|
+
const completeListener = (taskIdEvent, exitCode) => {
|
|
2508
|
+
if (taskIdEvent === taskId && onCompleteHandler) {
|
|
2509
|
+
onCompleteHandler(exitCode);
|
|
2510
|
+
// Cleanup listeners
|
|
2511
|
+
BackgroundTaskManager.off('taskOutput', outputListener);
|
|
2512
|
+
BackgroundTaskManager.off('taskCompleted', completeListener);
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
BackgroundTaskManager.on('taskOutput', outputListener);
|
|
2516
|
+
BackgroundTaskManager.on('taskCompleted', completeListener);
|
|
2517
|
+
// Invoke the callback to set up shellState in App.tsx
|
|
2518
|
+
this.onBackgroundTaskViewCallback({
|
|
2519
|
+
id: taskId,
|
|
2520
|
+
command: task.command,
|
|
2521
|
+
cwd: task.cwd,
|
|
2522
|
+
output,
|
|
2523
|
+
isRunning: task.isRunning,
|
|
2524
|
+
onOutput: (handler) => { onOutputHandler = handler; },
|
|
2525
|
+
onComplete: (handler) => { onCompleteHandler = handler; }
|
|
2526
|
+
});
|
|
2527
|
+
// If task is already complete, immediately trigger completion
|
|
2528
|
+
if (!task.isRunning && task.exitCode !== undefined) {
|
|
2529
|
+
setTimeout(() => {
|
|
2530
|
+
if (onCompleteHandler) {
|
|
2531
|
+
onCompleteHandler(task.exitCode);
|
|
2532
|
+
}
|
|
2533
|
+
}, 100);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
else {
|
|
2537
|
+
// Fallback: show as static message
|
|
2538
|
+
if (this.onDirectMessageCallback) {
|
|
2539
|
+
this.onDirectMessageCallback(`šŗ Viewing output for: ${task.command}\n\n${output || '(no output yet)'}`);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Set callback for showing a background task in focus mode
|
|
2545
|
+
*/
|
|
2546
|
+
setOnBackgroundTaskViewCallback(callback) {
|
|
2547
|
+
this.onBackgroundTaskViewCallback = callback;
|
|
2548
|
+
}
|
|
2119
2549
|
/**
|
|
2120
2550
|
* Handle chat rename operation
|
|
2121
2551
|
*/
|
|
@@ -2177,6 +2607,104 @@ Start by listing the directory structure to understand what you're working with.
|
|
|
2177
2607
|
}
|
|
2178
2608
|
}
|
|
2179
2609
|
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Handle background mode execution - runs command in background and returns immediately
|
|
2612
|
+
* Supports both local and remote shells (SSH, WSL, Docker)
|
|
2613
|
+
*/
|
|
2614
|
+
handleBackgroundModeExecution(command) {
|
|
2615
|
+
if (!command.trim()) {
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
// Get current context to determine if we're in a remote shell
|
|
2619
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
2620
|
+
let taskId;
|
|
2621
|
+
let effectiveCwd;
|
|
2622
|
+
let remoteContextDisplay;
|
|
2623
|
+
if (currentContext.type === 'local') {
|
|
2624
|
+
// Local execution - use PTY
|
|
2625
|
+
effectiveCwd = this.cwd;
|
|
2626
|
+
taskId = BackgroundTaskManager.startTask(command, effectiveCwd);
|
|
2627
|
+
}
|
|
2628
|
+
else {
|
|
2629
|
+
// Remote execution - use appropriate remote PTY
|
|
2630
|
+
const metadata = currentContext.metadata;
|
|
2631
|
+
effectiveCwd = metadata?.workingDirectory || '~';
|
|
2632
|
+
// Build remote context display string
|
|
2633
|
+
if (currentContext.type === 'ssh' && metadata) {
|
|
2634
|
+
remoteContextDisplay = `${metadata.username || 'user'}@${metadata.hostname || 'remote'}`;
|
|
2635
|
+
}
|
|
2636
|
+
else if (currentContext.type === 'wsl' && metadata) {
|
|
2637
|
+
remoteContextDisplay = `wsl:${metadata.distroName || 'wsl'}`;
|
|
2638
|
+
}
|
|
2639
|
+
else if (currentContext.type === 'docker' && metadata) {
|
|
2640
|
+
remoteContextDisplay = `docker:${metadata.containerId?.substring(0, 12) || 'container'}`;
|
|
2641
|
+
}
|
|
2642
|
+
// Create the remote PTY process and register with BackgroundTaskManager
|
|
2643
|
+
if (currentContext.type === 'ssh') {
|
|
2644
|
+
const sshClient = currentContext.handler?.client;
|
|
2645
|
+
if (!sshClient) {
|
|
2646
|
+
if (this.onResponseCallback) {
|
|
2647
|
+
this.onResponseCallback('ā SSH client not available for background task');
|
|
2648
|
+
}
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
// Start remote task first to get callbacks
|
|
2652
|
+
const remoteTask = BackgroundTaskManager.startRemoteTask(command, effectiveCwd, remoteContextDisplay || 'ssh');
|
|
2653
|
+
taskId = remoteTask.id;
|
|
2654
|
+
// Create SSH PTY with the callbacks from BackgroundTaskManager
|
|
2655
|
+
const sshPty = runSSHCommand(sshClient, command, effectiveCwd, remoteTask.onData, remoteTask.onExit);
|
|
2656
|
+
remoteTask.setRemotePty(sshPty);
|
|
2657
|
+
}
|
|
2658
|
+
else if (currentContext.type === 'wsl') {
|
|
2659
|
+
const distribution = metadata?.distroName || 'Ubuntu';
|
|
2660
|
+
// Start remote task first to get callbacks
|
|
2661
|
+
const remoteTask = BackgroundTaskManager.startRemoteTask(command, effectiveCwd, remoteContextDisplay || 'wsl');
|
|
2662
|
+
taskId = remoteTask.id;
|
|
2663
|
+
// Create WSL PTY with the callbacks from BackgroundTaskManager
|
|
2664
|
+
const wslPty = runWSLCommand(distribution, command, effectiveCwd, remoteTask.onData, remoteTask.onExit);
|
|
2665
|
+
remoteTask.setRemotePty(wslPty);
|
|
2666
|
+
}
|
|
2667
|
+
else if (currentContext.type === 'docker') {
|
|
2668
|
+
const containerId = metadata?.containerId;
|
|
2669
|
+
if (!containerId) {
|
|
2670
|
+
if (this.onResponseCallback) {
|
|
2671
|
+
this.onResponseCallback('ā Docker container ID not available for background task');
|
|
2672
|
+
}
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
// Start remote task first to get callbacks
|
|
2676
|
+
const remoteTask = BackgroundTaskManager.startRemoteTask(command, effectiveCwd, remoteContextDisplay || 'docker');
|
|
2677
|
+
taskId = remoteTask.id;
|
|
2678
|
+
// Create Docker PTY with the callbacks from BackgroundTaskManager
|
|
2679
|
+
const dockerPty = runDockerCommand(containerId, command, effectiveCwd, remoteTask.onData, remoteTask.onExit);
|
|
2680
|
+
remoteTask.setRemotePty(dockerPty);
|
|
2681
|
+
}
|
|
2682
|
+
else {
|
|
2683
|
+
// Unknown remote type - fall back to local
|
|
2684
|
+
effectiveCwd = this.cwd;
|
|
2685
|
+
taskId = BackgroundTaskManager.startTask(command, effectiveCwd);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
// Notify user that task started
|
|
2689
|
+
const count = BackgroundTaskManager.getRunningCount();
|
|
2690
|
+
const locationInfo = remoteContextDisplay
|
|
2691
|
+
? `\nš Running on: ${remoteContextDisplay}`
|
|
2692
|
+
: '';
|
|
2693
|
+
if (this.onDirectMessageCallback) {
|
|
2694
|
+
this.onDirectMessageCallback(`š Background task started: ${command}${locationInfo}\n\nTask ID: ${taskId}\nUse /background-task list to see running tasks.`);
|
|
2695
|
+
}
|
|
2696
|
+
// Optionally record to history for AI context
|
|
2697
|
+
this.recordShellCommandToHistory(command, '[Running in background]', effectiveCwd);
|
|
2698
|
+
// Reset to Auto mode after starting task
|
|
2699
|
+
this.backgroundMode = false;
|
|
2700
|
+
if (this.onBackgroundModeChange) {
|
|
2701
|
+
this.onBackgroundModeChange(false);
|
|
2702
|
+
}
|
|
2703
|
+
// Enable Auto mode in InputBox
|
|
2704
|
+
if (this.onSetAutoMode) {
|
|
2705
|
+
this.onSetAutoMode(true);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2180
2708
|
/**
|
|
2181
2709
|
* Record a user shell command and its output to conversation history
|
|
2182
2710
|
* This allows the AI to see what commands the user ran in command mode
|