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.
- package/dist/cli-adapter.d.ts +29 -2
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +526 -84
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/build-config.d.ts +1 -1
- package/dist/config/build-config.js +1 -1
- package/dist/config/models.d.ts +8 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +29 -0
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.d.ts +1 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +14 -1
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/hooks/useConnectivity.d.ts +2 -0
- package/dist/hooks/useConnectivity.d.ts.map +1 -0
- package/dist/hooks/useConnectivity.js +12 -0
- package/dist/hooks/useConnectivity.js.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
- package/dist/mcp/mcp-command-handler.js +0 -3
- package/dist/mcp/mcp-command-handler.js.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.js +8 -0
- package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
- package/dist/services/ai-service-client.d.ts +7 -1
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +6 -6
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.d.ts +35 -38
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +38 -30
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/connectivity-manager.d.ts +18 -0
- package/dist/services/connectivity-manager.d.ts.map +1 -0
- package/dist/services/connectivity-manager.js +72 -0
- package/dist/services/connectivity-manager.js.map +1 -0
- package/dist/services/local-chat-storage.d.ts +5 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -1
- package/dist/services/local-chat-storage.js +33 -0
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/session-quota-manager.d.ts +101 -0
- package/dist/services/session-quota-manager.d.ts.map +1 -0
- package/dist/services/session-quota-manager.js +242 -0
- package/dist/services/session-quota-manager.js.map +1 -0
- package/dist/tools/background-command.d.ts +11 -0
- package/dist/tools/background-command.d.ts.map +1 -0
- package/dist/tools/background-command.js +162 -0
- package/dist/tools/background-command.js.map +1 -0
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +20 -6
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/create-image.d.ts +10 -0
- package/dist/tools/create-image.d.ts.map +1 -0
- package/dist/tools/create-image.js +189 -0
- package/dist/tools/create-image.js.map +1 -0
- package/dist/tools/get-diff.d.ts.map +1 -1
- package/dist/tools/get-diff.js +4 -1
- package/dist/tools/get-diff.js.map +1 -1
- package/dist/tools/task-complete.d.ts.map +1 -1
- package/dist/tools/task-complete.js +8 -14
- package/dist/tools/task-complete.js.map +1 -1
- package/dist/ui/components/App.d.ts +5 -2
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +165 -45
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/ContextWindowIndicator.d.ts.map +1 -1
- package/dist/ui/components/ContextWindowIndicator.js +43 -22
- package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +2 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +217 -200
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +8 -15
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/SlashCommandAutocomplete.d.ts +2 -0
- package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +1 -1
- package/dist/ui/components/SlashCommandAutocomplete.js +19 -10
- package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
- package/dist/ui/components/StatusBar.d.ts.map +1 -1
- package/dist/ui/components/StatusBar.js +4 -0
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +198 -39
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.js +1 -0
- package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
- package/dist/utils/chat-formatter.d.ts +12 -0
- package/dist/utils/chat-formatter.d.ts.map +1 -0
- package/dist/utils/chat-formatter.js +326 -0
- package/dist/utils/chat-formatter.js.map +1 -0
- package/dist/utils/editor-utils.d.ts +3 -3
- package/dist/utils/editor-utils.d.ts.map +1 -1
- package/dist/utils/editor-utils.js +15 -12
- package/dist/utils/editor-utils.js.map +1 -1
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +140 -20
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/terminal-output.d.ts.map +1 -1
- package/dist/utils/terminal-output.js +198 -171
- package/dist/utils/terminal-output.js.map +1 -1
- package/dist/utils/text-clipboard.d.ts +12 -0
- package/dist/utils/text-clipboard.d.ts.map +1 -0
- package/dist/utils/text-clipboard.js +63 -0
- package/dist/utils/text-clipboard.js.map +1 -0
- package/package.json +1 -1
package/dist/cli-adapter.js
CHANGED
|
@@ -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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
//
|
|
559
|
-
|
|
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) =>
|
|
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
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
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'}
|
|
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
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
//
|
|
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] ***
|
|
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(); //
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 = '
|
|
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 = '
|
|
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}
|
|
1956
|
-
}).join('
|
|
1957
|
-
(chats.length > 10 ?
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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 = '
|
|
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
|
}
|