centaurus-cli 2.8.9 → 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 (88) hide show
  1. package/dist/cli-adapter.d.ts +21 -2
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +302 -68
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/models.d.ts +8 -0
  6. package/dist/config/models.d.ts.map +1 -1
  7. package/dist/config/models.js +29 -0
  8. package/dist/config/models.js.map +1 -1
  9. package/dist/config/slash-commands.d.ts +1 -0
  10. package/dist/config/slash-commands.d.ts.map +1 -1
  11. package/dist/config/slash-commands.js +13 -1
  12. package/dist/config/slash-commands.js.map +1 -1
  13. package/dist/hooks/useConnectivity.d.ts +2 -0
  14. package/dist/hooks/useConnectivity.d.ts.map +1 -0
  15. package/dist/hooks/useConnectivity.js +12 -0
  16. package/dist/hooks/useConnectivity.js.map +1 -0
  17. package/dist/index.js +3 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  20. package/dist/mcp/mcp-command-handler.js +0 -3
  21. package/dist/mcp/mcp-command-handler.js.map +1 -1
  22. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  23. package/dist/mcp/mcp-tool-wrapper.js +8 -0
  24. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  25. package/dist/services/ai-service-client.d.ts +1 -0
  26. package/dist/services/ai-service-client.d.ts.map +1 -1
  27. package/dist/services/ai-service-client.js.map +1 -1
  28. package/dist/services/api-client.d.ts +26 -40
  29. package/dist/services/api-client.d.ts.map +1 -1
  30. package/dist/services/api-client.js +32 -34
  31. package/dist/services/api-client.js.map +1 -1
  32. package/dist/services/connectivity-manager.d.ts +18 -0
  33. package/dist/services/connectivity-manager.d.ts.map +1 -0
  34. package/dist/services/connectivity-manager.js +72 -0
  35. package/dist/services/connectivity-manager.js.map +1 -0
  36. package/dist/services/local-chat-storage.d.ts +5 -0
  37. package/dist/services/local-chat-storage.d.ts.map +1 -1
  38. package/dist/services/local-chat-storage.js +33 -0
  39. package/dist/services/local-chat-storage.js.map +1 -1
  40. package/dist/tools/background-command.d.ts +11 -0
  41. package/dist/tools/background-command.d.ts.map +1 -0
  42. package/dist/tools/background-command.js +162 -0
  43. package/dist/tools/background-command.js.map +1 -0
  44. package/dist/tools/command.d.ts.map +1 -1
  45. package/dist/tools/command.js +6 -3
  46. package/dist/tools/command.js.map +1 -1
  47. package/dist/tools/create-image.d.ts +10 -0
  48. package/dist/tools/create-image.d.ts.map +1 -0
  49. package/dist/tools/create-image.js +189 -0
  50. package/dist/tools/create-image.js.map +1 -0
  51. package/dist/ui/components/App.d.ts +3 -2
  52. package/dist/ui/components/App.d.ts.map +1 -1
  53. package/dist/ui/components/App.js +127 -44
  54. package/dist/ui/components/App.js.map +1 -1
  55. package/dist/ui/components/ContextWindowIndicator.d.ts.map +1 -1
  56. package/dist/ui/components/ContextWindowIndicator.js +43 -22
  57. package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
  58. package/dist/ui/components/InputBox.d.ts.map +1 -1
  59. package/dist/ui/components/InputBox.js +198 -199
  60. package/dist/ui/components/InputBox.js.map +1 -1
  61. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  62. package/dist/ui/components/MessageDisplay.js +8 -15
  63. package/dist/ui/components/MessageDisplay.js.map +1 -1
  64. package/dist/ui/components/SlashCommandAutocomplete.d.ts +2 -0
  65. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -1
  66. package/dist/ui/components/SlashCommandAutocomplete.js +19 -10
  67. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  68. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  69. package/dist/ui/components/StatusBar.js +4 -0
  70. package/dist/ui/components/StatusBar.js.map +1 -1
  71. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  72. package/dist/ui/components/ToolExecutionMessage.js +153 -39
  73. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  74. package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
  75. package/dist/ui/components/ToolExecutionStatus.js +1 -0
  76. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  77. package/dist/utils/chat-formatter.d.ts +12 -0
  78. package/dist/utils/chat-formatter.d.ts.map +1 -0
  79. package/dist/utils/chat-formatter.js +326 -0
  80. package/dist/utils/chat-formatter.js.map +1 -0
  81. package/dist/utils/input-classifier.d.ts.map +1 -1
  82. package/dist/utils/input-classifier.js +139 -20
  83. package/dist/utils/input-classifier.js.map +1 -1
  84. package/dist/utils/text-clipboard.d.ts +12 -0
  85. package/dist/utils/text-clipboard.d.ts.map +1 -0
  86. package/dist/utils/text-clipboard.js +63 -0
  87. package/dist/utils/text-clipboard.js.map +1 -0
  88. package/package.json +1 -1
@@ -17,6 +17,8 @@ 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';
@@ -89,6 +91,7 @@ export class CentaurusCLI {
89
91
  onShowBackgroundTaskPickerCallback;
90
92
  onShowBackgroundTaskCancelPickerCallback;
91
93
  onBackgroundTaskViewCallback;
94
+ onTokenCountUpdate; // Report actual AI context token count to UI
92
95
  constructor() {
93
96
  this.configManager = new ConfigManager();
94
97
  this.toolRegistry = new ToolRegistry();
@@ -174,6 +177,9 @@ export class CentaurusCLI {
174
177
  setOnConnectionStatusUpdate(callback) {
175
178
  this.onConnectionStatusUpdate = callback;
176
179
  }
180
+ setOnTokenCountUpdate(callback) {
181
+ this.onTokenCountUpdate = callback;
182
+ }
177
183
  async initializeMCP() {
178
184
  try {
179
185
  const mcpConfigManager = new MCPConfigManager();
@@ -248,9 +254,9 @@ export class CentaurusCLI {
248
254
  // This prevents caching issues when we update model configs
249
255
  this.configManager.set('model', selectedModel.id);
250
256
  this.configManager.set('modelName', selectedModel.name);
251
- // Notify UI of model name change
257
+ // Notify UI of model name change and contextWindow
252
258
  if (this.onModelChange) {
253
- this.onModelChange(selectedModel.name);
259
+ this.onModelChange(selectedModel.name, selectedModel.contextWindow);
254
260
  }
255
261
  const responseMessage = `Model changed to: ${selectedModel.name}`;
256
262
  // Send response back to UI
@@ -343,6 +349,8 @@ export class CentaurusCLI {
343
349
  this.toolRegistry.register(fetchUrlTool);
344
350
  this.toolRegistry.register(taskCompleteTool);
345
351
  this.toolRegistry.register(readBinaryFileTool);
352
+ this.toolRegistry.register(createImageTool);
353
+ this.toolRegistry.register(backgroundCommandTool);
346
354
  // Load configuration
347
355
  const config = this.configManager.load();
348
356
  // Enable backend sync if authenticated
@@ -404,47 +412,24 @@ Press Enter to continue...
404
412
  }
405
413
  /**
406
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
407
417
  */
408
418
  async ensureConversationStarted() {
409
- if (this.conversationStarted || !apiClient.isAuthenticated()) {
410
- return;
411
- }
412
- // Check if conversationManager already has a conversation (e.g., created by App.tsx for image upload)
413
- // This prevents duplicate conversation creation
414
- if (conversationManager.getCurrentConversationId()) {
415
- this.conversationStarted = true;
416
- return;
417
- }
418
- try {
419
- const config = this.configManager.load();
420
- const title = `Conversation ${new Date().toLocaleString()}`;
421
- await conversationManager.startNewConversation(title, config.model || 'gemini-2.5-flash', 'google', // Always use Google provider now
422
- this.cwd);
423
- this.conversationStarted = true;
424
- }
425
- catch (error) {
426
- // Silently continue without backend persistence
427
- }
419
+ // No-op: Backend conversation creation has been disabled
420
+ // Conversations are now managed locally via local-chat-storage.ts
421
+ return;
428
422
  }
429
423
  /**
430
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
431
427
  */
432
428
  async saveMessageToBackend(role, content) {
433
- if (!apiClient.isAuthenticated()) {
434
- return;
435
- }
436
- try {
437
- await this.ensureConversationStarted();
438
- await conversationManager.addMessage({
439
- role,
440
- content,
441
- contentType: 'text',
442
- metadata: {},
443
- });
444
- }
445
- catch (error) {
446
- // Silently continue without backend persistence
447
- }
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;
448
433
  }
449
434
  getModel() {
450
435
  const config = this.configManager.load();
@@ -500,6 +485,27 @@ Press Enter to continue...
500
485
  this.conversationHistory.splice(lastAssistantWithToolCallsIndex);
501
486
  }
502
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
+ }
503
509
  async handleMessage(message) {
504
510
  // Handle command mode - execute commands directly
505
511
  if (this.commandMode) {
@@ -550,13 +556,17 @@ CRITICAL INSTRUCTIONS:
550
556
  DO NOT use write_to_file, edit_file, or execute_command until the plan is approved.`;
551
557
  userMessageContent = planModePrefix;
552
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();
553
563
  // Add user message to history
554
564
  this.conversationHistory.push({
555
565
  role: 'user',
556
566
  content: userMessageContent,
557
567
  });
558
- // Save user message to backend (save original message for display, not the prefixed version)
559
- 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/
560
570
  // Start logging session and log user message
561
571
  conversationLogger.startSession();
562
572
  conversationLogger.logUserMessage(message);
@@ -606,7 +616,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
606
616
  let narrationAttempts = 0; // Track how many times AI narrated without executing
607
617
  let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
608
618
  let thoughtStartTime = null; // Track when thinking started
609
- 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
610
621
  // ANTI-LOOP: Track duplicate tool calls to detect infinite loops
611
622
  const MAX_DUPLICATE_CALLS = 2; // Max times same operation allowed on same target
612
623
  const fileWriteTracker = new Map(); // Track writes per file
@@ -631,7 +642,28 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
631
642
  // DEBUG: Log message history state before AI call
632
643
  const messageStats = {
633
644
  totalMessages: messages.length,
634
- 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),
635
667
  byRole: {
636
668
  system: messages.filter(m => m.role === 'system').length,
637
669
  user: messages.filter(m => m.role === 'user').length,
@@ -647,6 +679,13 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
647
679
  quickLog(`[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
648
680
  }
649
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
+ }
650
689
  // Stream AI response from backend
651
690
  // Backend will inject system prompt automatically with environment context
652
691
  for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
@@ -687,6 +726,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
687
726
  if (this.onThoughtCompleteCallback) {
688
727
  this.onThoughtCompleteCallback(thinkingDuration);
689
728
  }
729
+ // Capture thinking for this turn before clearing
730
+ currentTurnThinking = thoughtContent;
690
731
  thoughtStartTime = null;
691
732
  thoughtContent = '';
692
733
  }
@@ -719,6 +760,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
719
760
  if (this.onThoughtCompleteCallback) {
720
761
  this.onThoughtCompleteCallback(thinkingDuration);
721
762
  }
763
+ // Capture thinking for this turn before clearing
764
+ currentTurnThinking = thoughtContent;
722
765
  thoughtStartTime = null;
723
766
  thoughtContent = '';
724
767
  }
@@ -747,6 +790,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
747
790
  if (this.onThoughtCompleteCallback) {
748
791
  this.onThoughtCompleteCallback(thinkingDuration);
749
792
  }
793
+ // Capture thinking for this turn before clearing
794
+ currentTurnThinking = thoughtContent;
750
795
  thoughtStartTime = null;
751
796
  thoughtContent = '';
752
797
  }
@@ -845,11 +890,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
845
890
  this.onPlanModeChange(false);
846
891
  }
847
892
  // Add assistant message with plan tool call to history
848
- this.conversationHistory.push({
893
+ const planAssistantMsg = {
849
894
  role: 'assistant',
850
895
  content: '',
851
896
  tool_calls: [toolCall],
852
- });
897
+ };
898
+ if (currentTurnThinking) {
899
+ planAssistantMsg.thinking = currentTurnThinking;
900
+ }
901
+ this.conversationHistory.push(planAssistantMsg);
853
902
  // Add plan approval response
854
903
  this.conversationHistory.push({
855
904
  role: 'tool',
@@ -885,11 +934,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
885
934
  else {
886
935
  // No approval callback - add the tool result to history and wait for user response
887
936
  // This ensures the AI doesn't get stuck in a silent loop
888
- this.conversationHistory.push({
937
+ const planAssistantMsg = {
889
938
  role: 'assistant',
890
939
  content: '',
891
940
  tool_calls: [toolCall],
892
- });
941
+ };
942
+ if (currentTurnThinking) {
943
+ planAssistantMsg.thinking = currentTurnThinking;
944
+ }
945
+ this.conversationHistory.push(planAssistantMsg);
893
946
  this.conversationHistory.push({
894
947
  role: 'tool',
895
948
  tool_call_id: toolCall.id,
@@ -909,11 +962,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
909
962
  // Log error and add error result to history so AI knows
910
963
  logWarning(`Failed to parse plan: ${parseError?.message || parseError}`);
911
964
  // CRITICAL: Add tool result even on parse error to prevent silent loop
912
- this.conversationHistory.push({
965
+ const errorAssistantMsg = {
913
966
  role: 'assistant',
914
967
  content: '',
915
968
  tool_calls: [toolCall],
916
- });
969
+ };
970
+ if (currentTurnThinking) {
971
+ errorAssistantMsg.thinking = currentTurnThinking;
972
+ }
973
+ this.conversationHistory.push(errorAssistantMsg);
917
974
  this.conversationHistory.push({
918
975
  role: 'tool',
919
976
  tool_call_id: toolCall.id,
@@ -926,11 +983,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
926
983
  }
927
984
  else {
928
985
  // Tool returned non-PLAN_CREATED result - add it to history
929
- this.conversationHistory.push({
986
+ const resultAssistantMsg = {
930
987
  role: 'assistant',
931
988
  content: '',
932
989
  tool_calls: [toolCall],
933
- });
990
+ };
991
+ if (currentTurnThinking) {
992
+ resultAssistantMsg.thinking = currentTurnThinking;
993
+ }
994
+ this.conversationHistory.push(resultAssistantMsg);
934
995
  this.conversationHistory.push({
935
996
  role: 'tool',
936
997
  tool_call_id: toolCall.id,
@@ -959,7 +1020,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
959
1020
  const mainTaskNum = parseInt(taskNumParts[0], 10) - 1;
960
1021
  const task = currentPlanData.steps[mainTaskNum];
961
1022
  if (task) {
962
- 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
+ );
963
1025
  }
964
1026
  }
965
1027
  // Notify UI about completed task/subtask
@@ -979,11 +1041,15 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
979
1041
  result: `Task completed. Moving to Task ${nextPhase.taskNumber}: ${nextPhase.task.description}`,
980
1042
  });
981
1043
  // Add the tool call and result to history
982
- this.conversationHistory.push({
1044
+ const nextPhaseAssistantMsg = {
983
1045
  role: 'assistant',
984
1046
  content: '',
985
1047
  tool_calls: [toolCall],
986
- });
1048
+ };
1049
+ if (currentTurnThinking) {
1050
+ nextPhaseAssistantMsg.thinking = currentTurnThinking;
1051
+ }
1052
+ this.conversationHistory.push(nextPhaseAssistantMsg);
987
1053
  this.conversationHistory.push({
988
1054
  role: 'tool',
989
1055
  tool_call_id: toolCall.id,
@@ -1216,12 +1282,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1216
1282
  }
1217
1283
  // If user cancelled an operation, stop the agentic loop immediately
1218
1284
  if (userCancelledOperation) {
1219
- // Add assistant message to history
1220
- this.conversationHistory.push({
1285
+ // Add assistant message to history with thinking if available
1286
+ const cancelledAssistantMsg = {
1221
1287
  role: 'assistant',
1222
1288
  content: assistantMessage || '',
1223
1289
  tool_calls: toolCalls, // Store tool calls for MaaS models
1224
- });
1290
+ };
1291
+ if (currentTurnThinking) {
1292
+ cancelledAssistantMsg.thinking = currentTurnThinking;
1293
+ }
1294
+ this.conversationHistory.push(cancelledAssistantMsg);
1225
1295
  // Add tool results to history
1226
1296
  for (const toolResult of toolResults) {
1227
1297
  this.conversationHistory.push({
@@ -1259,11 +1329,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1259
1329
  const unhandledToolResults = toolResults.filter(tr => !handledToolCallIds.has(tr.tool_call_id));
1260
1330
  // Only add assistant message if there are unhandled tool calls
1261
1331
  if (unhandledToolCalls.length > 0) {
1262
- this.conversationHistory.push({
1332
+ const assistantHistoryMsg = {
1263
1333
  role: 'assistant',
1264
1334
  content: assistantMessage || '',
1265
1335
  tool_calls: unhandledToolCalls, // Only include unhandled tool calls
1266
- });
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);
1267
1342
  }
1268
1343
  // Add tool results to conversation history as tool messages
1269
1344
  // Format: { tool_call_id, name, result: <object or string> }
@@ -1278,8 +1353,10 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1278
1353
  });
1279
1354
  }
1280
1355
  // Rebuild messages array with updated history
1281
- // 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)
1282
1358
  messages = [...this.conversationHistory];
1359
+ // No need to reset currentTurnThinking - keep accumulating for the task
1283
1360
  // Re-inject subshell context
1284
1361
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
1285
1362
  continue; // Loop back to AI service
@@ -1469,8 +1546,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1469
1546
  role: 'assistant',
1470
1547
  content: finalMessage,
1471
1548
  });
1472
- // Save assistant message to backend
1473
- await this.saveMessageToBackend('assistant', finalMessage);
1549
+ // Messages are stored locally only via saveCurrentChat() below
1474
1550
  } // End of while loop
1475
1551
  // Auto-save conversation to local storage
1476
1552
  this.saveCurrentChat();
@@ -1511,11 +1587,13 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1511
1587
  `/init - Analyze project and create/load centaurus.md context file\n` +
1512
1588
  `/chat - Manage chat sessions (resume previous chats)\n` +
1513
1589
  `/clear - Clear conversation and start a new chat\n` +
1590
+ `/sync - Sync data to/from cloud (upload/restore)\n` +
1514
1591
  `/config - View current configuration\n` +
1515
1592
  `/model - Select from available Google models\n` +
1516
1593
  `/plan - Toggle plan mode for complex implementations\n` +
1517
1594
  `/mcp - Manage configured MCP servers and tools\n` +
1518
1595
  `/docs - Open Centaurus documentation in browser\n` +
1596
+ `/copy-chat-context - Copy chat as readable text to clipboard\n` +
1519
1597
  `/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
1520
1598
  `/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
1521
1599
  `/sign-in - Sign in with Google (if not already signed in)\n` +
@@ -1938,7 +2016,7 @@ Start by listing the directory structure to understand what you're working with.
1938
2016
  // Show list of saved chats for resuming (interactive picker)
1939
2017
  const chats = localChatStorage.listChats();
1940
2018
  if (chats.length === 0) {
1941
- 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!';
1942
2020
  }
1943
2021
  else {
1944
2022
  // Format chat list for picker display
@@ -1948,13 +2026,13 @@ Start by listing the directory structure to understand what you're working with.
1948
2026
  }
1949
2027
  else {
1950
2028
  // Fallback: show as text if no picker callback
1951
- responseMessage = '📚 Saved Chats:\\n\\n' +
2029
+ responseMessage = 'Saved Chats:\n\n' +
1952
2030
  chats.slice(0, 10).map((chat, i) => {
1953
2031
  const date = new Date(chat.updatedAt).toLocaleDateString();
1954
2032
  const time = new Date(chat.updatedAt).toLocaleTimeString();
1955
- return `${i + 1}. ${chat.title}\\n 📅 ${date} ${time} | 💬 ${chat.messageCount} messages`;
1956
- }).join('\\n\\n') +
1957
- (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` : '');
1958
2036
  }
1959
2037
  }
1960
2038
  }
@@ -1962,7 +2040,7 @@ Start by listing the directory structure to understand what you're working with.
1962
2040
  // Show read-only list of saved chats (no selection)
1963
2041
  const chats = localChatStorage.listChats();
1964
2042
  if (chats.length === 0) {
1965
- 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!';
1966
2044
  }
1967
2045
  else {
1968
2046
  // Format chat list for read-only display
@@ -1987,7 +2065,7 @@ Start by listing the directory structure to understand what you're working with.
1987
2065
  // Show list of saved chats for deletion
1988
2066
  const chats = localChatStorage.listChats();
1989
2067
  if (chats.length === 0) {
1990
- responseMessage = '📭 No saved chats to delete.';
2068
+ responseMessage = 'No saved chats to delete.';
1991
2069
  }
1992
2070
  else {
1993
2071
  // Format chat list for delete picker display
@@ -2007,13 +2085,13 @@ Start by listing the directory structure to understand what you're working with.
2007
2085
  if (this.onRestoreMessagesCallback) {
2008
2086
  this.onRestoreMessagesCallback([]);
2009
2087
  }
2010
- 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.';
2011
2089
  }
2012
2090
  else if (chatSubCommand === 'rename') {
2013
2091
  // Show list of saved chats for renaming
2014
2092
  const chats = localChatStorage.listChats();
2015
2093
  if (chats.length === 0) {
2016
- responseMessage = '📭 No saved chats to rename.';
2094
+ responseMessage = 'No saved chats to rename.';
2017
2095
  }
2018
2096
  else {
2019
2097
  // Format chat list for rename picker display
@@ -2030,6 +2108,30 @@ Start by listing the directory structure to understand what you're working with.
2030
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`;
2031
2109
  }
2032
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;
2033
2135
  case 'exit':
2034
2136
  process.exit(0);
2035
2137
  break;
@@ -2147,6 +2249,138 @@ Start by listing the directory structure to understand what you're working with.
2147
2249
  ' /background-task cancel - Cancel a running background task';
2148
2250
  }
2149
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;
2150
2384
  default:
2151
2385
  responseMessage = `Unknown command: /${cmd}\nType /help for available commands.`;
2152
2386
  }