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.
Files changed (134) hide show
  1. package/dist/cli-adapter.d.ts +82 -2
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +622 -94
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/ConfigManager.d.ts.map +1 -1
  6. package/dist/config/ConfigManager.js +6 -5
  7. package/dist/config/ConfigManager.js.map +1 -1
  8. package/dist/config/build-config.d.ts +42 -0
  9. package/dist/config/build-config.d.ts.map +1 -0
  10. package/dist/config/build-config.js +44 -0
  11. package/dist/config/build-config.js.map +1 -0
  12. package/dist/config/manager.d.ts +2 -2
  13. package/dist/config/manager.d.ts.map +1 -1
  14. package/dist/config/manager.js +9 -12
  15. package/dist/config/manager.js.map +1 -1
  16. package/dist/config/mcp-config-manager.d.ts +5 -0
  17. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  18. package/dist/config/mcp-config-manager.js +8 -0
  19. package/dist/config/mcp-config-manager.js.map +1 -1
  20. package/dist/config/models.d.ts +48 -42
  21. package/dist/config/models.d.ts.map +1 -1
  22. package/dist/config/models.js +148 -133
  23. package/dist/config/models.js.map +1 -1
  24. package/dist/config/slash-commands.d.ts +2 -0
  25. package/dist/config/slash-commands.d.ts.map +1 -1
  26. package/dist/config/slash-commands.js +34 -1
  27. package/dist/config/slash-commands.js.map +1 -1
  28. package/dist/context/context-manager.d.ts.map +1 -1
  29. package/dist/context/context-manager.js +6 -6
  30. package/dist/context/context-manager.js.map +1 -1
  31. package/dist/hooks/useConnectivity.d.ts +2 -0
  32. package/dist/hooks/useConnectivity.d.ts.map +1 -0
  33. package/dist/hooks/useConnectivity.js +12 -0
  34. package/dist/hooks/useConnectivity.js.map +1 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +37 -23
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  40. package/dist/mcp/mcp-command-handler.js +2 -5
  41. package/dist/mcp/mcp-command-handler.js.map +1 -1
  42. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  43. package/dist/mcp/mcp-tool-wrapper.js +8 -0
  44. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  45. package/dist/services/ai-service-client.d.ts +1 -0
  46. package/dist/services/ai-service-client.d.ts.map +1 -1
  47. package/dist/services/ai-service-client.js +8 -6
  48. package/dist/services/ai-service-client.js.map +1 -1
  49. package/dist/services/api-client.d.ts +46 -34
  50. package/dist/services/api-client.d.ts.map +1 -1
  51. package/dist/services/api-client.js +47 -37
  52. package/dist/services/api-client.js.map +1 -1
  53. package/dist/services/background-task-manager.d.ts +114 -0
  54. package/dist/services/background-task-manager.d.ts.map +1 -0
  55. package/dist/services/background-task-manager.js +301 -0
  56. package/dist/services/background-task-manager.js.map +1 -0
  57. package/dist/services/connectivity-manager.d.ts +18 -0
  58. package/dist/services/connectivity-manager.d.ts.map +1 -0
  59. package/dist/services/connectivity-manager.js +72 -0
  60. package/dist/services/connectivity-manager.js.map +1 -0
  61. package/dist/services/local-chat-storage.d.ts +5 -0
  62. package/dist/services/local-chat-storage.d.ts.map +1 -1
  63. package/dist/services/local-chat-storage.js +38 -4
  64. package/dist/services/local-chat-storage.js.map +1 -1
  65. package/dist/tools/background-command.d.ts +11 -0
  66. package/dist/tools/background-command.d.ts.map +1 -0
  67. package/dist/tools/background-command.js +162 -0
  68. package/dist/tools/background-command.js.map +1 -0
  69. package/dist/tools/command.d.ts.map +1 -1
  70. package/dist/tools/command.js +6 -3
  71. package/dist/tools/command.js.map +1 -1
  72. package/dist/tools/create-image.d.ts +10 -0
  73. package/dist/tools/create-image.d.ts.map +1 -0
  74. package/dist/tools/create-image.js +189 -0
  75. package/dist/tools/create-image.js.map +1 -0
  76. package/dist/tools/web-search.d.ts.map +1 -1
  77. package/dist/tools/web-search.js +8 -6
  78. package/dist/tools/web-search.js.map +1 -1
  79. package/dist/ui/components/App.d.ts +34 -2
  80. package/dist/ui/components/App.d.ts.map +1 -1
  81. package/dist/ui/components/App.js +403 -67
  82. package/dist/ui/components/App.js.map +1 -1
  83. package/dist/ui/components/ContextWindowIndicator.d.ts.map +1 -1
  84. package/dist/ui/components/ContextWindowIndicator.js +43 -22
  85. package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
  86. package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
  87. package/dist/ui/components/ErrorBoundary.js +2 -1
  88. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  89. package/dist/ui/components/InputBox.d.ts +4 -0
  90. package/dist/ui/components/InputBox.d.ts.map +1 -1
  91. package/dist/ui/components/InputBox.js +289 -207
  92. package/dist/ui/components/InputBox.js.map +1 -1
  93. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  94. package/dist/ui/components/MessageDisplay.js +8 -15
  95. package/dist/ui/components/MessageDisplay.js.map +1 -1
  96. package/dist/ui/components/SlashCommandAutocomplete.d.ts +2 -0
  97. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -1
  98. package/dist/ui/components/SlashCommandAutocomplete.js +19 -10
  99. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  100. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  101. package/dist/ui/components/StatusBar.js +4 -0
  102. package/dist/ui/components/StatusBar.js.map +1 -1
  103. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  104. package/dist/ui/components/ToolExecutionMessage.js +155 -41
  105. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  106. package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
  107. package/dist/ui/components/ToolExecutionStatus.js +1 -0
  108. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  109. package/dist/utils/chat-formatter.d.ts +12 -0
  110. package/dist/utils/chat-formatter.d.ts.map +1 -0
  111. package/dist/utils/chat-formatter.js +326 -0
  112. package/dist/utils/chat-formatter.js.map +1 -0
  113. package/dist/utils/command-history.d.ts.map +1 -1
  114. package/dist/utils/command-history.js +2 -1
  115. package/dist/utils/command-history.js.map +1 -1
  116. package/dist/utils/conversation-logger.d.ts +15 -0
  117. package/dist/utils/conversation-logger.d.ts.map +1 -1
  118. package/dist/utils/conversation-logger.js +56 -2
  119. package/dist/utils/conversation-logger.js.map +1 -1
  120. package/dist/utils/editor-utils.d.ts.map +1 -1
  121. package/dist/utils/editor-utils.js +3 -2
  122. package/dist/utils/editor-utils.js.map +1 -1
  123. package/dist/utils/input-classifier.d.ts.map +1 -1
  124. package/dist/utils/input-classifier.js +140 -20
  125. package/dist/utils/input-classifier.js.map +1 -1
  126. package/dist/utils/logger.d.ts.map +1 -1
  127. package/dist/utils/logger.js +31 -1
  128. package/dist/utils/logger.js.map +1 -1
  129. package/dist/utils/text-clipboard.d.ts +12 -0
  130. package/dist/utils/text-clipboard.d.ts.map +1 -0
  131. package/dist/utils/text-clipboard.js +63 -0
  132. package/dist/utils/text-clipboard.js.map +1 -0
  133. package/package.json +1 -2
  134. package/models-config.json +0 -126
@@ -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 ALL_MODELS array
233
- const { ALL_MODELS } = await import('./config/models.js');
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 >= ALL_MODELS.length) {
249
+ if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >= modelsConfig.models.length) {
236
250
  throw new Error('Invalid model selection');
237
251
  }
238
- const selectedModel = ALL_MODELS[modelIndex];
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
- if (this.conversationStarted || !apiClient.isAuthenticated()) {
402
- return;
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
- if (!apiClient.isAuthenticated()) {
426
- return;
427
- }
428
- try {
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
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Cleaning up orphaned tool_calls: ${Array.from(toolCallIds).join(', ')}\n`);
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
- // Save user message to backend (save original message for display, not the prefixed version)
546
- await this.saveMessageToBackend('user', message);
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 models-config.json
573
- // This ensures we always use the latest config, not a cached version
574
- const { ALL_MODELS } = await import('./config/models.js');
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) => sum + (typeof m.content === 'string' ? m.content.length : 0), 0),
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
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] === TURN ${turnCount} AI CALL ===\n`);
633
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Message history: ${messageStats.totalMessages} messages, ${messageStats.totalCharacters} chars\n`);
634
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] By role: system=${messageStats.byRole.system}, user=${messageStats.byRole.user}, assistant=${messageStats.byRole.assistant}, tool=${messageStats.byRole.tool}\n`);
635
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
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
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED: ${toolCall?.name || 'unknown'}, toolCalls.length before push: ${toolCalls.length}\n`);
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
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** TOOL_CALL PUSHED: ${toolCall?.name || 'unknown'}, toolCalls.length after push: ${toolCalls.length}\n`);
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
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] *** ABOUT TO EXECUTE TOOL [${i + 1}/${toolCalls.length}]: ${toolCalls[i].name}\n`);
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
- this.conversationHistory.push({
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
- this.conversationHistory.push({
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
- this.conversationHistory.push({
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
- this.conversationHistory.push({
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
- this.conversationHistory.push({
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
- this.conversationHistory.push({
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
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Adapting assistant message: has toolCalls=${toolCalls.length}, first=${JSON.stringify(toolCalls[0])}\n`);
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
- fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [CLI] Adapting assistant message: NO toolCalls\n`);
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
- this.conversationHistory.push({
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
- // Backend will inject system prompt
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
- // Save assistant message to backend
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
- // Import ALL_MODELS to show all variants including duplicates
1869
- const { ALL_MODELS } = await import('./config/models.js');
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: ALL_MODELS.map((modelConfig, index) => ({
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 = 'šŸ“­ No saved chats found.\\n\\nStart a new conversation and it will be automatically saved!';
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 = 'šŸ“š Saved Chats:\\n\\n' +
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}\\n šŸ“… ${date} ${time} | šŸ’¬ ${chat.messageCount} messages`;
1944
- }).join('\\n\\n') +
1945
- (chats.length > 10 ? `\\n\\n...and ${chats.length - 10} more chats` : '');
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 = 'šŸ“­ No saved chats found.\\n\\nStart a new conversation and it will be automatically saved!';
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 = 'šŸ“­ No saved chats to delete.';
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 = 'šŸ†• Started a new chat session!\n\nYour previous conversation has been saved and can be resumed with /chat resume.';
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 = 'šŸ“­ No saved chats to rename.';
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