centaurus-cli 2.9.1 → 2.9.2

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 (127) hide show
  1. package/dist/cli-adapter.d.ts +70 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +349 -156
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/mcp-config-manager.d.ts +21 -0
  6. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  7. package/dist/config/mcp-config-manager.js +184 -1
  8. package/dist/config/mcp-config-manager.js.map +1 -1
  9. package/dist/config/models.d.ts +1 -0
  10. package/dist/config/models.d.ts.map +1 -1
  11. package/dist/config/models.js +7 -2
  12. package/dist/config/models.js.map +1 -1
  13. package/dist/config/slash-commands.d.ts.map +1 -1
  14. package/dist/config/slash-commands.js +4 -3
  15. package/dist/config/slash-commands.js.map +1 -1
  16. package/dist/index.js +60 -11
  17. package/dist/index.js.map +1 -1
  18. package/dist/mcp/mcp-command-handler.d.ts +34 -3
  19. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  20. package/dist/mcp/mcp-command-handler.js +171 -83
  21. package/dist/mcp/mcp-command-handler.js.map +1 -1
  22. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  23. package/dist/mcp/mcp-server-manager.js +9 -23
  24. package/dist/mcp/mcp-server-manager.js.map +1 -1
  25. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  26. package/dist/mcp/mcp-tool-wrapper.js +42 -5
  27. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  28. package/dist/services/api-client.d.ts +9 -0
  29. package/dist/services/api-client.d.ts.map +1 -1
  30. package/dist/services/api-client.js +25 -0
  31. package/dist/services/api-client.js.map +1 -1
  32. package/dist/services/input-detection-agent.d.ts +40 -0
  33. package/dist/services/input-detection-agent.d.ts.map +1 -0
  34. package/dist/services/input-detection-agent.js +213 -0
  35. package/dist/services/input-detection-agent.js.map +1 -0
  36. package/dist/services/input-requirement-detector.d.ts +28 -0
  37. package/dist/services/input-requirement-detector.d.ts.map +1 -0
  38. package/dist/services/input-requirement-detector.js +203 -0
  39. package/dist/services/input-requirement-detector.js.map +1 -0
  40. package/dist/services/monitored-shell-manager.d.ts +120 -0
  41. package/dist/services/monitored-shell-manager.d.ts.map +1 -0
  42. package/dist/services/monitored-shell-manager.js +239 -0
  43. package/dist/services/monitored-shell-manager.js.map +1 -0
  44. package/dist/services/shell-input-agent.d.ts +89 -0
  45. package/dist/services/shell-input-agent.d.ts.map +1 -0
  46. package/dist/services/shell-input-agent.js +361 -0
  47. package/dist/services/shell-input-agent.js.map +1 -0
  48. package/dist/services/sub-agent-manager.d.ts +139 -0
  49. package/dist/services/sub-agent-manager.d.ts.map +1 -0
  50. package/dist/services/sub-agent-manager.js +517 -0
  51. package/dist/services/sub-agent-manager.js.map +1 -0
  52. package/dist/tools/background-command.d.ts.map +1 -1
  53. package/dist/tools/background-command.js +33 -13
  54. package/dist/tools/background-command.js.map +1 -1
  55. package/dist/tools/command.d.ts.map +1 -1
  56. package/dist/tools/command.js +64 -1
  57. package/dist/tools/command.js.map +1 -1
  58. package/dist/tools/file-ops.d.ts.map +1 -1
  59. package/dist/tools/file-ops.js +33 -19
  60. package/dist/tools/file-ops.js.map +1 -1
  61. package/dist/tools/get-diff.js +1 -1
  62. package/dist/tools/get-diff.js.map +1 -1
  63. package/dist/tools/grep-search.d.ts.map +1 -1
  64. package/dist/tools/grep-search.js +41 -15
  65. package/dist/tools/grep-search.js.map +1 -1
  66. package/dist/tools/plan-mode.js +3 -3
  67. package/dist/tools/plan-mode.js.map +1 -1
  68. package/dist/tools/registry.js +1 -1
  69. package/dist/tools/registry.js.map +1 -1
  70. package/dist/tools/sub-agent.d.ts +9 -0
  71. package/dist/tools/sub-agent.d.ts.map +1 -0
  72. package/dist/tools/sub-agent.js +232 -0
  73. package/dist/tools/sub-agent.js.map +1 -0
  74. package/dist/tools/task-complete.d.ts.map +1 -1
  75. package/dist/tools/task-complete.js +14 -26
  76. package/dist/tools/task-complete.js.map +1 -1
  77. package/dist/ui/components/App.d.ts +43 -0
  78. package/dist/ui/components/App.d.ts.map +1 -1
  79. package/dist/ui/components/App.js +560 -94
  80. package/dist/ui/components/App.js.map +1 -1
  81. package/dist/ui/components/CircularSelectInput.d.ts +24 -0
  82. package/dist/ui/components/CircularSelectInput.d.ts.map +1 -0
  83. package/dist/ui/components/CircularSelectInput.js +71 -0
  84. package/dist/ui/components/CircularSelectInput.js.map +1 -0
  85. package/dist/ui/components/ErrorBoundary.d.ts +3 -2
  86. package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
  87. package/dist/ui/components/ErrorBoundary.js +29 -1
  88. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  89. package/dist/ui/components/InputBox.d.ts +2 -0
  90. package/dist/ui/components/InputBox.d.ts.map +1 -1
  91. package/dist/ui/components/InputBox.js +23 -3
  92. package/dist/ui/components/InputBox.js.map +1 -1
  93. package/dist/ui/components/InteractiveShell.d.ts +6 -0
  94. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  95. package/dist/ui/components/InteractiveShell.js +57 -6
  96. package/dist/ui/components/InteractiveShell.js.map +1 -1
  97. package/dist/ui/components/MCPAddScreen.d.ts +13 -0
  98. package/dist/ui/components/MCPAddScreen.d.ts.map +1 -0
  99. package/dist/ui/components/MCPAddScreen.js +54 -0
  100. package/dist/ui/components/MCPAddScreen.js.map +1 -0
  101. package/dist/ui/components/MCPListScreen.d.ts +17 -0
  102. package/dist/ui/components/MCPListScreen.d.ts.map +1 -0
  103. package/dist/ui/components/MCPListScreen.js +50 -0
  104. package/dist/ui/components/MCPListScreen.js.map +1 -0
  105. package/dist/ui/components/MCPServerListScreen.d.ts +16 -0
  106. package/dist/ui/components/MCPServerListScreen.d.ts.map +1 -0
  107. package/dist/ui/components/MCPServerListScreen.js +59 -0
  108. package/dist/ui/components/MCPServerListScreen.js.map +1 -0
  109. package/dist/ui/components/MonitorModeAIPanel.d.ts +23 -0
  110. package/dist/ui/components/MonitorModeAIPanel.d.ts.map +1 -0
  111. package/dist/ui/components/MonitorModeAIPanel.js +69 -0
  112. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -0
  113. package/dist/ui/components/MultiLineInput.d.ts +13 -0
  114. package/dist/ui/components/MultiLineInput.d.ts.map +1 -0
  115. package/dist/ui/components/MultiLineInput.js +223 -0
  116. package/dist/ui/components/MultiLineInput.js.map +1 -0
  117. package/dist/ui/components/StatusBar.d.ts +2 -0
  118. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  119. package/dist/ui/components/StatusBar.js +33 -2
  120. package/dist/ui/components/StatusBar.js.map +1 -1
  121. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  122. package/dist/ui/components/ToolExecutionMessage.js +226 -12
  123. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  124. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  125. package/dist/ui/components/VersionUpdatePrompt.js +3 -2
  126. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  127. package/package.json +2 -1
@@ -19,6 +19,9 @@ import { taskCompleteTool } from './tools/task-complete.js';
19
19
  import { readBinaryFileTool } from './tools/read-binary-file.js';
20
20
  import { createImageTool } from './tools/create-image.js';
21
21
  import { backgroundCommandTool } from './tools/background-command.js';
22
+ import { subAgentTool } from './tools/sub-agent.js';
23
+ import { SubAgentManager } from './services/sub-agent-manager.js';
24
+ import { ShellInputAgent } from './services/shell-input-agent.js';
22
25
  import { apiClient } from './services/api-client.js';
23
26
  import { conversationManager } from './services/conversation-manager.js';
24
27
  import { aiServiceClient } from './services/ai-service-client.js';
@@ -48,6 +51,7 @@ export class CentaurusCLI {
48
51
  pendingPlanRequest = null; // Stores original user request during planning phase
49
52
  commandMode = false;
50
53
  backgroundMode = false; // Background shell mode for running commands in background
54
+ shellIdCounter = 1;
51
55
  previousMode = 'execution';
52
56
  onResponseCallback;
53
57
  onDirectMessageCallback; // For slash commands - adds directly to history
@@ -94,7 +98,18 @@ export class CentaurusCLI {
94
98
  onShowBackgroundTaskCancelPickerCallback;
95
99
  onBackgroundTaskViewCallback;
96
100
  onTokenCountUpdate; // Report actual AI context token count to UI
101
+ currentTokenCount = 0; // Track current token count for context limit checking
102
+ contextLimitReached = false; // Track if context limit has been reached
103
+ onContextLimitReached; // Notify UI about context limit state
97
104
  onSessionQuotaUpdate;
105
+ // MCP screen callbacks
106
+ onShowMCPAddScreen;
107
+ onShowMCPRemoveScreen;
108
+ onShowMCPEnableScreen;
109
+ onShowMCPDisableScreen;
110
+ onShowMCPListScreen;
111
+ onSubAgentCountChange; // Callback for sub-agent count changes
112
+ onPromptAnswered; // Callback when AI answers a shell prompt
98
113
  constructor() {
99
114
  this.configManager = new ConfigManager();
100
115
  this.toolRegistry = new ToolRegistry();
@@ -115,6 +130,11 @@ export class CentaurusCLI {
115
130
  });
116
131
  // Initialize MCP
117
132
  this.initializeMCP();
133
+ // Initialize ShellInputAgent with tool registry and wire shell input callback
134
+ ShellInputAgent.initialize(this.toolRegistry);
135
+ ShellInputAgent.setOnShellInput((shellId, input) => {
136
+ this.writeToShellStdin(input);
137
+ });
118
138
  }
119
139
  setOnResponseCallback(callback) {
120
140
  this.onResponseCallback = callback;
@@ -186,9 +206,131 @@ export class CentaurusCLI {
186
206
  setOnTokenCountUpdate(callback) {
187
207
  this.onTokenCountUpdate = callback;
188
208
  }
209
+ setOnContextLimitReached(callback) {
210
+ this.onContextLimitReached = callback;
211
+ }
212
+ setOnSubAgentCountChange(callback) {
213
+ this.onSubAgentCountChange = callback;
214
+ }
215
+ setOnPromptAnswered(callback) {
216
+ this.onPromptAnswered = callback;
217
+ // Wire this callback to ShellInputAgent
218
+ ShellInputAgent.setOnPromptAnswered(callback);
219
+ }
220
+ /**
221
+ * Calculate and update token count based on current conversation history
222
+ * This ensures UI is always in sync with the actual AI context
223
+ * Uses backend's accurate token counting API (Vertex AI countTokens)
224
+ */
225
+ async updateTokenCount() {
226
+ if (!this.onTokenCountUpdate)
227
+ return;
228
+ try {
229
+ // Get current model
230
+ const currentModel = this.configManager.get('modelName') || 'gemini-2.5-flash';
231
+ // Prepare messages for token counting
232
+ // Backend will automatically include system prompt when counting
233
+ // We just send the conversation history
234
+ const messagesForCounting = [...this.conversationHistory];
235
+ // Call backend API for accurate token counting
236
+ const tokenCount = await apiClient.countTokens(currentModel, messagesForCounting);
237
+ // Store locally for context limit checking
238
+ this.currentTokenCount = tokenCount;
239
+ // Update UI with accurate count
240
+ this.onTokenCountUpdate(tokenCount);
241
+ quickLog(`[${new Date().toISOString()}] [updateTokenCount] Accurate count: ${tokenCount} tokens for ${messagesForCounting.length} messages\n`);
242
+ }
243
+ catch (error) {
244
+ // Fallback to character-based estimation if API fails
245
+ const SYSTEM_PROMPT_ESTIMATE = 14000; // Backend injects ~14K char system prompt
246
+ // Calculate total characters from conversation history
247
+ let totalCharacters = 0;
248
+ for (const msg of this.conversationHistory) {
249
+ // Content
250
+ if (typeof msg.content === 'string') {
251
+ totalCharacters += msg.content.length;
252
+ }
253
+ // Thinking content
254
+ if (msg.thinking) {
255
+ totalCharacters += msg.thinking.length;
256
+ }
257
+ // Tool calls
258
+ if (msg.tool_calls) {
259
+ for (const tc of msg.tool_calls) {
260
+ totalCharacters += tc.name.length;
261
+ if (tc.arguments) {
262
+ totalCharacters += JSON.stringify(tc.arguments).length;
263
+ }
264
+ }
265
+ }
266
+ // Tool call ID
267
+ if (msg.role === 'tool' && msg.tool_call_id) {
268
+ totalCharacters += msg.tool_call_id.length;
269
+ }
270
+ }
271
+ // Estimate tokens (1 token ≈ 4 chars)
272
+ // Only include system prompt estimate if there's conversation history
273
+ const systemPromptChars = this.conversationHistory.length > 0 ? SYSTEM_PROMPT_ESTIMATE : 0;
274
+ const estimatedTokens = Math.ceil((totalCharacters + systemPromptChars) / 4);
275
+ // Store locally for context limit checking
276
+ this.currentTokenCount = estimatedTokens;
277
+ this.onTokenCountUpdate(estimatedTokens);
278
+ quickLog(`[${new Date().toISOString()}] [updateTokenCount] Fallback estimate: ${estimatedTokens} tokens (API error: ${error})\n`);
279
+ }
280
+ }
281
+ /**
282
+ * Get current token count for context limit checking
283
+ */
284
+ getCurrentTokenCount() {
285
+ return this.currentTokenCount;
286
+ }
189
287
  setOnSessionQuotaUpdate(callback) {
190
288
  this.onSessionQuotaUpdate = callback;
191
289
  }
290
+ // MCP screen callback setters
291
+ setOnMCPAddScreenSetup(callback) {
292
+ this.onShowMCPAddScreen = callback;
293
+ }
294
+ setOnMCPRemoveScreenSetup(callback) {
295
+ this.onShowMCPRemoveScreen = callback;
296
+ }
297
+ setOnMCPEnableScreenSetup(callback) {
298
+ this.onShowMCPEnableScreen = callback;
299
+ }
300
+ setOnMCPDisableScreenSetup(callback) {
301
+ this.onShowMCPDisableScreen = callback;
302
+ }
303
+ setOnMCPListScreenSetup(callback) {
304
+ this.onShowMCPListScreen = callback;
305
+ }
306
+ // MCP server operation methods (called from UI)
307
+ mcpAddServer(config) {
308
+ if (this.mcpCommandHandler) {
309
+ return this.mcpCommandHandler.addServer(config);
310
+ }
311
+ return { success: false, error: 'MCP not initialized' };
312
+ }
313
+ mcpRemoveServer(name) {
314
+ if (this.mcpCommandHandler) {
315
+ this.mcpCommandHandler.removeServer(name);
316
+ }
317
+ }
318
+ mcpEnableServer(name) {
319
+ if (this.mcpCommandHandler) {
320
+ this.mcpCommandHandler.enableServer(name);
321
+ }
322
+ }
323
+ mcpDisableServer(name) {
324
+ if (this.mcpCommandHandler) {
325
+ this.mcpCommandHandler.disableServer(name);
326
+ }
327
+ }
328
+ mcpValidateConfig(jsonString) {
329
+ if (this.mcpCommandHandler) {
330
+ return this.mcpCommandHandler.validateServerConfig(jsonString);
331
+ }
332
+ return { valid: false, error: 'MCP not initialized' };
333
+ }
192
334
  /**
193
335
  * Notify UI about session quota status
194
336
  */
@@ -205,6 +347,32 @@ export class CentaurusCLI {
205
347
  const mcpConfigManager = new MCPConfigManager();
206
348
  const mcpServerManager = new MCPServerManager();
207
349
  this.mcpCommandHandler = new MCPCommandHandler(mcpConfigManager, mcpServerManager, this.toolRegistry);
350
+ // Wire MCP screen callbacks
351
+ this.mcpCommandHandler.setOnShowMCPAddScreen(() => {
352
+ if (this.onShowMCPAddScreen) {
353
+ this.onShowMCPAddScreen();
354
+ }
355
+ });
356
+ this.mcpCommandHandler.setOnShowMCPRemoveScreen((servers) => {
357
+ if (this.onShowMCPRemoveScreen) {
358
+ this.onShowMCPRemoveScreen(servers);
359
+ }
360
+ });
361
+ this.mcpCommandHandler.setOnShowMCPEnableScreen((servers) => {
362
+ if (this.onShowMCPEnableScreen) {
363
+ this.onShowMCPEnableScreen(servers);
364
+ }
365
+ });
366
+ this.mcpCommandHandler.setOnShowMCPDisableScreen((servers) => {
367
+ if (this.onShowMCPDisableScreen) {
368
+ this.onShowMCPDisableScreen(servers);
369
+ }
370
+ });
371
+ this.mcpCommandHandler.setOnShowMCPListScreen((servers) => {
372
+ if (this.onShowMCPListScreen) {
373
+ this.onShowMCPListScreen(servers);
374
+ }
375
+ });
208
376
  // Initialize MCP servers and tools
209
377
  await this.mcpCommandHandler.initializeMCP();
210
378
  }
@@ -249,12 +417,22 @@ export class CentaurusCLI {
249
417
  getCommandMode() {
250
418
  return this.commandMode;
251
419
  }
420
+ /**
421
+ * Get current conversation history for shell input agent context
422
+ * Returns a copy to prevent modification
423
+ */
424
+ getConversationHistory() {
425
+ return [...this.conversationHistory];
426
+ }
252
427
  getCurrentWorkingDirectory() {
253
428
  return this.cwd;
254
429
  }
255
430
  getCurrentSubshellContext() {
256
431
  return this.contextManager.getCurrentContext();
257
432
  }
433
+ getCurrentInteractiveProcess() {
434
+ return this.currentInteractiveProcess;
435
+ }
258
436
  /**
259
437
  * Get the current conversation ID for file uploads
260
438
  */
@@ -371,6 +549,14 @@ export class CentaurusCLI {
371
549
  this.toolRegistry.register(readBinaryFileTool);
372
550
  this.toolRegistry.register(createImageTool);
373
551
  this.toolRegistry.register(backgroundCommandTool);
552
+ this.toolRegistry.register(subAgentTool);
553
+ // Initialize SubAgentManager with tool registry
554
+ SubAgentManager.initialize(this.toolRegistry);
555
+ SubAgentManager.setOnSubAgentCountChange((count) => {
556
+ if (this.onSubAgentCountChange) {
557
+ this.onSubAgentCountChange(count);
558
+ }
559
+ });
374
560
  // Load configuration
375
561
  const config = this.configManager.load();
376
562
  // Enable backend sync if authenticated
@@ -562,6 +748,35 @@ Press Enter to continue...
562
748
  this.notifySessionQuotaStatus();
563
749
  return;
564
750
  }
751
+ // Check context window limit before accepting new messages
752
+ // Get current model's context window
753
+ const currentModel = this.configManager.get('modelName') || 'gemini-2.5-flash';
754
+ const { getModelContextWindowSync } = await import('./config/models.js');
755
+ const maxTokens = getModelContextWindowSync(currentModel);
756
+ // Calculate current token usage percentage
757
+ // We need to estimate tokens for the new message too
758
+ const newMessageChars = message.length;
759
+ const estimatedNewMessageTokens = Math.ceil(newMessageChars / 4);
760
+ // Get current token count from state (updated by updateTokenCount)
761
+ const currentTokens = this.getCurrentTokenCount();
762
+ const projectedTokens = currentTokens + estimatedNewMessageTokens;
763
+ const usagePercent = (projectedTokens / maxTokens) * 100;
764
+ // Block new messages if context is ≥80% full
765
+ if (usagePercent >= 80) {
766
+ // Set context limit reached state
767
+ if (!this.contextLimitReached) {
768
+ this.contextLimitReached = true;
769
+ if (this.onContextLimitReached) {
770
+ this.onContextLimitReached(true);
771
+ }
772
+ }
773
+ const contextLimitMessage = `\n⚠️ Context limit reached (${usagePercent.toFixed(1)}% of ${maxTokens.toLocaleString()} tokens used).\n\nYour conversation has grown too large for the AI to process effectively.\n\nPlease start a new chat to continue:\n • Use /new to start a fresh conversation\n • Or use /chat to switch to a different chat\n\nYour current conversation has been saved and you can return to it later.\n\nYou can still use:\n • Slash commands (e.g., /help, /new, /chat)\n • Terminal commands (in Command mode)`;
774
+ if (this.onDirectMessageCallback) {
775
+ this.onDirectMessageCallback(contextLimitMessage);
776
+ }
777
+ quickLog(`[${new Date().toISOString()}] [handleMessage] Context limit reached: ${usagePercent.toFixed(1)}% (${projectedTokens}/${maxTokens} tokens)\n`);
778
+ return;
779
+ }
565
780
  // Cancel any active request when a new message comes in
566
781
  // This enables "interrupt and replace" - new message takes priority
567
782
  if (this.currentAbortController) {
@@ -613,6 +828,12 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
613
828
  contextManager: this.contextManager,
614
829
  cliAdapter: this, // Pass CLI adapter reference for interactive process management
615
830
  requireApproval: async (message, risky, preview, operationType, operationDetails) => {
831
+ // Special bypass for shell input to running processes:
832
+ // If the AI is sending input to an existing shell (via shell_input), we bypass the separate approval step.
833
+ // The user already implicitly approved the interaction by running the command in agent control mode.
834
+ if (operationType === 'execute_command' && operationDetails?.shell_input) {
835
+ return true;
836
+ }
616
837
  if (this.onToolApprovalRequest) {
617
838
  return await this.onToolApprovalRequest({ message, risky, preview, operationType, operationDetails });
618
839
  }
@@ -646,11 +867,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
646
867
  const mode = this.getMode();
647
868
  let finalAssistantMessage = '';
648
869
  const MAX_TURNS = 500; // Allow up to 500 turns for complex tasks
649
- const MAX_TOOL_CALLS_PER_TURN = 5; // Limit tool calls per turn to prevent overthinking
650
- const MAX_NARRATION_ATTEMPTS = 3; // Maximum times we'll prompt AI to stop narrating
651
870
  let turnCount = 0;
652
- let narrationAttempts = 0; // Track how many times AI narrated without executing
653
- let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
654
871
  let thoughtStartTime = null; // Track when thinking started
655
872
  let thoughtContent = ''; // Accumulate thought content during streaming
656
873
  let currentTurnThinking = ''; // Persist thinking for the current turn to attach to assistant message
@@ -736,13 +953,11 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
736
953
  quickLog(`[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
737
954
  }
738
955
  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
- }
956
+ // Update token count using accurate API
957
+ // This will use backend's Vertex AI countTokens for precision
958
+ this.updateTokenCount().catch(err => {
959
+ quickLog(`[${new Date().toISOString()}] [CLI] Failed to update token count: ${err}\n`);
960
+ });
746
961
  // Stream AI response from backend
747
962
  // Backend will inject system prompt automatically with environment context
748
963
  for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
@@ -817,6 +1032,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
817
1032
  // Handle tool call chunks
818
1033
  if (chunk.type === 'tool_call') {
819
1034
  const toolCall = chunk.toolCall;
1035
+ // Kiro/Claude compatibility: Parse string arguments early so they are objects throughout the pipeline
1036
+ // This ensures logging, UI updates, and tool execution all see the parsed object
1037
+ if (toolCall.arguments && typeof toolCall.arguments === 'string') {
1038
+ try {
1039
+ toolCall.arguments = JSON.parse(toolCall.arguments);
1040
+ }
1041
+ catch (e) {
1042
+ // Ignore parsing error, will be handled by downstream logic
1043
+ }
1044
+ }
820
1045
  // Debug: Log every tool_call chunk received
821
1046
  try {
822
1047
  quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED (REAL-TIME): ${toolCall?.name || 'unknown'}\n`);
@@ -856,9 +1081,11 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
856
1081
  // REAL-TIME EXECUTION: Execute tool immediately during streaming
857
1082
  // This reduces latency by not waiting for the entire stream to finish
858
1083
  try {
859
- // Extract and display reason_text if present (skip for task_complete)
1084
+ // Extract and display reason_text if present (skip for task_complete and shell_input)
860
1085
  const reasonText = toolCall.arguments.reason_text;
861
- if (reasonText && this.onResponseStreamCallback) {
1086
+ // Don't show reason text for shell inputs (hidden from history per user request)
1087
+ const isShellInput = toolCall.name === 'execute_command' && toolCall.arguments.shell_input;
1088
+ if (reasonText && !isShellInput && this.onResponseStreamCallback) {
862
1089
  this.onResponseStreamCallback(reasonText + '\n\n');
863
1090
  }
864
1091
  // Show 'executing' status immediately
@@ -866,6 +1093,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
866
1093
  // Log tool execution start
867
1094
  conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
868
1095
  // Execute the tool (it will request approval if needed via requireApproval callback)
1096
+ // SPECIAL: Intercept sub_agent spawn to enforce approval
1097
+ if (toolCall.name === 'sub_agent' && toolCall.arguments?.action === 'spawn') {
1098
+ const approved = await context.requireApproval(`Spawn Sub-Agent`, true, // risky
1099
+ undefined, 'execute_command', { command: `spawn sub-agent` });
1100
+ if (!approved) {
1101
+ throw new Error('User rejected sub-agent spawn request');
1102
+ }
1103
+ }
869
1104
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
870
1105
  if (result.success) {
871
1106
  conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
@@ -947,8 +1182,6 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
947
1182
  assistantMessageLength: assistantMessage.length,
948
1183
  hasToolCalls: toolCalls.length > 0,
949
1184
  willContinue: toolCalls.length > 0,
950
- narrationAttempts,
951
- completionAttempts,
952
1185
  });
953
1186
  // If there are tool calls, execute them
954
1187
  if (toolCalls.length > 0) {
@@ -961,11 +1194,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
961
1194
  // Suppress text output - AI should only use reason_text
962
1195
  assistantMessage = ''; // Clear ALL text output - AI should only use reason_text
963
1196
  }
964
- // Limit tool calls per turn to prevent overthinking
965
- if (toolCalls.length > MAX_TOOL_CALLS_PER_TURN) {
966
- // Silently limit tool calls
967
- toolCalls = toolCalls.slice(0, MAX_TOOL_CALLS_PER_TURN);
968
- }
1197
+ // Tool call limit removed - let AI use as many tools as needed per turn
969
1198
  const toolResults = [...inStreamToolResults]; // Start with in-stream results
970
1199
  const handledToolCallIds = new Set(); // Only for special tools (create_plan, mark_task_complete)
971
1200
  let userCancelledOperation = false;
@@ -989,25 +1218,24 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
989
1218
  try {
990
1219
  // Check if this is task_complete FIRST (before displaying anything)
991
1220
  if (toolCall.name === 'task_complete') {
992
- taskCompleted = true;
993
- taskCompleteSummary = toolCall.arguments.summary || '';
994
- conversationLogger.logTaskComplete(taskCompleteSummary);
995
- // If task_complete has a summary, use that as the final message
996
- // If no summary provided, PRESERVE the already-streamed assistantMessage text
997
- if (taskCompleteSummary) {
998
- // AI provided a summary - suppress any other text and use the summary
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
- }
1005
- // Stream the summary to UI so it's visible
1006
- if (this.onResponseStreamCallback) {
1007
- this.onResponseStreamCallback(taskCompleteSummary);
1008
- }
1221
+ // SUBAGENT BLOCKING: Check if any sub-agents are still running
1222
+ const runningSubAgents = SubAgentManager.getRunningSubAgents();
1223
+ if (runningSubAgents.length > 0) {
1224
+ // Block task_complete and provide feedback
1225
+ const agentIds = runningSubAgents.map(a => a.id).join(', ');
1226
+ toolResults.push({
1227
+ tool_call_id: toolCall.id,
1228
+ name: toolCall.name,
1229
+ result: `Cannot complete task: ${runningSubAgents.length} sub-agent(s) still running. IDs: ${agentIds}. Check their status periodically with sub_agent(action="status", agent_id="...") and wait for completion before calling task_complete.`,
1230
+ });
1231
+ handledToolCallIds.add(toolCall.id);
1232
+ continue; // Skip task_complete execution, keep loop running
1009
1233
  }
1010
- // If no summary, keep assistantMessage as-is (it was already streamed to UI)
1234
+ taskCompleted = true;
1235
+ conversationLogger.logTaskComplete('');
1236
+ // task_complete no longer has a summary parameter
1237
+ // The AI streams all response text BEFORE calling task_complete()
1238
+ // So we just preserve whatever assistantMessage was already streamed
1011
1239
  // Execute the tool for proper result handling
1012
1240
  await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
1013
1241
  // Clear the plan when task is complete
@@ -1240,7 +1468,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1240
1468
  tool_call_id: toolCall.id,
1241
1469
  name: toolCall.name,
1242
1470
  result: completion.allComplete
1243
- ? 'All tasks completed! Call task_complete with a summary of what was accomplished.'
1471
+ ? 'All tasks completed! Output your summary of what was accomplished, then call task_complete().'
1244
1472
  : completion.nextSubtask
1245
1473
  ? `Subtask ${completion.taskNumber} completed. Next subtask: ${completion.nextSubtask}`
1246
1474
  : completion.nextTask
@@ -1250,7 +1478,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1250
1478
  // If all tasks are complete, prompt AI to call task_complete
1251
1479
  if (completion.allComplete) {
1252
1480
  toolResults[toolResults.length - 1].result =
1253
- 'All tasks in the plan are now completed! Call task_complete with a summary of what was accomplished.';
1481
+ 'All tasks in the plan are now completed! Output your summary of what was accomplished, then call task_complete().';
1254
1482
  }
1255
1483
  }
1256
1484
  catch (parseError) {
@@ -1314,12 +1542,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1314
1542
  remoteContext = `docker:${metadata.containerId?.substring(0, 12) || 'container'}`;
1315
1543
  }
1316
1544
  }
1317
- // Notify UI: tool is executing (send before execution starts)
1545
+ // Notify UI: tool executing
1318
1546
  if (this.onToolExecutionUpdate) {
1319
- // Add cwd to arguments for execute_command tool, and remoteContext for all tools
1320
- const toolArgs = toolCall.name === 'execute_command'
1321
- ? { ...toolCall.arguments, cwd: effectiveCwd, remoteContext }
1322
- : { ...toolCall.arguments, remoteContext };
1547
+ let toolArgs = { ...toolCall.arguments, remoteContext };
1548
+ // Special handling for execute_command
1549
+ if (toolCall.name === 'execute_command') {
1550
+ // Add effective CWD
1551
+ toolArgs.cwd = effectiveCwd;
1552
+ }
1323
1553
  this.onToolExecutionUpdate({
1324
1554
  toolName: toolCall.name,
1325
1555
  status: 'executing',
@@ -1329,6 +1559,31 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1329
1559
  // Log tool execution start
1330
1560
  conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
1331
1561
  // Execute the tool (it will request approval if needed)
1562
+ // SPECIAL: Intercept sub_agent spawn to enforce approval
1563
+ if (toolCall.name === 'sub_agent' && toolCall.arguments?.action === 'spawn') {
1564
+ const approved = await context.requireApproval(`Spawn Sub-Agent`, true, // risky
1565
+ undefined, 'execute_command', { command: `spawn sub-agent` });
1566
+ if (!approved) {
1567
+ // User rejected - log result as error and skip execution
1568
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, 'User rejected');
1569
+ // Notify UI: tool failed
1570
+ if (this.onToolExecutionUpdate) {
1571
+ this.onToolExecutionUpdate({
1572
+ toolName: toolCall.name,
1573
+ status: 'error',
1574
+ error: 'User rejected',
1575
+ arguments: toolCall.arguments
1576
+ });
1577
+ }
1578
+ toolResults.push({
1579
+ tool_call_id: toolCall.id,
1580
+ name: toolCall.name,
1581
+ result: 'User rejected sub-agent spawn request',
1582
+ error: 'User rejected'
1583
+ });
1584
+ continue;
1585
+ }
1586
+ }
1332
1587
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
1333
1588
  if (result.success) {
1334
1589
  // Log successful tool result
@@ -1442,6 +1697,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1442
1697
  }
1443
1698
  }
1444
1699
  }
1700
+ // STOP AGENT LOOP if shell_input was provided
1701
+ // Interactive shell input implies handing control back to the shell/user
1702
+ const hasShellInput = toolCalls.some(tc => tc.name === 'execute_command' && tc.arguments && tc.arguments.shell_input);
1703
+ if (hasShellInput) {
1704
+ try {
1705
+ quickLog(`[${new Date().toISOString()}] [CLI] Input sent to shell. Stopping agent loop to await output.\n`);
1706
+ }
1707
+ catch (e) { }
1708
+ taskCompleted = true;
1709
+ }
1445
1710
  // If task_complete was called, stop the agentic loop immediately
1446
1711
  if (taskCompleted) {
1447
1712
  // Set the final message: use summary if provided, otherwise use the streamed assistantMessage
@@ -1556,8 +1821,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1556
1821
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
1557
1822
  '**This is not allowed.** You must either:\n' +
1558
1823
  '1. Execute a tool call if more work is needed, OR\n' +
1559
- '2. Call task_complete() with a summary of what you accomplished\n\n' +
1560
- '**If you have completed the task**, call task_complete() NOW with a comprehensive summary.\n' +
1824
+ '2. Output your response text, then call task_complete()\n\n' +
1825
+ '**If you have completed the task**, output your summary now, then call task_complete().\n' +
1561
1826
  '**If more work is needed**, execute the next tool call immediately.';
1562
1827
  conversationLogger.logSystemPrompt('silent_stop_prompt', silentStopPrompt);
1563
1828
  this.conversationHistory.push({
@@ -1565,113 +1830,17 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1565
1830
  content: silentStopPrompt,
1566
1831
  });
1567
1832
  }
1568
- // Case 2: Text-only response (Narration or Summary)
1833
+ // Case 2: Text-only response - accept it immediately as final
1569
1834
  else {
1570
- const isNarration = /\b(I will|I'll|Let me|Let's|I need to|I'm going to|I should|I can)\b/i.test(assistantMessage);
1571
- if (isNarration) {
1572
- narrationAttempts++;
1573
- conversationLogger.logNarrationDetection('narration', {
1574
- turn: turnCount,
1575
- narrationAttempts,
1576
- maxAttempts: MAX_NARRATION_ATTEMPTS,
1577
- messagePreview: assistantMessage.substring(0, 200),
1578
- });
1579
- // If AI keeps narrating without executing, force completion immediately
1580
- if (narrationAttempts >= MAX_NARRATION_ATTEMPTS) {
1581
- // Force task completion with error message
1582
- conversationLogger.logNarrationDetection('narration', {
1583
- action: 'forced_completion',
1584
- reason: 'max_narration_attempts_reached',
1585
- });
1586
- finalAssistantMessage = '⚠️ **Task Incomplete**: The AI repeatedly described actions without executing them.\n\n' +
1587
- '**What happened**: The AI entered a narration loop, describing what it wanted to do instead of using tool calls.\n\n' +
1588
- '**Suggestions**:\n' +
1589
- '1. Try rephrasing your request more specifically\n' +
1590
- '2. Break the task into smaller, concrete steps\n' +
1591
- '3. Provide explicit file paths if known\n' +
1592
- '4. Check if the model supports tool calling properly\n\n' +
1593
- '**Last message**: ' + assistantMessage;
1594
- break;
1595
- }
1596
- // First narration attempt - give a strong warning with specific guidance
1597
- if (narrationAttempts === 1) {
1598
- const completionPrompt = '🛑 **CRITICAL ERROR**: You output text without using tools.\n\n' +
1599
- '**COMMUNICATION RULE VIOLATION**: You can ONLY communicate through:\n' +
1600
- '1. `reason_text` parameter in tool calls\n' +
1601
- '2. `summary` parameter in task_complete tool\n\n' +
1602
- '**Your text output was HIDDEN from the user.**\n\n' +
1603
- '**MANDATORY CORRECTION**:\n' +
1604
- '- If you need to DO something: Call the tool with `reason_text`\n' +
1605
- '- If you are DONE: Call `task_complete(summary="your message")`\n' +
1606
- '- NEVER output plain text - it will be hidden\n\n' +
1607
- '**Example for greeting**:\n' +
1608
- '```\n' +
1609
- '<thought>User said hello, I should greet back</thought>\n' +
1610
- '(Call task_complete with summary="Hello! How can I help you today?")\n' +
1611
- '```\n\n' +
1612
- '**Your NEXT response MUST use tools.**';
1613
- this.conversationHistory.push({
1614
- role: 'user',
1615
- content: completionPrompt,
1616
- });
1617
- }
1618
- else {
1619
- // Second narration attempt - final warning before forced completion
1620
- const completionPrompt = '🚨 **FINAL WARNING** (Attempt ' + narrationAttempts + '/' + MAX_NARRATION_ATTEMPTS + '): You are STILL narrating instead of executing.\n\n' +
1621
- '**This is your LAST chance**:\n' +
1622
- '1. Execute a tool call NOW, or\n' +
1623
- '2. Call task_complete() to end\n\n' +
1624
- 'If you output narration text again, the task will be forcibly terminated.';
1625
- this.conversationHistory.push({
1626
- role: 'user',
1627
- content: completionPrompt,
1628
- });
1629
- }
1630
- }
1631
- else {
1632
- // AI output a response without narration - it should finish
1633
- // Reset narration counter since this is a valid response
1634
- narrationAttempts = 0;
1635
- // Check if the message looks like a final answer/summary
1636
- // If it has substantial length, assume it's a summary attempt
1637
- const isFinalAnswer = assistantMessage.length > 20;
1638
- if (isFinalAnswer) {
1639
- completionAttempts++;
1640
- conversationLogger.logNarrationDetection('final_answer', {
1641
- turn: turnCount,
1642
- completionAttempts,
1643
- messagePreview: assistantMessage.substring(0, 200),
1644
- });
1645
- // If AI keeps providing text summaries without calling task_complete, accept the text and finish
1646
- // This prevents the infinite loop where the AI keeps summarizing in response to our prompt
1647
- if (completionAttempts > 1) {
1648
- conversationLogger.logNarrationDetection('final_answer', {
1649
- action: 'accepting_text_as_final',
1650
- reason: 'multiple_completion_attempts',
1651
- });
1652
- finalAssistantMessage = assistantMessage;
1653
- break;
1654
- }
1655
- // This looks like a final answer - prompt to call task_complete
1656
- const completionPrompt = '✅ **Possible Completion Detected**: You provided a text response but did not call `task_complete`.\n\n' +
1657
- '**To finish the conversation, you MUST call the `task_complete` tool.**\n\n' +
1658
- 'Please call `task_complete` now with your summary as the argument.';
1659
- this.conversationHistory.push({
1660
- role: 'user',
1661
- content: completionPrompt,
1662
- });
1663
- }
1664
- else {
1665
- // Short message without clear intent - ask for clarification or completion
1666
- const completionPrompt = 'Your response is unclear. Either:\n' +
1667
- '1. Execute the next tool call if more work is needed, or\n' +
1668
- '2. Call task_complete() if the task is done';
1669
- this.conversationHistory.push({
1670
- role: 'user',
1671
- content: completionPrompt,
1672
- });
1673
- }
1674
- }
1835
+ // Log that we're accepting this as a final answer
1836
+ conversationLogger.logNarrationDetection('final_answer', {
1837
+ turn: turnCount,
1838
+ messagePreview: assistantMessage.substring(0, 200),
1839
+ action: 'accepting_immediately',
1840
+ });
1841
+ // Accept the text as the final message and break
1842
+ finalAssistantMessage = assistantMessage;
1843
+ break;
1675
1844
  }
1676
1845
  // Rebuild messages array with updated history
1677
1846
  // Backend will inject system prompt
@@ -1687,8 +1856,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1687
1856
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
1688
1857
  '**This is not allowed.** You must either:\n' +
1689
1858
  '1. Execute a tool call if more work is needed, OR\n' +
1690
- '2. Call task_complete() with a summary of what you accomplished\n\n' +
1691
- '**If you have completed the task**, call task_complete() NOW with a comprehensive summary.\n' +
1859
+ '2. Output your response text, then call task_complete()\n\n' +
1860
+ '**If you have completed the task**, output your summary now, then call task_complete().\n' +
1692
1861
  '**If more work is needed**, execute the next tool call immediately.';
1693
1862
  this.conversationHistory.push({
1694
1863
  role: 'user',
@@ -2092,7 +2261,7 @@ Start by listing the directory structure to understand what you're working with.
2092
2261
  '• Work silently without narrating actions\n' +
2093
2262
  '• Use Touch-First safety (never guess file paths)\n' +
2094
2263
  '• Apply surgical precision to file edits\n' +
2095
- '• Call task_complete when done with comprehensive summary\n' +
2264
+ '• Output summary text, then call task_complete() when done\n' +
2096
2265
  '• Inject intelligent error recovery hints\n\n' +
2097
2266
  'This is the industry-standard autonomous agent mode.'
2098
2267
  : '⚠️ Autonomous Mode disabled\n\n' +
@@ -3034,6 +3203,19 @@ Start by listing the directory structure to understand what you're working with.
3034
3203
  this.onCwdChange(chat.cwd);
3035
3204
  }
3036
3205
  }
3206
+ // Reset context limit state when loading a chat
3207
+ // We'll recalculate it based on the loaded conversation
3208
+ if (this.contextLimitReached) {
3209
+ this.contextLimitReached = false;
3210
+ if (this.onContextLimitReached) {
3211
+ this.onContextLimitReached(false);
3212
+ }
3213
+ }
3214
+ // Update token count to reflect loaded conversation
3215
+ // This will also check if the loaded chat is near the limit
3216
+ this.updateTokenCount().catch(err => {
3217
+ quickLog(`[${new Date().toISOString()}] [loadChatFromPicker] Failed to update token count: ${err}\n`);
3218
+ });
3037
3219
  return true;
3038
3220
  }
3039
3221
  /**
@@ -3202,6 +3384,17 @@ Start by listing the directory structure to understand what you're working with.
3202
3384
  this.uiMessageHistory = [];
3203
3385
  this.localCwdBeforeRemote = null;
3204
3386
  this.lastConnectionCommand = null;
3387
+ // Reset context limit state
3388
+ if (this.contextLimitReached) {
3389
+ this.contextLimitReached = false;
3390
+ if (this.onContextLimitReached) {
3391
+ this.onContextLimitReached(false);
3392
+ }
3393
+ }
3394
+ // Update token count to reflect empty conversation
3395
+ this.updateTokenCount().catch(err => {
3396
+ quickLog(`[${new Date().toISOString()}] [startNewChat] Failed to update token count: ${err}\n`);
3397
+ });
3205
3398
  }
3206
3399
  /**
3207
3400
  * Update UI message history (called from App.tsx via callback)