centaurus-cli 2.8.9 → 2.9.1

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 (109) hide show
  1. package/dist/cli-adapter.d.ts +29 -2
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +526 -84
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/build-config.d.ts +1 -1
  6. package/dist/config/build-config.js +1 -1
  7. package/dist/config/models.d.ts +8 -0
  8. package/dist/config/models.d.ts.map +1 -1
  9. package/dist/config/models.js +29 -0
  10. package/dist/config/models.js.map +1 -1
  11. package/dist/config/slash-commands.d.ts +1 -0
  12. package/dist/config/slash-commands.d.ts.map +1 -1
  13. package/dist/config/slash-commands.js +14 -1
  14. package/dist/config/slash-commands.js.map +1 -1
  15. package/dist/hooks/useConnectivity.d.ts +2 -0
  16. package/dist/hooks/useConnectivity.d.ts.map +1 -0
  17. package/dist/hooks/useConnectivity.js +12 -0
  18. package/dist/hooks/useConnectivity.js.map +1 -0
  19. package/dist/index.js +9 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  22. package/dist/mcp/mcp-command-handler.js +0 -3
  23. package/dist/mcp/mcp-command-handler.js.map +1 -1
  24. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  25. package/dist/mcp/mcp-tool-wrapper.js +8 -0
  26. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  27. package/dist/services/ai-service-client.d.ts +7 -1
  28. package/dist/services/ai-service-client.d.ts.map +1 -1
  29. package/dist/services/ai-service-client.js +6 -6
  30. package/dist/services/ai-service-client.js.map +1 -1
  31. package/dist/services/api-client.d.ts +35 -38
  32. package/dist/services/api-client.d.ts.map +1 -1
  33. package/dist/services/api-client.js +38 -30
  34. package/dist/services/api-client.js.map +1 -1
  35. package/dist/services/connectivity-manager.d.ts +18 -0
  36. package/dist/services/connectivity-manager.d.ts.map +1 -0
  37. package/dist/services/connectivity-manager.js +72 -0
  38. package/dist/services/connectivity-manager.js.map +1 -0
  39. package/dist/services/local-chat-storage.d.ts +5 -0
  40. package/dist/services/local-chat-storage.d.ts.map +1 -1
  41. package/dist/services/local-chat-storage.js +33 -0
  42. package/dist/services/local-chat-storage.js.map +1 -1
  43. package/dist/services/session-quota-manager.d.ts +101 -0
  44. package/dist/services/session-quota-manager.d.ts.map +1 -0
  45. package/dist/services/session-quota-manager.js +242 -0
  46. package/dist/services/session-quota-manager.js.map +1 -0
  47. package/dist/tools/background-command.d.ts +11 -0
  48. package/dist/tools/background-command.d.ts.map +1 -0
  49. package/dist/tools/background-command.js +162 -0
  50. package/dist/tools/background-command.js.map +1 -0
  51. package/dist/tools/command.d.ts.map +1 -1
  52. package/dist/tools/command.js +20 -6
  53. package/dist/tools/command.js.map +1 -1
  54. package/dist/tools/create-image.d.ts +10 -0
  55. package/dist/tools/create-image.d.ts.map +1 -0
  56. package/dist/tools/create-image.js +189 -0
  57. package/dist/tools/create-image.js.map +1 -0
  58. package/dist/tools/get-diff.d.ts.map +1 -1
  59. package/dist/tools/get-diff.js +4 -1
  60. package/dist/tools/get-diff.js.map +1 -1
  61. package/dist/tools/task-complete.d.ts.map +1 -1
  62. package/dist/tools/task-complete.js +8 -14
  63. package/dist/tools/task-complete.js.map +1 -1
  64. package/dist/ui/components/App.d.ts +5 -2
  65. package/dist/ui/components/App.d.ts.map +1 -1
  66. package/dist/ui/components/App.js +165 -45
  67. package/dist/ui/components/App.js.map +1 -1
  68. package/dist/ui/components/ContextWindowIndicator.d.ts.map +1 -1
  69. package/dist/ui/components/ContextWindowIndicator.js +43 -22
  70. package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
  71. package/dist/ui/components/InputBox.d.ts +2 -0
  72. package/dist/ui/components/InputBox.d.ts.map +1 -1
  73. package/dist/ui/components/InputBox.js +217 -200
  74. package/dist/ui/components/InputBox.js.map +1 -1
  75. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  76. package/dist/ui/components/MessageDisplay.js +8 -15
  77. package/dist/ui/components/MessageDisplay.js.map +1 -1
  78. package/dist/ui/components/SlashCommandAutocomplete.d.ts +2 -0
  79. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -1
  80. package/dist/ui/components/SlashCommandAutocomplete.js +19 -10
  81. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  82. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  83. package/dist/ui/components/StatusBar.js +4 -0
  84. package/dist/ui/components/StatusBar.js.map +1 -1
  85. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  86. package/dist/ui/components/ToolExecutionMessage.js +198 -39
  87. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  88. package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
  89. package/dist/ui/components/ToolExecutionStatus.js +1 -0
  90. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  91. package/dist/utils/chat-formatter.d.ts +12 -0
  92. package/dist/utils/chat-formatter.d.ts.map +1 -0
  93. package/dist/utils/chat-formatter.js +326 -0
  94. package/dist/utils/chat-formatter.js.map +1 -0
  95. package/dist/utils/editor-utils.d.ts +3 -3
  96. package/dist/utils/editor-utils.d.ts.map +1 -1
  97. package/dist/utils/editor-utils.js +15 -12
  98. package/dist/utils/editor-utils.js.map +1 -1
  99. package/dist/utils/input-classifier.d.ts.map +1 -1
  100. package/dist/utils/input-classifier.js +140 -20
  101. package/dist/utils/input-classifier.js.map +1 -1
  102. package/dist/utils/terminal-output.d.ts.map +1 -1
  103. package/dist/utils/terminal-output.js +198 -171
  104. package/dist/utils/terminal-output.js.map +1 -1
  105. package/dist/utils/text-clipboard.d.ts +12 -0
  106. package/dist/utils/text-clipboard.d.ts.map +1 -0
  107. package/dist/utils/text-clipboard.js +63 -0
  108. package/dist/utils/text-clipboard.js.map +1 -0
  109. 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';
@@ -36,6 +38,7 @@ 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';
38
40
  import { BackgroundTaskManager } from './services/background-task-manager.js';
41
+ import { sessionQuotaManager } from './services/session-quota-manager.js';
39
42
  export class CentaurusCLI {
40
43
  configManager;
41
44
  toolRegistry;
@@ -49,6 +52,7 @@ export class CentaurusCLI {
49
52
  onResponseCallback;
50
53
  onDirectMessageCallback; // For slash commands - adds directly to history
51
54
  onResponseStreamCallback;
55
+ onClearStreamedResponse; // Clear streamed text when task_complete has summary
52
56
  onThoughtStreamCallback;
53
57
  onThoughtCompleteCallback;
54
58
  onCommandModeChange;
@@ -89,6 +93,8 @@ export class CentaurusCLI {
89
93
  onShowBackgroundTaskPickerCallback;
90
94
  onShowBackgroundTaskCancelPickerCallback;
91
95
  onBackgroundTaskViewCallback;
96
+ onTokenCountUpdate; // Report actual AI context token count to UI
97
+ onSessionQuotaUpdate;
92
98
  constructor() {
93
99
  this.configManager = new ConfigManager();
94
100
  this.toolRegistry = new ToolRegistry();
@@ -119,6 +125,9 @@ export class CentaurusCLI {
119
125
  setOnResponseStreamCallback(callback) {
120
126
  this.onResponseStreamCallback = callback;
121
127
  }
128
+ setOnClearStreamedResponse(callback) {
129
+ this.onClearStreamedResponse = callback;
130
+ }
122
131
  setOnThoughtStreamCallback(callback) {
123
132
  this.onThoughtStreamCallback = callback;
124
133
  }
@@ -174,6 +183,23 @@ export class CentaurusCLI {
174
183
  setOnConnectionStatusUpdate(callback) {
175
184
  this.onConnectionStatusUpdate = callback;
176
185
  }
186
+ setOnTokenCountUpdate(callback) {
187
+ this.onTokenCountUpdate = callback;
188
+ }
189
+ setOnSessionQuotaUpdate(callback) {
190
+ this.onSessionQuotaUpdate = callback;
191
+ }
192
+ /**
193
+ * Notify UI about session quota status
194
+ */
195
+ notifySessionQuotaStatus() {
196
+ if (this.onSessionQuotaUpdate) {
197
+ const remaining = sessionQuotaManager.getRemainingMessages();
198
+ const canSend = sessionQuotaManager.canSendMessage();
199
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
200
+ this.onSessionQuotaUpdate(remaining, canSend, timeRemaining);
201
+ }
202
+ }
177
203
  async initializeMCP() {
178
204
  try {
179
205
  const mcpConfigManager = new MCPConfigManager();
@@ -248,9 +274,9 @@ export class CentaurusCLI {
248
274
  // This prevents caching issues when we update model configs
249
275
  this.configManager.set('model', selectedModel.id);
250
276
  this.configManager.set('modelName', selectedModel.name);
251
- // Notify UI of model name change
277
+ // Notify UI of model name change and contextWindow
252
278
  if (this.onModelChange) {
253
- this.onModelChange(selectedModel.name);
279
+ this.onModelChange(selectedModel.name, selectedModel.contextWindow);
254
280
  }
255
281
  const responseMessage = `Model changed to: ${selectedModel.name}`;
256
282
  // Send response back to UI
@@ -343,6 +369,8 @@ export class CentaurusCLI {
343
369
  this.toolRegistry.register(fetchUrlTool);
344
370
  this.toolRegistry.register(taskCompleteTool);
345
371
  this.toolRegistry.register(readBinaryFileTool);
372
+ this.toolRegistry.register(createImageTool);
373
+ this.toolRegistry.register(backgroundCommandTool);
346
374
  // Load configuration
347
375
  const config = this.configManager.load();
348
376
  // Enable backend sync if authenticated
@@ -359,6 +387,11 @@ export class CentaurusCLI {
359
387
  const dockerHandler = new DockerHandler();
360
388
  this.contextManager.registerHandler('docker', dockerHandler);
361
389
  this.commandDetector.registerHandler(dockerHandler);
390
+ // Fetch rate limits configuration from backend (async, non-blocking)
391
+ // Uses cached values if backend is unreachable
392
+ sessionQuotaManager.fetchConfigFromBackend().catch(() => {
393
+ // Silently fall back to cached/default config
394
+ });
362
395
  // Note: No need to initialize AI provider - using backend proxy via aiServiceClient
363
396
  }
364
397
  /**
@@ -404,47 +437,24 @@ Press Enter to continue...
404
437
  }
405
438
  /**
406
439
  * Start a new conversation in the backend
440
+ * @deprecated Backend conversation creation is no longer needed since messages are stored locally
441
+ * This function is kept as a no-op for compatibility
407
442
  */
408
443
  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
- }
444
+ // No-op: Backend conversation creation has been disabled
445
+ // Conversations are now managed locally via local-chat-storage.ts
446
+ return;
428
447
  }
429
448
  /**
430
449
  * Save a message to the backend
450
+ * @deprecated Messages are now stored locally only via saveCurrentChat()
451
+ * This function is kept as a no-op for compatibility
431
452
  */
432
453
  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
- }
454
+ // No-op: Messages are stored locally only
455
+ // Backend message storage has been disabled to avoid "failed to fetch" errors
456
+ // All conversation history is persisted via local-chat-storage.ts
457
+ return;
448
458
  }
449
459
  getModel() {
450
460
  const config = this.configManager.load();
@@ -500,6 +510,27 @@ Press Enter to continue...
500
510
  this.conversationHistory.splice(lastAssistantWithToolCallsIndex);
501
511
  }
502
512
  }
513
+ /**
514
+ * Strip thinking blocks from ALL assistant messages in the conversation history.
515
+ *
516
+ * This is called at the START of a new user request to clear thinking from
517
+ * previous tasks. During the current agent loop, thinking is preserved for
518
+ * all turns to help the AI maintain reasoning context.
519
+ *
520
+ * Pattern:
521
+ * - New user request starts → strip ALL thinking from history
522
+ * - During agent loop (multi-turn tool execution) → keep ALL thinking
523
+ * - This allows AI to remember reasoning for current task but not old tasks
524
+ */
525
+ stripThinkingFromHistory() {
526
+ for (let i = 0; i < this.conversationHistory.length; i++) {
527
+ const msg = this.conversationHistory[i];
528
+ if (msg.role === 'assistant' && msg.thinking) {
529
+ // Remove thinking from the message in place
530
+ delete msg.thinking;
531
+ }
532
+ }
533
+ }
503
534
  async handleMessage(message) {
504
535
  // Handle command mode - execute commands directly
505
536
  if (this.commandMode) {
@@ -520,6 +551,17 @@ Press Enter to continue...
520
551
  if (!apiClient.isAuthenticated()) {
521
552
  throw new Error('Authentication required. Please sign in to use AI features.');
522
553
  }
554
+ // Check session quota before making any AI request
555
+ if (!sessionQuotaManager.canSendMessage()) {
556
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
557
+ const message = `\n⚠️ Session quota reached. You have used all ${sessionQuotaManager.getCurrentConfig().maxMessagesPerSession} messages for this session.\n\nYour quota will reset in ${timeRemaining}.\n\nYou can still use:\n • Slash commands (e.g., /help, /session-limits, /exit)\n • Terminal commands (in Command mode)\n\nUse /session-limits to check your quota status.`;
558
+ if (this.onDirectMessageCallback) {
559
+ this.onDirectMessageCallback(message);
560
+ }
561
+ // Notify UI about quota status
562
+ this.notifySessionQuotaStatus();
563
+ return;
564
+ }
523
565
  // Cancel any active request when a new message comes in
524
566
  // This enables "interrupt and replace" - new message takes priority
525
567
  if (this.currentAbortController) {
@@ -550,13 +592,17 @@ CRITICAL INSTRUCTIONS:
550
592
  DO NOT use write_to_file, edit_file, or execute_command until the plan is approved.`;
551
593
  userMessageContent = planModePrefix;
552
594
  }
595
+ // NEW USER REQUEST: Strip thinking from previous tasks
596
+ // This clears thinking from the previous agent loop but thinking will be
597
+ // preserved for all turns within the current agent loop
598
+ this.stripThinkingFromHistory();
553
599
  // Add user message to history
554
600
  this.conversationHistory.push({
555
601
  role: 'user',
556
602
  content: userMessageContent,
557
603
  });
558
- // Save user message to backend (save original message for display, not the prefixed version)
559
- await this.saveMessageToBackend('user', message);
604
+ // Messages are stored locally only - no backend persistence needed
605
+ // Local storage is handled by saveCurrentChat() which saves to ~/.centaurus/chats/
560
606
  // Start logging session and log user message
561
607
  conversationLogger.startSession();
562
608
  conversationLogger.logUserMessage(message);
@@ -606,7 +652,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
606
652
  let narrationAttempts = 0; // Track how many times AI narrated without executing
607
653
  let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
608
654
  let thoughtStartTime = null; // Track when thinking started
609
- let thoughtContent = ''; // Accumulate thought content
655
+ let thoughtContent = ''; // Accumulate thought content during streaming
656
+ let currentTurnThinking = ''; // Persist thinking for the current turn to attach to assistant message
657
+ let currentTurnThinkingSignature = ''; // Persist thinking signature for Claude extended thinking
610
658
  // ANTI-LOOP: Track duplicate tool calls to detect infinite loops
611
659
  const MAX_DUPLICATE_CALLS = 2; // Max times same operation allowed on same target
612
660
  const fileWriteTracker = new Map(); // Track writes per file
@@ -623,15 +671,56 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
623
671
  // Multi-turn tool execution loop
624
672
  while (turnCount < MAX_TURNS) {
625
673
  turnCount++;
674
+ // Track session quota - each AI call in the agent loop counts as 1 message
675
+ sessionQuotaManager.incrementMessageCount();
676
+ this.notifySessionQuotaStatus();
677
+ // Check if session quota is now exhausted after incrementing
678
+ if (!sessionQuotaManager.canSendMessage() && turnCount > 1) {
679
+ // Quota exhausted mid-loop, stop and inform user
680
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
681
+ const quotaMessage = `\n\n⚠️ **Session quota reached** during agent execution.\n\nYou have used all ${sessionQuotaManager.getCurrentConfig().maxMessagesPerSession} messages for this session.\nQuota will reset in ${timeRemaining}.\n\nYour current task may be incomplete. You can resume when your quota resets.\n\nUse /session-limits to check your quota status.`;
682
+ if (this.onResponseCallback) {
683
+ this.onResponseCallback(quotaMessage);
684
+ }
685
+ logWarning('Agent loop stopped due to session quota exhaustion');
686
+ return;
687
+ }
626
688
  // Refresh environment context to capture any CWD changes from previous turns
627
689
  // This is sent to backend which will inject it into the system prompt
628
690
  environmentContext = this.getEnvironmentContext();
629
691
  let assistantMessage = '';
630
692
  let toolCalls = [];
693
+ // REAL-TIME TOOL EXECUTION: Track execution state and results during streaming
694
+ const inStreamToolResults = []; // Results from tools executed during streaming
695
+ const inStreamHandledIds = new Set(); // IDs of tools already executed in-stream
696
+ let toolsExecutedInStream = false; // Flag to indicate tools were executed during stream
697
+ let pendingTextBuffer = ''; // Buffer for text while tool is executing
698
+ let isToolExecuting = false; // Flag to pause text streaming during tool execution
631
699
  // DEBUG: Log message history state before AI call
632
700
  const messageStats = {
633
701
  totalMessages: messages.length,
634
- totalCharacters: messages.reduce((sum, m) => sum + (typeof m.content === 'string' ? m.content.length : 0), 0),
702
+ totalCharacters: messages.reduce((sum, m) => {
703
+ let len = typeof m.content === 'string' ? m.content.length : 0;
704
+ // Include thinking content (internal reasoning)
705
+ if (m.thinking) {
706
+ len += m.thinking.length;
707
+ }
708
+ // Include tool calls (name + arguments)
709
+ if (m.tool_calls) {
710
+ m.tool_calls.forEach(tc => {
711
+ len += tc.name.length;
712
+ // Arguments are JSON stringified in the payload
713
+ if (tc.arguments) {
714
+ len += JSON.stringify(tc.arguments).length;
715
+ }
716
+ });
717
+ }
718
+ // Include tool_call_id for tool messages
719
+ if (m.role === 'tool' && m.tool_call_id) {
720
+ len += m.tool_call_id.length;
721
+ }
722
+ return sum + len;
723
+ }, 0),
635
724
  byRole: {
636
725
  system: messages.filter(m => m.role === 'system').length,
637
726
  user: messages.filter(m => m.role === 'user').length,
@@ -647,6 +736,13 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
647
736
  quickLog(`[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
648
737
  }
649
738
  catch (e) { }
739
+ // Report actual token count to UI (estimate: 1 token ≈ 4 chars)
740
+ // Include system prompt estimate (~14000 chars based on backend logs)
741
+ const SYSTEM_PROMPT_ESTIMATE = 14000; // Backend injects ~14K char system prompt
742
+ const estimatedTokens = Math.ceil((messageStats.totalCharacters + SYSTEM_PROMPT_ESTIMATE) / 4);
743
+ if (this.onTokenCountUpdate) {
744
+ this.onTokenCountUpdate(estimatedTokens);
745
+ }
650
746
  // Stream AI response from backend
651
747
  // Backend will inject system prompt automatically with environment context
652
748
  for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
@@ -678,6 +774,12 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
678
774
  }
679
775
  continue;
680
776
  }
777
+ // Handle thinking_signature chunks (Claude extended thinking)
778
+ if (chunk.type === 'thinking_signature') {
779
+ // Store the signature for this turn - it must be passed back with thinking content
780
+ currentTurnThinkingSignature = chunk.signature;
781
+ continue;
782
+ }
681
783
  // Handle text chunks
682
784
  if (chunk.type === 'text') {
683
785
  // If we were thinking and now got text, finalize the thought
@@ -687,6 +789,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
687
789
  if (this.onThoughtCompleteCallback) {
688
790
  this.onThoughtCompleteCallback(thinkingDuration);
689
791
  }
792
+ // Capture thinking for this turn before clearing
793
+ currentTurnThinking = thoughtContent;
690
794
  thoughtStartTime = null;
691
795
  thoughtContent = '';
692
796
  }
@@ -697,9 +801,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
697
801
  if (filteredContent) {
698
802
  assistantMessage += filteredContent;
699
803
  conversationLogger.logAITextChunk(filteredContent);
700
- // Send chunk to UI in real-time
701
- if (this.onResponseStreamCallback) {
702
- this.onResponseStreamCallback(filteredContent);
804
+ // REAL-TIME TOOL EXECUTION: If a tool is executing, accumulate text
805
+ // This text will be flushed after the tool completes
806
+ if (isToolExecuting) {
807
+ pendingTextBuffer += filteredContent;
808
+ }
809
+ else {
810
+ // Normal streaming - send chunk to UI in real-time
811
+ if (this.onResponseStreamCallback) {
812
+ this.onResponseStreamCallback(filteredContent);
813
+ }
703
814
  }
704
815
  }
705
816
  }
@@ -708,7 +819,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
708
819
  const toolCall = chunk.toolCall;
709
820
  // Debug: Log every tool_call chunk received
710
821
  try {
711
- quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED: ${toolCall?.name || 'unknown'}, toolCalls.length before push: ${toolCalls.length}\n`);
822
+ quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED (REAL-TIME): ${toolCall?.name || 'unknown'}\n`);
712
823
  }
713
824
  catch (e) { }
714
825
  conversationLogger.logToolCall(toolCall?.name || 'unknown', toolCall?.id || 'unknown', toolCall?.arguments || {});
@@ -719,22 +830,94 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
719
830
  if (this.onThoughtCompleteCallback) {
720
831
  this.onThoughtCompleteCallback(thinkingDuration);
721
832
  }
833
+ // Capture thinking for this turn before clearing
834
+ currentTurnThinking = thoughtContent;
722
835
  thoughtStartTime = null;
723
836
  thoughtContent = '';
724
837
  }
725
838
  toolCalls.push(chunk.toolCall);
726
- // IMMEDIATELY notify UI that a tool call was received (pending status)
727
- // This shows tool calls in the UI as they stream in, not after the stream ends
728
- if (this.onToolExecutionUpdate && toolCall) {
729
- this.onToolExecutionUpdate({
730
- toolName: toolCall.name,
731
- status: 'pending', // 'pending' means received but not yet executing
732
- arguments: toolCall.arguments
839
+ // SPECIAL TOOLS: Skip in-stream execution for tools that need post-stream handling
840
+ // These tools have special logic (setting flags, clearing state, etc.) that must run post-stream
841
+ const SPECIAL_TOOLS = ['task_complete', 'create_plan', 'mark_task_complete'];
842
+ if (SPECIAL_TOOLS.includes(toolCall.name)) {
843
+ // Just notify UI with pending status, execute in post-stream loop
844
+ if (this.onToolExecutionUpdate) {
845
+ this.onToolExecutionUpdate({
846
+ toolName: toolCall.name,
847
+ status: 'pending',
848
+ arguments: toolCall.arguments
849
+ });
850
+ }
851
+ continue; // Skip to next chunk, handle this tool in post-stream loop
852
+ }
853
+ // Mark that we're executing a tool (text will accumulate)
854
+ isToolExecuting = true;
855
+ toolsExecutedInStream = true;
856
+ // REAL-TIME EXECUTION: Execute tool immediately during streaming
857
+ // This reduces latency by not waiting for the entire stream to finish
858
+ try {
859
+ // Extract and display reason_text if present (skip for task_complete)
860
+ const reasonText = toolCall.arguments.reason_text;
861
+ if (reasonText && this.onResponseStreamCallback) {
862
+ this.onResponseStreamCallback(reasonText + '\n\n');
863
+ }
864
+ // Show 'executing' status immediately
865
+ this.notifyToolStatus(toolCall.name, 'executing', toolCall.arguments);
866
+ // Log tool execution start
867
+ conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
868
+ // Execute the tool (it will request approval if needed via requireApproval callback)
869
+ const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
870
+ if (result.success) {
871
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
872
+ // Notify UI: tool succeeded
873
+ this.notifyToolStatus(toolCall.name, 'completed', toolCall.arguments, result.result);
874
+ // Parse and truncate result for AI
875
+ let parsedResult = result.result;
876
+ if (typeof result.result === 'string') {
877
+ try {
878
+ parsedResult = JSON.parse(result.result);
879
+ }
880
+ catch {
881
+ parsedResult = result.result;
882
+ }
883
+ }
884
+ inStreamToolResults.push({
885
+ tool_call_id: toolCall.id,
886
+ name: toolCall.name,
887
+ result: this.truncateResult(parsedResult),
888
+ });
889
+ }
890
+ else {
891
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, result.error);
892
+ // Notify UI: tool failed
893
+ this.notifyToolStatus(toolCall.name, 'error', toolCall.arguments, undefined, result.error);
894
+ inStreamToolResults.push({
895
+ tool_call_id: toolCall.id,
896
+ name: toolCall.name,
897
+ result: `Error: ${result.error}`,
898
+ });
899
+ }
900
+ inStreamHandledIds.add(toolCall.id);
901
+ }
902
+ catch (error) {
903
+ conversationLogger.logError(`Tool execution: ${toolCall.name}`, error);
904
+ this.notifyToolStatus(toolCall.name, 'error', toolCall.arguments, undefined, error.message);
905
+ inStreamToolResults.push({
906
+ tool_call_id: toolCall.id,
907
+ name: toolCall.name,
908
+ result: `Error: ${error.message}`,
733
909
  });
910
+ inStreamHandledIds.add(toolCall.id);
734
911
  }
735
- // Debug: Log after push
912
+ // Tool execution complete - flush pending text
913
+ isToolExecuting = false;
914
+ if (pendingTextBuffer && this.onResponseStreamCallback) {
915
+ this.onResponseStreamCallback(pendingTextBuffer);
916
+ pendingTextBuffer = '';
917
+ }
918
+ // Debug: Log after execution
736
919
  try {
737
- quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL PUSHED: ${toolCall?.name || 'unknown'}, toolCalls.length after push: ${toolCalls.length}\n`);
920
+ quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL EXECUTED IN-STREAM: ${toolCall?.name || 'unknown'}\n`);
738
921
  }
739
922
  catch (e) { }
740
923
  }
@@ -747,6 +930,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
747
930
  if (this.onThoughtCompleteCallback) {
748
931
  this.onThoughtCompleteCallback(thinkingDuration);
749
932
  }
933
+ // Capture thinking for this turn before clearing
934
+ currentTurnThinking = thoughtContent;
750
935
  thoughtStartTime = null;
751
936
  thoughtContent = '';
752
937
  }
@@ -781,18 +966,26 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
781
966
  // Silently limit tool calls
782
967
  toolCalls = toolCalls.slice(0, MAX_TOOL_CALLS_PER_TURN);
783
968
  }
784
- const toolResults = [];
785
- const handledToolCallIds = new Set(); // Track tool calls that were handled directly (e.g., create_plan, mark_task_complete)
969
+ const toolResults = [...inStreamToolResults]; // Start with in-stream results
970
+ const handledToolCallIds = new Set(); // Only for special tools (create_plan, mark_task_complete)
786
971
  let userCancelledOperation = false;
787
972
  let taskCompleted = false;
788
973
  let taskCompleteSummary = '';
789
974
  for (let i = 0; i < toolCalls.length; i++) {
975
+ const toolCall = toolCalls[i];
976
+ // REAL-TIME EXECUTION: Skip tools that were already executed in-stream
977
+ if (inStreamHandledIds.has(toolCall.id)) {
978
+ try {
979
+ quickLog(`[${new Date().toISOString()}] [CLI] *** SKIPPING TOOL (already executed in-stream): ${toolCall.name}\n`);
980
+ }
981
+ catch (e) { }
982
+ continue;
983
+ }
790
984
  // Debug: Log which tool we're about to execute
791
985
  try {
792
986
  quickLog(`[${new Date().toISOString()}] [CLI] *** ABOUT TO EXECUTE TOOL [${i + 1}/${toolCalls.length}]: ${toolCalls[i].name}\n`);
793
987
  }
794
988
  catch (e) { }
795
- const toolCall = toolCalls[i];
796
989
  try {
797
990
  // Check if this is task_complete FIRST (before displaying anything)
798
991
  if (toolCall.name === 'task_complete') {
@@ -804,6 +997,11 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
804
997
  if (taskCompleteSummary) {
805
998
  // AI provided a summary - suppress any other text and use the summary
806
999
  assistantMessage = '';
1000
+ // IMPORTANT: Clear the already-streamed text from UI before showing summary
1001
+ // This prevents duplicate content (streamed text + summary)
1002
+ if (this.onClearStreamedResponse) {
1003
+ this.onClearStreamedResponse();
1004
+ }
807
1005
  // Stream the summary to UI so it's visible
808
1006
  if (this.onResponseStreamCallback) {
809
1007
  this.onResponseStreamCallback(taskCompleteSummary);
@@ -845,11 +1043,18 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
845
1043
  this.onPlanModeChange(false);
846
1044
  }
847
1045
  // Add assistant message with plan tool call to history
848
- this.conversationHistory.push({
1046
+ const planAssistantMsg = {
849
1047
  role: 'assistant',
850
1048
  content: '',
851
1049
  tool_calls: [toolCall],
852
- });
1050
+ };
1051
+ if (currentTurnThinking) {
1052
+ planAssistantMsg.thinking = currentTurnThinking;
1053
+ }
1054
+ if (currentTurnThinkingSignature) {
1055
+ planAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1056
+ }
1057
+ this.conversationHistory.push(planAssistantMsg);
853
1058
  // Add plan approval response
854
1059
  this.conversationHistory.push({
855
1060
  role: 'tool',
@@ -885,11 +1090,18 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
885
1090
  else {
886
1091
  // No approval callback - add the tool result to history and wait for user response
887
1092
  // This ensures the AI doesn't get stuck in a silent loop
888
- this.conversationHistory.push({
1093
+ const planAssistantMsg = {
889
1094
  role: 'assistant',
890
1095
  content: '',
891
1096
  tool_calls: [toolCall],
892
- });
1097
+ };
1098
+ if (currentTurnThinking) {
1099
+ planAssistantMsg.thinking = currentTurnThinking;
1100
+ }
1101
+ if (currentTurnThinkingSignature) {
1102
+ planAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1103
+ }
1104
+ this.conversationHistory.push(planAssistantMsg);
893
1105
  this.conversationHistory.push({
894
1106
  role: 'tool',
895
1107
  tool_call_id: toolCall.id,
@@ -909,11 +1121,18 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
909
1121
  // Log error and add error result to history so AI knows
910
1122
  logWarning(`Failed to parse plan: ${parseError?.message || parseError}`);
911
1123
  // CRITICAL: Add tool result even on parse error to prevent silent loop
912
- this.conversationHistory.push({
1124
+ const errorAssistantMsg = {
913
1125
  role: 'assistant',
914
1126
  content: '',
915
1127
  tool_calls: [toolCall],
916
- });
1128
+ };
1129
+ if (currentTurnThinking) {
1130
+ errorAssistantMsg.thinking = currentTurnThinking;
1131
+ }
1132
+ if (currentTurnThinkingSignature) {
1133
+ errorAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1134
+ }
1135
+ this.conversationHistory.push(errorAssistantMsg);
917
1136
  this.conversationHistory.push({
918
1137
  role: 'tool',
919
1138
  tool_call_id: toolCall.id,
@@ -926,11 +1145,18 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
926
1145
  }
927
1146
  else {
928
1147
  // Tool returned non-PLAN_CREATED result - add it to history
929
- this.conversationHistory.push({
1148
+ const resultAssistantMsg = {
930
1149
  role: 'assistant',
931
1150
  content: '',
932
1151
  tool_calls: [toolCall],
933
- });
1152
+ };
1153
+ if (currentTurnThinking) {
1154
+ resultAssistantMsg.thinking = currentTurnThinking;
1155
+ }
1156
+ if (currentTurnThinkingSignature) {
1157
+ resultAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1158
+ }
1159
+ this.conversationHistory.push(resultAssistantMsg);
934
1160
  this.conversationHistory.push({
935
1161
  role: 'tool',
936
1162
  tool_call_id: toolCall.id,
@@ -959,7 +1185,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
959
1185
  const mainTaskNum = parseInt(taskNumParts[0], 10) - 1;
960
1186
  const task = currentPlanData.steps[mainTaskNum];
961
1187
  if (task) {
962
- this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote);
1188
+ this.onTaskCompleted(task, completion.taskNumber, completion.totalCount, completion.completionNote, completion.taskDescription // Pass the actual task/subtask description
1189
+ );
963
1190
  }
964
1191
  }
965
1192
  // Notify UI about completed task/subtask
@@ -979,11 +1206,18 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
979
1206
  result: `Task completed. Moving to Task ${nextPhase.taskNumber}: ${nextPhase.task.description}`,
980
1207
  });
981
1208
  // Add the tool call and result to history
982
- this.conversationHistory.push({
1209
+ const nextPhaseAssistantMsg = {
983
1210
  role: 'assistant',
984
1211
  content: '',
985
1212
  tool_calls: [toolCall],
986
- });
1213
+ };
1214
+ if (currentTurnThinking) {
1215
+ nextPhaseAssistantMsg.thinking = currentTurnThinking;
1216
+ }
1217
+ if (currentTurnThinkingSignature) {
1218
+ nextPhaseAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1219
+ }
1220
+ this.conversationHistory.push(nextPhaseAssistantMsg);
987
1221
  this.conversationHistory.push({
988
1222
  role: 'tool',
989
1223
  tool_call_id: toolCall.id,
@@ -1216,12 +1450,19 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1216
1450
  }
1217
1451
  // If user cancelled an operation, stop the agentic loop immediately
1218
1452
  if (userCancelledOperation) {
1219
- // Add assistant message to history
1220
- this.conversationHistory.push({
1453
+ // Add assistant message to history with thinking if available
1454
+ const cancelledAssistantMsg = {
1221
1455
  role: 'assistant',
1222
1456
  content: assistantMessage || '',
1223
1457
  tool_calls: toolCalls, // Store tool calls for MaaS models
1224
- });
1458
+ };
1459
+ if (currentTurnThinking) {
1460
+ cancelledAssistantMsg.thinking = currentTurnThinking;
1461
+ }
1462
+ if (currentTurnThinkingSignature) {
1463
+ cancelledAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1464
+ }
1465
+ this.conversationHistory.push(cancelledAssistantMsg);
1225
1466
  // Add tool results to history
1226
1467
  for (const toolResult of toolResults) {
1227
1468
  this.conversationHistory.push({
@@ -1259,11 +1500,26 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1259
1500
  const unhandledToolResults = toolResults.filter(tr => !handledToolCallIds.has(tr.tool_call_id));
1260
1501
  // Only add assistant message if there are unhandled tool calls
1261
1502
  if (unhandledToolCalls.length > 0) {
1262
- this.conversationHistory.push({
1503
+ const assistantHistoryMsg = {
1263
1504
  role: 'assistant',
1264
1505
  content: assistantMessage || '',
1265
1506
  tool_calls: unhandledToolCalls, // Only include unhandled tool calls
1266
- });
1507
+ };
1508
+ // Include thinking from this turn if available (Extended Thinking pattern)
1509
+ if (currentTurnThinking) {
1510
+ assistantHistoryMsg.thinking = currentTurnThinking;
1511
+ }
1512
+ // Include thinking signature from this turn (required for Claude extended thinking)
1513
+ if (currentTurnThinkingSignature) {
1514
+ assistantHistoryMsg.thinkingSignature = currentTurnThinkingSignature;
1515
+ }
1516
+ // Log signature info for debugging multi-turn flows
1517
+ const geminiSigCount = unhandledToolCalls.filter(tc => !!tc.thoughtSignature).length;
1518
+ try {
1519
+ quickLog(`[${new Date().toISOString()}] [CLI] *** STORING ASSISTANT MSG: ${unhandledToolCalls.length} tool_calls, Gemini signatures: ${geminiSigCount}, Claude thinking: ${!!currentTurnThinking}, Claude sig: ${!!currentTurnThinkingSignature}\n`);
1520
+ }
1521
+ catch (e) { }
1522
+ this.conversationHistory.push(assistantHistoryMsg);
1267
1523
  }
1268
1524
  // Add tool results to conversation history as tool messages
1269
1525
  // Format: { tool_call_id, name, result: <object or string> }
@@ -1278,8 +1534,10 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1278
1534
  });
1279
1535
  }
1280
1536
  // Rebuild messages array with updated history
1281
- // Backend will inject system prompt
1537
+ // During agent loop: keep ALL thinking for current task
1538
+ // (Thinking from previous tasks was already stripped at request start)
1282
1539
  messages = [...this.conversationHistory];
1540
+ // No need to reset currentTurnThinking - keep accumulating for the task
1283
1541
  // Re-inject subshell context
1284
1542
  messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
1285
1543
  continue; // Loop back to AI service
@@ -1469,8 +1727,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1469
1727
  role: 'assistant',
1470
1728
  content: finalMessage,
1471
1729
  });
1472
- // Save assistant message to backend
1473
- await this.saveMessageToBackend('assistant', finalMessage);
1730
+ // Messages are stored locally only via saveCurrentChat() below
1474
1731
  } // End of while loop
1475
1732
  // Auto-save conversation to local storage
1476
1733
  this.saveCurrentChat();
@@ -1511,11 +1768,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1511
1768
  `/init - Analyze project and create/load centaurus.md context file\n` +
1512
1769
  `/chat - Manage chat sessions (resume previous chats)\n` +
1513
1770
  `/clear - Clear conversation and start a new chat\n` +
1771
+ `/sync - Sync data to/from cloud (upload/restore)\n` +
1514
1772
  `/config - View current configuration\n` +
1515
1773
  `/model - Select from available Google models\n` +
1516
1774
  `/plan - Toggle plan mode for complex implementations\n` +
1517
1775
  `/mcp - Manage configured MCP servers and tools\n` +
1518
1776
  `/docs - Open Centaurus documentation in browser\n` +
1777
+ `/copy-chat-context - Copy chat as readable text to clipboard\n` +
1778
+ `/session-limits - View session quota usage and limits\n` +
1519
1779
  `/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
1520
1780
  `/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
1521
1781
  `/sign-in - Sign in with Google (if not already signed in)\n` +
@@ -1529,6 +1789,32 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1529
1789
  `Ctrl+Z - Undo last input change\n` +
1530
1790
  `Ctrl+A - Select all text`;
1531
1791
  break;
1792
+ case 'session-limits': {
1793
+ const config = sessionQuotaManager.getCurrentConfig();
1794
+ const messagesUsed = sessionQuotaManager.getMessagesUsed();
1795
+ const remaining = sessionQuotaManager.getRemainingMessages();
1796
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
1797
+ const maxMessages = config.maxMessagesPerSession;
1798
+ // Calculate percentage used (cap at 100% for display)
1799
+ const percentUsed = maxMessages > 0 ? Math.min(100, Math.round((messagesUsed / maxMessages) * 100)) : 0;
1800
+ // Create a visual progress bar (clamp to valid range)
1801
+ const barLength = 20;
1802
+ const filledLength = Math.min(barLength, Math.max(0, Math.round((messagesUsed / maxMessages) * barLength)));
1803
+ const emptyLength = barLength - filledLength;
1804
+ const progressBar = '█'.repeat(filledLength) + '░'.repeat(emptyLength);
1805
+ // Status message based on quota
1806
+ const quotaStatus = remaining <= 0
1807
+ ? '\n\n⚠️ Session quota exhausted! AI requests are blocked until reset.'
1808
+ : '';
1809
+ responseMessage = `📊 Session Limits\n\n` +
1810
+ `Plan: free\n` +
1811
+ `Session Window: ${config.sessionDurationMs / (60 * 60 * 1000)} hours\n\n` +
1812
+ `Messages Used: ${messagesUsed} / ${maxMessages} (${percentUsed}%)\n` +
1813
+ `Messages Left: ${Math.max(0, remaining)}\n` +
1814
+ `Progress: [${progressBar}]\n` +
1815
+ `Time Remaining: ${timeRemaining || 'Session not started'}${quotaStatus}`;
1816
+ break;
1817
+ }
1532
1818
  case 'init':
1533
1819
  try {
1534
1820
  // Define the context file names in priority order
@@ -1938,7 +2224,7 @@ Start by listing the directory structure to understand what you're working with.
1938
2224
  // Show list of saved chats for resuming (interactive picker)
1939
2225
  const chats = localChatStorage.listChats();
1940
2226
  if (chats.length === 0) {
1941
- responseMessage = '📭 No saved chats found.\\n\\nStart a new conversation and it will be automatically saved!';
2227
+ responseMessage = 'No saved chats found.\n\nStart a new conversation and it will be automatically saved!';
1942
2228
  }
1943
2229
  else {
1944
2230
  // Format chat list for picker display
@@ -1948,13 +2234,13 @@ Start by listing the directory structure to understand what you're working with.
1948
2234
  }
1949
2235
  else {
1950
2236
  // Fallback: show as text if no picker callback
1951
- responseMessage = '📚 Saved Chats:\\n\\n' +
2237
+ responseMessage = 'Saved Chats:\n\n' +
1952
2238
  chats.slice(0, 10).map((chat, i) => {
1953
2239
  const date = new Date(chat.updatedAt).toLocaleDateString();
1954
2240
  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` : '');
2241
+ return `${i + 1}. ${chat.title}\n ${date} ${time} | ${chat.messageCount} messages`;
2242
+ }).join('\n\n') +
2243
+ (chats.length > 10 ? `\n\n...and ${chats.length - 10} more chats` : '');
1958
2244
  }
1959
2245
  }
1960
2246
  }
@@ -1962,7 +2248,7 @@ Start by listing the directory structure to understand what you're working with.
1962
2248
  // Show read-only list of saved chats (no selection)
1963
2249
  const chats = localChatStorage.listChats();
1964
2250
  if (chats.length === 0) {
1965
- responseMessage = '📭 No saved chats found.\\n\\nStart a new conversation and it will be automatically saved!';
2251
+ responseMessage = 'No saved chats found.\n\nStart a new conversation and it will be automatically saved!';
1966
2252
  }
1967
2253
  else {
1968
2254
  // Format chat list for read-only display
@@ -1987,7 +2273,7 @@ Start by listing the directory structure to understand what you're working with.
1987
2273
  // Show list of saved chats for deletion
1988
2274
  const chats = localChatStorage.listChats();
1989
2275
  if (chats.length === 0) {
1990
- responseMessage = '📭 No saved chats to delete.';
2276
+ responseMessage = 'No saved chats to delete.';
1991
2277
  }
1992
2278
  else {
1993
2279
  // Format chat list for delete picker display
@@ -2007,13 +2293,13 @@ Start by listing the directory structure to understand what you're working with.
2007
2293
  if (this.onRestoreMessagesCallback) {
2008
2294
  this.onRestoreMessagesCallback([]);
2009
2295
  }
2010
- responseMessage = '🆕 Started a new chat session!\n\nYour previous conversation has been saved and can be resumed with /chat resume.';
2296
+ responseMessage = ' Started a new chat session!\n\nYour previous conversation has been saved and can be resumed with /chat resume.';
2011
2297
  }
2012
2298
  else if (chatSubCommand === 'rename') {
2013
2299
  // Show list of saved chats for renaming
2014
2300
  const chats = localChatStorage.listChats();
2015
2301
  if (chats.length === 0) {
2016
- responseMessage = '📭 No saved chats to rename.';
2302
+ responseMessage = 'No saved chats to rename.';
2017
2303
  }
2018
2304
  else {
2019
2305
  // Format chat list for rename picker display
@@ -2030,6 +2316,30 @@ Start by listing the directory structure to understand what you're working with.
2030
2316
  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
2317
  }
2032
2318
  break;
2319
+ case 'copy-chat-context':
2320
+ try {
2321
+ const { formatChatForClipboard } = await import('./utils/chat-formatter.js');
2322
+ const { copyTextToClipboard } = await import('./utils/text-clipboard.js');
2323
+ if (this.uiMessageHistory.length === 0) {
2324
+ responseMessage = 'No messages in current chat to copy.';
2325
+ }
2326
+ else {
2327
+ const formattedChat = formatChatForClipboard(this.uiMessageHistory);
2328
+ const success = await copyTextToClipboard(formattedChat);
2329
+ if (success) {
2330
+ responseMessage = '✅ Chat content copied to clipboard!';
2331
+ }
2332
+ else {
2333
+ responseMessage = '❌ Failed to copy to clipboard.\n\n' +
2334
+ 'This might happen if clipboard access is not available in your environment.\n' +
2335
+ 'Try running the CLI in a terminal with clipboard access.';
2336
+ }
2337
+ }
2338
+ }
2339
+ catch (error) {
2340
+ responseMessage = `❌ Error copying chat content: ${error.message}`;
2341
+ }
2342
+ break;
2033
2343
  case 'exit':
2034
2344
  process.exit(0);
2035
2345
  break;
@@ -2147,6 +2457,138 @@ Start by listing the directory structure to understand what you're working with.
2147
2457
  ' /background-task cancel - Cancel a running background task';
2148
2458
  }
2149
2459
  break;
2460
+ case 'sync':
2461
+ // Sync local data to/from cloud
2462
+ if (!apiClient.isAuthenticated()) {
2463
+ responseMessage = '❌ You must be signed in to sync data.\n\nUse /sign-in to authenticate first.';
2464
+ break;
2465
+ }
2466
+ // Parse subcommand
2467
+ const syncSubcommand = args[0]?.toLowerCase();
2468
+ if (!syncSubcommand) {
2469
+ // Show sync help
2470
+ responseMessage = '☁️ Sync Commands:\n\n' +
2471
+ ' /sync upload - Upload local data to cloud (overwrites cloud data)\n' +
2472
+ ' /sync restore - Download cloud data and restore locally (overwrites local data)\n\n' +
2473
+ 'Use these commands to backup or restore your chat history and settings.';
2474
+ break;
2475
+ }
2476
+ if (syncSubcommand === 'upload') {
2477
+ try {
2478
+ responseMessage = '☁️ Uploading data to cloud...';
2479
+ // Send initial response
2480
+ if (this.onDirectMessageCallback) {
2481
+ this.onDirectMessageCallback(responseMessage);
2482
+ }
2483
+ // Gather all local data
2484
+ const config = this.configManager.load();
2485
+ const chats = localChatStorage.listChats();
2486
+ // Load full chat data for each chat
2487
+ const fullChats = chats.map(chatMeta => {
2488
+ const fullChat = localChatStorage.loadChat(chatMeta.id);
2489
+ return fullChat;
2490
+ }).filter(chat => chat !== null);
2491
+ // Create sync data structure
2492
+ const syncData = {
2493
+ version: 1,
2494
+ exportedAt: new Date().toISOString(),
2495
+ config: {
2496
+ model: config.model,
2497
+ modelName: config.modelName,
2498
+ enhancedQuality: config.enhancedQuality,
2499
+ autonomousMode: config.autonomousMode,
2500
+ },
2501
+ chats: fullChats,
2502
+ metadata: {
2503
+ totalChats: fullChats.length,
2504
+ totalMessages: fullChats.reduce((sum, chat) => sum + (chat?.messageCount || 0), 0),
2505
+ }
2506
+ };
2507
+ // Upload to backend
2508
+ const result = await apiClient.uploadSyncData(syncData);
2509
+ responseMessage = `✅ Data uploaded successfully!\n\n` +
2510
+ `📊 Upload Summary:\n` +
2511
+ ` • Chats: ${fullChats.length}\n` +
2512
+ ` • Messages: ${syncData.metadata.totalMessages}\n` +
2513
+ ` • Version: ${result.version}\n` +
2514
+ ` • Updated: ${new Date(result.updatedAt).toLocaleString()}\n\n` +
2515
+ `Your local data is now backed up to the cloud.`;
2516
+ }
2517
+ catch (error) {
2518
+ responseMessage = `❌ Upload failed: ${error.message}\n\nPlease try again later.`;
2519
+ }
2520
+ }
2521
+ else if (syncSubcommand === 'restore') {
2522
+ try {
2523
+ responseMessage = '☁️ Downloading data from cloud...';
2524
+ // Send initial response
2525
+ if (this.onDirectMessageCallback) {
2526
+ this.onDirectMessageCallback(responseMessage);
2527
+ }
2528
+ // Get sync data from backend
2529
+ const cloudData = await apiClient.getSyncData();
2530
+ if (!cloudData) {
2531
+ responseMessage = '❌ No cloud data found.\n\nUse /sync upload first to backup your data.';
2532
+ break;
2533
+ }
2534
+ const syncData = cloudData.syncData;
2535
+ // Validate sync data structure
2536
+ if (!syncData || typeof syncData !== 'object') {
2537
+ responseMessage = '❌ Invalid cloud data format.\n\nPlease try uploading again with /sync upload.';
2538
+ break;
2539
+ }
2540
+ // Restore config if present
2541
+ if (syncData.config) {
2542
+ const currentConfig = this.configManager.load();
2543
+ if (syncData.config.model) {
2544
+ currentConfig.model = syncData.config.model;
2545
+ }
2546
+ if (syncData.config.modelName) {
2547
+ currentConfig.modelName = syncData.config.modelName;
2548
+ }
2549
+ if (typeof syncData.config.enhancedQuality === 'boolean') {
2550
+ currentConfig.enhancedQuality = syncData.config.enhancedQuality;
2551
+ }
2552
+ if (typeof syncData.config.autonomousMode === 'boolean') {
2553
+ currentConfig.autonomousMode = syncData.config.autonomousMode;
2554
+ }
2555
+ this.configManager.save(currentConfig);
2556
+ }
2557
+ // Restore chats if present
2558
+ let restoredChats = 0;
2559
+ let restoredMessages = 0;
2560
+ if (syncData.chats && Array.isArray(syncData.chats)) {
2561
+ for (const chat of syncData.chats) {
2562
+ if (chat && chat.id) {
2563
+ // Import each chat to local storage
2564
+ const success = localChatStorage.importChat(chat);
2565
+ if (success) {
2566
+ restoredChats++;
2567
+ restoredMessages += chat.messageCount || chat.messages?.length || 0;
2568
+ }
2569
+ }
2570
+ }
2571
+ }
2572
+ responseMessage = `✅ Data restored successfully!\n\n` +
2573
+ `📊 Restore Summary:\n` +
2574
+ ` • Chats restored: ${restoredChats}\n` +
2575
+ ` • Messages restored: ${restoredMessages}\n` +
2576
+ ` • Cloud version: ${cloudData.version}\n` +
2577
+ ` • Last updated: ${new Date(cloudData.updatedAt).toLocaleString()}\n\n` +
2578
+ `Your local data has been restored from the cloud.\n` +
2579
+ `Note: Existing local chats with the same ID were overwritten.`;
2580
+ }
2581
+ catch (error) {
2582
+ responseMessage = `❌ Restore failed: ${error.message}\n\nPlease try again later.`;
2583
+ }
2584
+ }
2585
+ else {
2586
+ responseMessage = `❌ Unknown sync subcommand: ${syncSubcommand}\n\n` +
2587
+ 'Available subcommands:\n' +
2588
+ ' /sync upload - Upload local data to cloud\n' +
2589
+ ' /sync restore - Download and restore cloud data locally';
2590
+ }
2591
+ break;
2150
2592
  default:
2151
2593
  responseMessage = `Unknown command: /${cmd}\nType /help for available commands.`;
2152
2594
  }