centaurus-cli 2.9.0 → 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 (148) hide show
  1. package/dist/cli-adapter.d.ts +78 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +566 -165
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/build-config.d.ts +1 -1
  6. package/dist/config/build-config.js +1 -1
  7. package/dist/config/mcp-config-manager.d.ts +21 -0
  8. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  9. package/dist/config/mcp-config-manager.js +184 -1
  10. package/dist/config/mcp-config-manager.js.map +1 -1
  11. package/dist/config/models.d.ts +1 -0
  12. package/dist/config/models.d.ts.map +1 -1
  13. package/dist/config/models.js +7 -2
  14. package/dist/config/models.js.map +1 -1
  15. package/dist/config/slash-commands.d.ts.map +1 -1
  16. package/dist/config/slash-commands.js +5 -3
  17. package/dist/config/slash-commands.js.map +1 -1
  18. package/dist/index.js +66 -11
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/mcp-command-handler.d.ts +34 -3
  21. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  22. package/dist/mcp/mcp-command-handler.js +171 -83
  23. package/dist/mcp/mcp-command-handler.js.map +1 -1
  24. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  25. package/dist/mcp/mcp-server-manager.js +9 -23
  26. package/dist/mcp/mcp-server-manager.js.map +1 -1
  27. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  28. package/dist/mcp/mcp-tool-wrapper.js +42 -5
  29. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  30. package/dist/services/ai-service-client.d.ts +6 -1
  31. package/dist/services/ai-service-client.d.ts.map +1 -1
  32. package/dist/services/ai-service-client.js +6 -6
  33. package/dist/services/ai-service-client.js.map +1 -1
  34. package/dist/services/api-client.d.ts +20 -0
  35. package/dist/services/api-client.d.ts.map +1 -1
  36. package/dist/services/api-client.js +35 -0
  37. package/dist/services/api-client.js.map +1 -1
  38. package/dist/services/input-detection-agent.d.ts +40 -0
  39. package/dist/services/input-detection-agent.d.ts.map +1 -0
  40. package/dist/services/input-detection-agent.js +213 -0
  41. package/dist/services/input-detection-agent.js.map +1 -0
  42. package/dist/services/input-requirement-detector.d.ts +28 -0
  43. package/dist/services/input-requirement-detector.d.ts.map +1 -0
  44. package/dist/services/input-requirement-detector.js +203 -0
  45. package/dist/services/input-requirement-detector.js.map +1 -0
  46. package/dist/services/monitored-shell-manager.d.ts +120 -0
  47. package/dist/services/monitored-shell-manager.d.ts.map +1 -0
  48. package/dist/services/monitored-shell-manager.js +239 -0
  49. package/dist/services/monitored-shell-manager.js.map +1 -0
  50. package/dist/services/session-quota-manager.d.ts +101 -0
  51. package/dist/services/session-quota-manager.d.ts.map +1 -0
  52. package/dist/services/session-quota-manager.js +242 -0
  53. package/dist/services/session-quota-manager.js.map +1 -0
  54. package/dist/services/shell-input-agent.d.ts +89 -0
  55. package/dist/services/shell-input-agent.d.ts.map +1 -0
  56. package/dist/services/shell-input-agent.js +361 -0
  57. package/dist/services/shell-input-agent.js.map +1 -0
  58. package/dist/services/sub-agent-manager.d.ts +139 -0
  59. package/dist/services/sub-agent-manager.d.ts.map +1 -0
  60. package/dist/services/sub-agent-manager.js +517 -0
  61. package/dist/services/sub-agent-manager.js.map +1 -0
  62. package/dist/tools/background-command.d.ts.map +1 -1
  63. package/dist/tools/background-command.js +33 -13
  64. package/dist/tools/background-command.js.map +1 -1
  65. package/dist/tools/command.d.ts.map +1 -1
  66. package/dist/tools/command.js +78 -4
  67. package/dist/tools/command.js.map +1 -1
  68. package/dist/tools/file-ops.d.ts.map +1 -1
  69. package/dist/tools/file-ops.js +33 -19
  70. package/dist/tools/file-ops.js.map +1 -1
  71. package/dist/tools/get-diff.d.ts.map +1 -1
  72. package/dist/tools/get-diff.js +5 -2
  73. package/dist/tools/get-diff.js.map +1 -1
  74. package/dist/tools/grep-search.d.ts.map +1 -1
  75. package/dist/tools/grep-search.js +41 -15
  76. package/dist/tools/grep-search.js.map +1 -1
  77. package/dist/tools/plan-mode.js +3 -3
  78. package/dist/tools/plan-mode.js.map +1 -1
  79. package/dist/tools/registry.js +1 -1
  80. package/dist/tools/registry.js.map +1 -1
  81. package/dist/tools/sub-agent.d.ts +9 -0
  82. package/dist/tools/sub-agent.d.ts.map +1 -0
  83. package/dist/tools/sub-agent.js +232 -0
  84. package/dist/tools/sub-agent.js.map +1 -0
  85. package/dist/tools/task-complete.d.ts.map +1 -1
  86. package/dist/tools/task-complete.js +14 -32
  87. package/dist/tools/task-complete.js.map +1 -1
  88. package/dist/ui/components/App.d.ts +45 -0
  89. package/dist/ui/components/App.d.ts.map +1 -1
  90. package/dist/ui/components/App.js +598 -95
  91. package/dist/ui/components/App.js.map +1 -1
  92. package/dist/ui/components/CircularSelectInput.d.ts +24 -0
  93. package/dist/ui/components/CircularSelectInput.d.ts.map +1 -0
  94. package/dist/ui/components/CircularSelectInput.js +71 -0
  95. package/dist/ui/components/CircularSelectInput.js.map +1 -0
  96. package/dist/ui/components/ErrorBoundary.d.ts +3 -2
  97. package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
  98. package/dist/ui/components/ErrorBoundary.js +29 -1
  99. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  100. package/dist/ui/components/InputBox.d.ts +4 -0
  101. package/dist/ui/components/InputBox.d.ts.map +1 -1
  102. package/dist/ui/components/InputBox.js +40 -2
  103. package/dist/ui/components/InputBox.js.map +1 -1
  104. package/dist/ui/components/InteractiveShell.d.ts +6 -0
  105. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  106. package/dist/ui/components/InteractiveShell.js +57 -6
  107. package/dist/ui/components/InteractiveShell.js.map +1 -1
  108. package/dist/ui/components/MCPAddScreen.d.ts +13 -0
  109. package/dist/ui/components/MCPAddScreen.d.ts.map +1 -0
  110. package/dist/ui/components/MCPAddScreen.js +54 -0
  111. package/dist/ui/components/MCPAddScreen.js.map +1 -0
  112. package/dist/ui/components/MCPListScreen.d.ts +17 -0
  113. package/dist/ui/components/MCPListScreen.d.ts.map +1 -0
  114. package/dist/ui/components/MCPListScreen.js +50 -0
  115. package/dist/ui/components/MCPListScreen.js.map +1 -0
  116. package/dist/ui/components/MCPServerListScreen.d.ts +16 -0
  117. package/dist/ui/components/MCPServerListScreen.d.ts.map +1 -0
  118. package/dist/ui/components/MCPServerListScreen.js +59 -0
  119. package/dist/ui/components/MCPServerListScreen.js.map +1 -0
  120. package/dist/ui/components/MonitorModeAIPanel.d.ts +23 -0
  121. package/dist/ui/components/MonitorModeAIPanel.d.ts.map +1 -0
  122. package/dist/ui/components/MonitorModeAIPanel.js +69 -0
  123. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -0
  124. package/dist/ui/components/MultiLineInput.d.ts +13 -0
  125. package/dist/ui/components/MultiLineInput.d.ts.map +1 -0
  126. package/dist/ui/components/MultiLineInput.js +223 -0
  127. package/dist/ui/components/MultiLineInput.js.map +1 -0
  128. package/dist/ui/components/StatusBar.d.ts +2 -0
  129. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  130. package/dist/ui/components/StatusBar.js +33 -2
  131. package/dist/ui/components/StatusBar.js.map +1 -1
  132. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  133. package/dist/ui/components/ToolExecutionMessage.js +271 -12
  134. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  135. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  136. package/dist/ui/components/VersionUpdatePrompt.js +3 -2
  137. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  138. package/dist/utils/editor-utils.d.ts +3 -3
  139. package/dist/utils/editor-utils.d.ts.map +1 -1
  140. package/dist/utils/editor-utils.js +15 -12
  141. package/dist/utils/editor-utils.js.map +1 -1
  142. package/dist/utils/input-classifier.d.ts.map +1 -1
  143. package/dist/utils/input-classifier.js +1 -0
  144. package/dist/utils/input-classifier.js.map +1 -1
  145. package/dist/utils/terminal-output.d.ts.map +1 -1
  146. package/dist/utils/terminal-output.js +198 -171
  147. package/dist/utils/terminal-output.js.map +1 -1
  148. 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';
@@ -38,6 +41,7 @@ import { conversationLogger, quickLog } from './utils/conversation-logger.js';
38
41
  import { localChatStorage } from './services/local-chat-storage.js';
39
42
  import { logWarning } from './utils/logger.js';
40
43
  import { BackgroundTaskManager } from './services/background-task-manager.js';
44
+ import { sessionQuotaManager } from './services/session-quota-manager.js';
41
45
  export class CentaurusCLI {
42
46
  configManager;
43
47
  toolRegistry;
@@ -47,10 +51,12 @@ export class CentaurusCLI {
47
51
  pendingPlanRequest = null; // Stores original user request during planning phase
48
52
  commandMode = false;
49
53
  backgroundMode = false; // Background shell mode for running commands in background
54
+ shellIdCounter = 1;
50
55
  previousMode = 'execution';
51
56
  onResponseCallback;
52
57
  onDirectMessageCallback; // For slash commands - adds directly to history
53
58
  onResponseStreamCallback;
59
+ onClearStreamedResponse; // Clear streamed text when task_complete has summary
54
60
  onThoughtStreamCallback;
55
61
  onThoughtCompleteCallback;
56
62
  onCommandModeChange;
@@ -92,6 +98,18 @@ export class CentaurusCLI {
92
98
  onShowBackgroundTaskCancelPickerCallback;
93
99
  onBackgroundTaskViewCallback;
94
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
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
95
113
  constructor() {
96
114
  this.configManager = new ConfigManager();
97
115
  this.toolRegistry = new ToolRegistry();
@@ -112,6 +130,11 @@ export class CentaurusCLI {
112
130
  });
113
131
  // Initialize MCP
114
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
+ });
115
138
  }
116
139
  setOnResponseCallback(callback) {
117
140
  this.onResponseCallback = callback;
@@ -122,6 +145,9 @@ export class CentaurusCLI {
122
145
  setOnResponseStreamCallback(callback) {
123
146
  this.onResponseStreamCallback = callback;
124
147
  }
148
+ setOnClearStreamedResponse(callback) {
149
+ this.onClearStreamedResponse = callback;
150
+ }
125
151
  setOnThoughtStreamCallback(callback) {
126
152
  this.onThoughtStreamCallback = callback;
127
153
  }
@@ -180,11 +206,173 @@ export class CentaurusCLI {
180
206
  setOnTokenCountUpdate(callback) {
181
207
  this.onTokenCountUpdate = callback;
182
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
+ }
287
+ setOnSessionQuotaUpdate(callback) {
288
+ this.onSessionQuotaUpdate = callback;
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
+ }
334
+ /**
335
+ * Notify UI about session quota status
336
+ */
337
+ notifySessionQuotaStatus() {
338
+ if (this.onSessionQuotaUpdate) {
339
+ const remaining = sessionQuotaManager.getRemainingMessages();
340
+ const canSend = sessionQuotaManager.canSendMessage();
341
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
342
+ this.onSessionQuotaUpdate(remaining, canSend, timeRemaining);
343
+ }
344
+ }
183
345
  async initializeMCP() {
184
346
  try {
185
347
  const mcpConfigManager = new MCPConfigManager();
186
348
  const mcpServerManager = new MCPServerManager();
187
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
+ });
188
376
  // Initialize MCP servers and tools
189
377
  await this.mcpCommandHandler.initializeMCP();
190
378
  }
@@ -229,12 +417,22 @@ export class CentaurusCLI {
229
417
  getCommandMode() {
230
418
  return this.commandMode;
231
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
+ }
232
427
  getCurrentWorkingDirectory() {
233
428
  return this.cwd;
234
429
  }
235
430
  getCurrentSubshellContext() {
236
431
  return this.contextManager.getCurrentContext();
237
432
  }
433
+ getCurrentInteractiveProcess() {
434
+ return this.currentInteractiveProcess;
435
+ }
238
436
  /**
239
437
  * Get the current conversation ID for file uploads
240
438
  */
@@ -351,6 +549,14 @@ export class CentaurusCLI {
351
549
  this.toolRegistry.register(readBinaryFileTool);
352
550
  this.toolRegistry.register(createImageTool);
353
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
+ });
354
560
  // Load configuration
355
561
  const config = this.configManager.load();
356
562
  // Enable backend sync if authenticated
@@ -367,6 +573,11 @@ export class CentaurusCLI {
367
573
  const dockerHandler = new DockerHandler();
368
574
  this.contextManager.registerHandler('docker', dockerHandler);
369
575
  this.commandDetector.registerHandler(dockerHandler);
576
+ // Fetch rate limits configuration from backend (async, non-blocking)
577
+ // Uses cached values if backend is unreachable
578
+ sessionQuotaManager.fetchConfigFromBackend().catch(() => {
579
+ // Silently fall back to cached/default config
580
+ });
370
581
  // Note: No need to initialize AI provider - using backend proxy via aiServiceClient
371
582
  }
372
583
  /**
@@ -526,6 +737,46 @@ Press Enter to continue...
526
737
  if (!apiClient.isAuthenticated()) {
527
738
  throw new Error('Authentication required. Please sign in to use AI features.');
528
739
  }
740
+ // Check session quota before making any AI request
741
+ if (!sessionQuotaManager.canSendMessage()) {
742
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
743
+ 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.`;
744
+ if (this.onDirectMessageCallback) {
745
+ this.onDirectMessageCallback(message);
746
+ }
747
+ // Notify UI about quota status
748
+ this.notifySessionQuotaStatus();
749
+ return;
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
+ }
529
780
  // Cancel any active request when a new message comes in
530
781
  // This enables "interrupt and replace" - new message takes priority
531
782
  if (this.currentAbortController) {
@@ -577,6 +828,12 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
577
828
  contextManager: this.contextManager,
578
829
  cliAdapter: this, // Pass CLI adapter reference for interactive process management
579
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
+ }
580
837
  if (this.onToolApprovalRequest) {
581
838
  return await this.onToolApprovalRequest({ message, risky, preview, operationType, operationDetails });
582
839
  }
@@ -610,14 +867,11 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
610
867
  const mode = this.getMode();
611
868
  let finalAssistantMessage = '';
612
869
  const MAX_TURNS = 500; // Allow up to 500 turns for complex tasks
613
- const MAX_TOOL_CALLS_PER_TURN = 5; // Limit tool calls per turn to prevent overthinking
614
- const MAX_NARRATION_ATTEMPTS = 3; // Maximum times we'll prompt AI to stop narrating
615
870
  let turnCount = 0;
616
- let narrationAttempts = 0; // Track how many times AI narrated without executing
617
- let completionAttempts = 0; // Track how many times AI provided text summary without task_complete
618
871
  let thoughtStartTime = null; // Track when thinking started
619
872
  let thoughtContent = ''; // Accumulate thought content during streaming
620
873
  let currentTurnThinking = ''; // Persist thinking for the current turn to attach to assistant message
874
+ let currentTurnThinkingSignature = ''; // Persist thinking signature for Claude extended thinking
621
875
  // ANTI-LOOP: Track duplicate tool calls to detect infinite loops
622
876
  const MAX_DUPLICATE_CALLS = 2; // Max times same operation allowed on same target
623
877
  const fileWriteTracker = new Map(); // Track writes per file
@@ -634,11 +888,31 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
634
888
  // Multi-turn tool execution loop
635
889
  while (turnCount < MAX_TURNS) {
636
890
  turnCount++;
891
+ // Track session quota - each AI call in the agent loop counts as 1 message
892
+ sessionQuotaManager.incrementMessageCount();
893
+ this.notifySessionQuotaStatus();
894
+ // Check if session quota is now exhausted after incrementing
895
+ if (!sessionQuotaManager.canSendMessage() && turnCount > 1) {
896
+ // Quota exhausted mid-loop, stop and inform user
897
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
898
+ 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.`;
899
+ if (this.onResponseCallback) {
900
+ this.onResponseCallback(quotaMessage);
901
+ }
902
+ logWarning('Agent loop stopped due to session quota exhaustion');
903
+ return;
904
+ }
637
905
  // Refresh environment context to capture any CWD changes from previous turns
638
906
  // This is sent to backend which will inject it into the system prompt
639
907
  environmentContext = this.getEnvironmentContext();
640
908
  let assistantMessage = '';
641
909
  let toolCalls = [];
910
+ // REAL-TIME TOOL EXECUTION: Track execution state and results during streaming
911
+ const inStreamToolResults = []; // Results from tools executed during streaming
912
+ const inStreamHandledIds = new Set(); // IDs of tools already executed in-stream
913
+ let toolsExecutedInStream = false; // Flag to indicate tools were executed during stream
914
+ let pendingTextBuffer = ''; // Buffer for text while tool is executing
915
+ let isToolExecuting = false; // Flag to pause text streaming during tool execution
642
916
  // DEBUG: Log message history state before AI call
643
917
  const messageStats = {
644
918
  totalMessages: messages.length,
@@ -679,13 +953,11 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
679
953
  quickLog(`[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
680
954
  }
681
955
  catch (e) { }
682
- // Report actual token count to UI (estimate: 1 token ≈ 4 chars)
683
- // Include system prompt estimate (~14000 chars based on backend logs)
684
- const SYSTEM_PROMPT_ESTIMATE = 14000; // Backend injects ~14K char system prompt
685
- const estimatedTokens = Math.ceil((messageStats.totalCharacters + SYSTEM_PROMPT_ESTIMATE) / 4);
686
- if (this.onTokenCountUpdate) {
687
- this.onTokenCountUpdate(estimatedTokens);
688
- }
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
+ });
689
961
  // Stream AI response from backend
690
962
  // Backend will inject system prompt automatically with environment context
691
963
  for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
@@ -717,6 +989,12 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
717
989
  }
718
990
  continue;
719
991
  }
992
+ // Handle thinking_signature chunks (Claude extended thinking)
993
+ if (chunk.type === 'thinking_signature') {
994
+ // Store the signature for this turn - it must be passed back with thinking content
995
+ currentTurnThinkingSignature = chunk.signature;
996
+ continue;
997
+ }
720
998
  // Handle text chunks
721
999
  if (chunk.type === 'text') {
722
1000
  // If we were thinking and now got text, finalize the thought
@@ -738,18 +1016,35 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
738
1016
  if (filteredContent) {
739
1017
  assistantMessage += filteredContent;
740
1018
  conversationLogger.logAITextChunk(filteredContent);
741
- // Send chunk to UI in real-time
742
- if (this.onResponseStreamCallback) {
743
- this.onResponseStreamCallback(filteredContent);
1019
+ // REAL-TIME TOOL EXECUTION: If a tool is executing, accumulate text
1020
+ // This text will be flushed after the tool completes
1021
+ if (isToolExecuting) {
1022
+ pendingTextBuffer += filteredContent;
1023
+ }
1024
+ else {
1025
+ // Normal streaming - send chunk to UI in real-time
1026
+ if (this.onResponseStreamCallback) {
1027
+ this.onResponseStreamCallback(filteredContent);
1028
+ }
744
1029
  }
745
1030
  }
746
1031
  }
747
1032
  // Handle tool call chunks
748
1033
  if (chunk.type === 'tool_call') {
749
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
+ }
750
1045
  // Debug: Log every tool_call chunk received
751
1046
  try {
752
- quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED: ${toolCall?.name || 'unknown'}, toolCalls.length before push: ${toolCalls.length}\n`);
1047
+ quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED (REAL-TIME): ${toolCall?.name || 'unknown'}\n`);
753
1048
  }
754
1049
  catch (e) { }
755
1050
  conversationLogger.logToolCall(toolCall?.name || 'unknown', toolCall?.id || 'unknown', toolCall?.arguments || {});
@@ -766,18 +1061,98 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
766
1061
  thoughtContent = '';
767
1062
  }
768
1063
  toolCalls.push(chunk.toolCall);
769
- // IMMEDIATELY notify UI that a tool call was received (pending status)
770
- // This shows tool calls in the UI as they stream in, not after the stream ends
771
- if (this.onToolExecutionUpdate && toolCall) {
772
- this.onToolExecutionUpdate({
773
- toolName: toolCall.name,
774
- status: 'pending', // 'pending' means received but not yet executing
775
- arguments: toolCall.arguments
1064
+ // SPECIAL TOOLS: Skip in-stream execution for tools that need post-stream handling
1065
+ // These tools have special logic (setting flags, clearing state, etc.) that must run post-stream
1066
+ const SPECIAL_TOOLS = ['task_complete', 'create_plan', 'mark_task_complete'];
1067
+ if (SPECIAL_TOOLS.includes(toolCall.name)) {
1068
+ // Just notify UI with pending status, execute in post-stream loop
1069
+ if (this.onToolExecutionUpdate) {
1070
+ this.onToolExecutionUpdate({
1071
+ toolName: toolCall.name,
1072
+ status: 'pending',
1073
+ arguments: toolCall.arguments
1074
+ });
1075
+ }
1076
+ continue; // Skip to next chunk, handle this tool in post-stream loop
1077
+ }
1078
+ // Mark that we're executing a tool (text will accumulate)
1079
+ isToolExecuting = true;
1080
+ toolsExecutedInStream = true;
1081
+ // REAL-TIME EXECUTION: Execute tool immediately during streaming
1082
+ // This reduces latency by not waiting for the entire stream to finish
1083
+ try {
1084
+ // Extract and display reason_text if present (skip for task_complete and shell_input)
1085
+ const reasonText = toolCall.arguments.reason_text;
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) {
1089
+ this.onResponseStreamCallback(reasonText + '\n\n');
1090
+ }
1091
+ // Show 'executing' status immediately
1092
+ this.notifyToolStatus(toolCall.name, 'executing', toolCall.arguments);
1093
+ // Log tool execution start
1094
+ conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
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
+ }
1104
+ const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
1105
+ if (result.success) {
1106
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
1107
+ // Notify UI: tool succeeded
1108
+ this.notifyToolStatus(toolCall.name, 'completed', toolCall.arguments, result.result);
1109
+ // Parse and truncate result for AI
1110
+ let parsedResult = result.result;
1111
+ if (typeof result.result === 'string') {
1112
+ try {
1113
+ parsedResult = JSON.parse(result.result);
1114
+ }
1115
+ catch {
1116
+ parsedResult = result.result;
1117
+ }
1118
+ }
1119
+ inStreamToolResults.push({
1120
+ tool_call_id: toolCall.id,
1121
+ name: toolCall.name,
1122
+ result: this.truncateResult(parsedResult),
1123
+ });
1124
+ }
1125
+ else {
1126
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, result.error);
1127
+ // Notify UI: tool failed
1128
+ this.notifyToolStatus(toolCall.name, 'error', toolCall.arguments, undefined, result.error);
1129
+ inStreamToolResults.push({
1130
+ tool_call_id: toolCall.id,
1131
+ name: toolCall.name,
1132
+ result: `Error: ${result.error}`,
1133
+ });
1134
+ }
1135
+ inStreamHandledIds.add(toolCall.id);
1136
+ }
1137
+ catch (error) {
1138
+ conversationLogger.logError(`Tool execution: ${toolCall.name}`, error);
1139
+ this.notifyToolStatus(toolCall.name, 'error', toolCall.arguments, undefined, error.message);
1140
+ inStreamToolResults.push({
1141
+ tool_call_id: toolCall.id,
1142
+ name: toolCall.name,
1143
+ result: `Error: ${error.message}`,
776
1144
  });
1145
+ inStreamHandledIds.add(toolCall.id);
1146
+ }
1147
+ // Tool execution complete - flush pending text
1148
+ isToolExecuting = false;
1149
+ if (pendingTextBuffer && this.onResponseStreamCallback) {
1150
+ this.onResponseStreamCallback(pendingTextBuffer);
1151
+ pendingTextBuffer = '';
777
1152
  }
778
- // Debug: Log after push
1153
+ // Debug: Log after execution
779
1154
  try {
780
- quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL PUSHED: ${toolCall?.name || 'unknown'}, toolCalls.length after push: ${toolCalls.length}\n`);
1155
+ quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL EXECUTED IN-STREAM: ${toolCall?.name || 'unknown'}\n`);
781
1156
  }
782
1157
  catch (e) { }
783
1158
  }
@@ -807,8 +1182,6 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
807
1182
  assistantMessageLength: assistantMessage.length,
808
1183
  hasToolCalls: toolCalls.length > 0,
809
1184
  willContinue: toolCalls.length > 0,
810
- narrationAttempts,
811
- completionAttempts,
812
1185
  });
813
1186
  // If there are tool calls, execute them
814
1187
  if (toolCalls.length > 0) {
@@ -821,40 +1194,48 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
821
1194
  // Suppress text output - AI should only use reason_text
822
1195
  assistantMessage = ''; // Clear ALL text output - AI should only use reason_text
823
1196
  }
824
- // Limit tool calls per turn to prevent overthinking
825
- if (toolCalls.length > MAX_TOOL_CALLS_PER_TURN) {
826
- // Silently limit tool calls
827
- toolCalls = toolCalls.slice(0, MAX_TOOL_CALLS_PER_TURN);
828
- }
829
- const toolResults = [];
830
- const handledToolCallIds = new Set(); // Track tool calls that were handled directly (e.g., create_plan, mark_task_complete)
1197
+ // Tool call limit removed - let AI use as many tools as needed per turn
1198
+ const toolResults = [...inStreamToolResults]; // Start with in-stream results
1199
+ const handledToolCallIds = new Set(); // Only for special tools (create_plan, mark_task_complete)
831
1200
  let userCancelledOperation = false;
832
1201
  let taskCompleted = false;
833
1202
  let taskCompleteSummary = '';
834
1203
  for (let i = 0; i < toolCalls.length; i++) {
1204
+ const toolCall = toolCalls[i];
1205
+ // REAL-TIME EXECUTION: Skip tools that were already executed in-stream
1206
+ if (inStreamHandledIds.has(toolCall.id)) {
1207
+ try {
1208
+ quickLog(`[${new Date().toISOString()}] [CLI] *** SKIPPING TOOL (already executed in-stream): ${toolCall.name}\n`);
1209
+ }
1210
+ catch (e) { }
1211
+ continue;
1212
+ }
835
1213
  // Debug: Log which tool we're about to execute
836
1214
  try {
837
1215
  quickLog(`[${new Date().toISOString()}] [CLI] *** ABOUT TO EXECUTE TOOL [${i + 1}/${toolCalls.length}]: ${toolCalls[i].name}\n`);
838
1216
  }
839
1217
  catch (e) { }
840
- const toolCall = toolCalls[i];
841
1218
  try {
842
1219
  // Check if this is task_complete FIRST (before displaying anything)
843
1220
  if (toolCall.name === 'task_complete') {
844
- taskCompleted = true;
845
- taskCompleteSummary = toolCall.arguments.summary || '';
846
- conversationLogger.logTaskComplete(taskCompleteSummary);
847
- // If task_complete has a summary, use that as the final message
848
- // If no summary provided, PRESERVE the already-streamed assistantMessage text
849
- if (taskCompleteSummary) {
850
- // AI provided a summary - suppress any other text and use the summary
851
- assistantMessage = '';
852
- // Stream the summary to UI so it's visible
853
- if (this.onResponseStreamCallback) {
854
- this.onResponseStreamCallback(taskCompleteSummary);
855
- }
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
856
1233
  }
857
- // 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
858
1239
  // Execute the tool for proper result handling
859
1240
  await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
860
1241
  // Clear the plan when task is complete
@@ -898,6 +1279,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
898
1279
  if (currentTurnThinking) {
899
1280
  planAssistantMsg.thinking = currentTurnThinking;
900
1281
  }
1282
+ if (currentTurnThinkingSignature) {
1283
+ planAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1284
+ }
901
1285
  this.conversationHistory.push(planAssistantMsg);
902
1286
  // Add plan approval response
903
1287
  this.conversationHistory.push({
@@ -942,6 +1326,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
942
1326
  if (currentTurnThinking) {
943
1327
  planAssistantMsg.thinking = currentTurnThinking;
944
1328
  }
1329
+ if (currentTurnThinkingSignature) {
1330
+ planAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1331
+ }
945
1332
  this.conversationHistory.push(planAssistantMsg);
946
1333
  this.conversationHistory.push({
947
1334
  role: 'tool',
@@ -970,6 +1357,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
970
1357
  if (currentTurnThinking) {
971
1358
  errorAssistantMsg.thinking = currentTurnThinking;
972
1359
  }
1360
+ if (currentTurnThinkingSignature) {
1361
+ errorAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1362
+ }
973
1363
  this.conversationHistory.push(errorAssistantMsg);
974
1364
  this.conversationHistory.push({
975
1365
  role: 'tool',
@@ -991,6 +1381,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
991
1381
  if (currentTurnThinking) {
992
1382
  resultAssistantMsg.thinking = currentTurnThinking;
993
1383
  }
1384
+ if (currentTurnThinkingSignature) {
1385
+ resultAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1386
+ }
994
1387
  this.conversationHistory.push(resultAssistantMsg);
995
1388
  this.conversationHistory.push({
996
1389
  role: 'tool',
@@ -1049,6 +1442,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1049
1442
  if (currentTurnThinking) {
1050
1443
  nextPhaseAssistantMsg.thinking = currentTurnThinking;
1051
1444
  }
1445
+ if (currentTurnThinkingSignature) {
1446
+ nextPhaseAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1447
+ }
1052
1448
  this.conversationHistory.push(nextPhaseAssistantMsg);
1053
1449
  this.conversationHistory.push({
1054
1450
  role: 'tool',
@@ -1072,7 +1468,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1072
1468
  tool_call_id: toolCall.id,
1073
1469
  name: toolCall.name,
1074
1470
  result: completion.allComplete
1075
- ? '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().'
1076
1472
  : completion.nextSubtask
1077
1473
  ? `Subtask ${completion.taskNumber} completed. Next subtask: ${completion.nextSubtask}`
1078
1474
  : completion.nextTask
@@ -1082,7 +1478,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1082
1478
  // If all tasks are complete, prompt AI to call task_complete
1083
1479
  if (completion.allComplete) {
1084
1480
  toolResults[toolResults.length - 1].result =
1085
- '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().';
1086
1482
  }
1087
1483
  }
1088
1484
  catch (parseError) {
@@ -1146,12 +1542,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1146
1542
  remoteContext = `docker:${metadata.containerId?.substring(0, 12) || 'container'}`;
1147
1543
  }
1148
1544
  }
1149
- // Notify UI: tool is executing (send before execution starts)
1545
+ // Notify UI: tool executing
1150
1546
  if (this.onToolExecutionUpdate) {
1151
- // Add cwd to arguments for execute_command tool, and remoteContext for all tools
1152
- const toolArgs = toolCall.name === 'execute_command'
1153
- ? { ...toolCall.arguments, cwd: effectiveCwd, remoteContext }
1154
- : { ...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
+ }
1155
1553
  this.onToolExecutionUpdate({
1156
1554
  toolName: toolCall.name,
1157
1555
  status: 'executing',
@@ -1161,6 +1559,31 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1161
1559
  // Log tool execution start
1162
1560
  conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
1163
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
+ }
1164
1587
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
1165
1588
  if (result.success) {
1166
1589
  // Log successful tool result
@@ -1274,6 +1697,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1274
1697
  }
1275
1698
  }
1276
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
+ }
1277
1710
  // If task_complete was called, stop the agentic loop immediately
1278
1711
  if (taskCompleted) {
1279
1712
  // Set the final message: use summary if provided, otherwise use the streamed assistantMessage
@@ -1291,6 +1724,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1291
1724
  if (currentTurnThinking) {
1292
1725
  cancelledAssistantMsg.thinking = currentTurnThinking;
1293
1726
  }
1727
+ if (currentTurnThinkingSignature) {
1728
+ cancelledAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
1729
+ }
1294
1730
  this.conversationHistory.push(cancelledAssistantMsg);
1295
1731
  // Add tool results to history
1296
1732
  for (const toolResult of toolResults) {
@@ -1338,6 +1774,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1338
1774
  if (currentTurnThinking) {
1339
1775
  assistantHistoryMsg.thinking = currentTurnThinking;
1340
1776
  }
1777
+ // Include thinking signature from this turn (required for Claude extended thinking)
1778
+ if (currentTurnThinkingSignature) {
1779
+ assistantHistoryMsg.thinkingSignature = currentTurnThinkingSignature;
1780
+ }
1781
+ // Log signature info for debugging multi-turn flows
1782
+ const geminiSigCount = unhandledToolCalls.filter(tc => !!tc.thoughtSignature).length;
1783
+ try {
1784
+ quickLog(`[${new Date().toISOString()}] [CLI] *** STORING ASSISTANT MSG: ${unhandledToolCalls.length} tool_calls, Gemini signatures: ${geminiSigCount}, Claude thinking: ${!!currentTurnThinking}, Claude sig: ${!!currentTurnThinkingSignature}\n`);
1785
+ }
1786
+ catch (e) { }
1341
1787
  this.conversationHistory.push(assistantHistoryMsg);
1342
1788
  }
1343
1789
  // Add tool results to conversation history as tool messages
@@ -1375,8 +1821,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1375
1821
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
1376
1822
  '**This is not allowed.** You must either:\n' +
1377
1823
  '1. Execute a tool call if more work is needed, OR\n' +
1378
- '2. Call task_complete() with a summary of what you accomplished\n\n' +
1379
- '**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' +
1380
1826
  '**If more work is needed**, execute the next tool call immediately.';
1381
1827
  conversationLogger.logSystemPrompt('silent_stop_prompt', silentStopPrompt);
1382
1828
  this.conversationHistory.push({
@@ -1384,113 +1830,17 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1384
1830
  content: silentStopPrompt,
1385
1831
  });
1386
1832
  }
1387
- // Case 2: Text-only response (Narration or Summary)
1833
+ // Case 2: Text-only response - accept it immediately as final
1388
1834
  else {
1389
- 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);
1390
- if (isNarration) {
1391
- narrationAttempts++;
1392
- conversationLogger.logNarrationDetection('narration', {
1393
- turn: turnCount,
1394
- narrationAttempts,
1395
- maxAttempts: MAX_NARRATION_ATTEMPTS,
1396
- messagePreview: assistantMessage.substring(0, 200),
1397
- });
1398
- // If AI keeps narrating without executing, force completion immediately
1399
- if (narrationAttempts >= MAX_NARRATION_ATTEMPTS) {
1400
- // Force task completion with error message
1401
- conversationLogger.logNarrationDetection('narration', {
1402
- action: 'forced_completion',
1403
- reason: 'max_narration_attempts_reached',
1404
- });
1405
- finalAssistantMessage = '⚠️ **Task Incomplete**: The AI repeatedly described actions without executing them.\n\n' +
1406
- '**What happened**: The AI entered a narration loop, describing what it wanted to do instead of using tool calls.\n\n' +
1407
- '**Suggestions**:\n' +
1408
- '1. Try rephrasing your request more specifically\n' +
1409
- '2. Break the task into smaller, concrete steps\n' +
1410
- '3. Provide explicit file paths if known\n' +
1411
- '4. Check if the model supports tool calling properly\n\n' +
1412
- '**Last message**: ' + assistantMessage;
1413
- break;
1414
- }
1415
- // First narration attempt - give a strong warning with specific guidance
1416
- if (narrationAttempts === 1) {
1417
- const completionPrompt = '🛑 **CRITICAL ERROR**: You output text without using tools.\n\n' +
1418
- '**COMMUNICATION RULE VIOLATION**: You can ONLY communicate through:\n' +
1419
- '1. `reason_text` parameter in tool calls\n' +
1420
- '2. `summary` parameter in task_complete tool\n\n' +
1421
- '**Your text output was HIDDEN from the user.**\n\n' +
1422
- '**MANDATORY CORRECTION**:\n' +
1423
- '- If you need to DO something: Call the tool with `reason_text`\n' +
1424
- '- If you are DONE: Call `task_complete(summary="your message")`\n' +
1425
- '- NEVER output plain text - it will be hidden\n\n' +
1426
- '**Example for greeting**:\n' +
1427
- '```\n' +
1428
- '<thought>User said hello, I should greet back</thought>\n' +
1429
- '(Call task_complete with summary="Hello! How can I help you today?")\n' +
1430
- '```\n\n' +
1431
- '**Your NEXT response MUST use tools.**';
1432
- this.conversationHistory.push({
1433
- role: 'user',
1434
- content: completionPrompt,
1435
- });
1436
- }
1437
- else {
1438
- // Second narration attempt - final warning before forced completion
1439
- const completionPrompt = '🚨 **FINAL WARNING** (Attempt ' + narrationAttempts + '/' + MAX_NARRATION_ATTEMPTS + '): You are STILL narrating instead of executing.\n\n' +
1440
- '**This is your LAST chance**:\n' +
1441
- '1. Execute a tool call NOW, or\n' +
1442
- '2. Call task_complete() to end\n\n' +
1443
- 'If you output narration text again, the task will be forcibly terminated.';
1444
- this.conversationHistory.push({
1445
- role: 'user',
1446
- content: completionPrompt,
1447
- });
1448
- }
1449
- }
1450
- else {
1451
- // AI output a response without narration - it should finish
1452
- // Reset narration counter since this is a valid response
1453
- narrationAttempts = 0;
1454
- // Check if the message looks like a final answer/summary
1455
- // If it has substantial length, assume it's a summary attempt
1456
- const isFinalAnswer = assistantMessage.length > 20;
1457
- if (isFinalAnswer) {
1458
- completionAttempts++;
1459
- conversationLogger.logNarrationDetection('final_answer', {
1460
- turn: turnCount,
1461
- completionAttempts,
1462
- messagePreview: assistantMessage.substring(0, 200),
1463
- });
1464
- // If AI keeps providing text summaries without calling task_complete, accept the text and finish
1465
- // This prevents the infinite loop where the AI keeps summarizing in response to our prompt
1466
- if (completionAttempts > 1) {
1467
- conversationLogger.logNarrationDetection('final_answer', {
1468
- action: 'accepting_text_as_final',
1469
- reason: 'multiple_completion_attempts',
1470
- });
1471
- finalAssistantMessage = assistantMessage;
1472
- break;
1473
- }
1474
- // This looks like a final answer - prompt to call task_complete
1475
- const completionPrompt = '✅ **Possible Completion Detected**: You provided a text response but did not call `task_complete`.\n\n' +
1476
- '**To finish the conversation, you MUST call the `task_complete` tool.**\n\n' +
1477
- 'Please call `task_complete` now with your summary as the argument.';
1478
- this.conversationHistory.push({
1479
- role: 'user',
1480
- content: completionPrompt,
1481
- });
1482
- }
1483
- else {
1484
- // Short message without clear intent - ask for clarification or completion
1485
- const completionPrompt = 'Your response is unclear. Either:\n' +
1486
- '1. Execute the next tool call if more work is needed, or\n' +
1487
- '2. Call task_complete() if the task is done';
1488
- this.conversationHistory.push({
1489
- role: 'user',
1490
- content: completionPrompt,
1491
- });
1492
- }
1493
- }
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;
1494
1844
  }
1495
1845
  // Rebuild messages array with updated history
1496
1846
  // Backend will inject system prompt
@@ -1506,8 +1856,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1506
1856
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
1507
1857
  '**This is not allowed.** You must either:\n' +
1508
1858
  '1. Execute a tool call if more work is needed, OR\n' +
1509
- '2. Call task_complete() with a summary of what you accomplished\n\n' +
1510
- '**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' +
1511
1861
  '**If more work is needed**, execute the next tool call immediately.';
1512
1862
  this.conversationHistory.push({
1513
1863
  role: 'user',
@@ -1594,6 +1944,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1594
1944
  `/mcp - Manage configured MCP servers and tools\n` +
1595
1945
  `/docs - Open Centaurus documentation in browser\n` +
1596
1946
  `/copy-chat-context - Copy chat as readable text to clipboard\n` +
1947
+ `/session-limits - View session quota usage and limits\n` +
1597
1948
  `/quality - Toggle enhanced quality features (thinking protocol, validation)\n` +
1598
1949
  `/autonomous - Toggle autonomous mode (Silent Operator with task_complete)\n` +
1599
1950
  `/sign-in - Sign in with Google (if not already signed in)\n` +
@@ -1607,6 +1958,32 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1607
1958
  `Ctrl+Z - Undo last input change\n` +
1608
1959
  `Ctrl+A - Select all text`;
1609
1960
  break;
1961
+ case 'session-limits': {
1962
+ const config = sessionQuotaManager.getCurrentConfig();
1963
+ const messagesUsed = sessionQuotaManager.getMessagesUsed();
1964
+ const remaining = sessionQuotaManager.getRemainingMessages();
1965
+ const timeRemaining = sessionQuotaManager.getFormattedTimeRemaining();
1966
+ const maxMessages = config.maxMessagesPerSession;
1967
+ // Calculate percentage used (cap at 100% for display)
1968
+ const percentUsed = maxMessages > 0 ? Math.min(100, Math.round((messagesUsed / maxMessages) * 100)) : 0;
1969
+ // Create a visual progress bar (clamp to valid range)
1970
+ const barLength = 20;
1971
+ const filledLength = Math.min(barLength, Math.max(0, Math.round((messagesUsed / maxMessages) * barLength)));
1972
+ const emptyLength = barLength - filledLength;
1973
+ const progressBar = '█'.repeat(filledLength) + '░'.repeat(emptyLength);
1974
+ // Status message based on quota
1975
+ const quotaStatus = remaining <= 0
1976
+ ? '\n\n⚠️ Session quota exhausted! AI requests are blocked until reset.'
1977
+ : '';
1978
+ responseMessage = `📊 Session Limits\n\n` +
1979
+ `Plan: free\n` +
1980
+ `Session Window: ${config.sessionDurationMs / (60 * 60 * 1000)} hours\n\n` +
1981
+ `Messages Used: ${messagesUsed} / ${maxMessages} (${percentUsed}%)\n` +
1982
+ `Messages Left: ${Math.max(0, remaining)}\n` +
1983
+ `Progress: [${progressBar}]\n` +
1984
+ `Time Remaining: ${timeRemaining || 'Session not started'}${quotaStatus}`;
1985
+ break;
1986
+ }
1610
1987
  case 'init':
1611
1988
  try {
1612
1989
  // Define the context file names in priority order
@@ -1884,7 +2261,7 @@ Start by listing the directory structure to understand what you're working with.
1884
2261
  '• Work silently without narrating actions\n' +
1885
2262
  '• Use Touch-First safety (never guess file paths)\n' +
1886
2263
  '• Apply surgical precision to file edits\n' +
1887
- '• Call task_complete when done with comprehensive summary\n' +
2264
+ '• Output summary text, then call task_complete() when done\n' +
1888
2265
  '• Inject intelligent error recovery hints\n\n' +
1889
2266
  'This is the industry-standard autonomous agent mode.'
1890
2267
  : '⚠️ Autonomous Mode disabled\n\n' +
@@ -2826,6 +3203,19 @@ Start by listing the directory structure to understand what you're working with.
2826
3203
  this.onCwdChange(chat.cwd);
2827
3204
  }
2828
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
+ });
2829
3219
  return true;
2830
3220
  }
2831
3221
  /**
@@ -2994,6 +3384,17 @@ Start by listing the directory structure to understand what you're working with.
2994
3384
  this.uiMessageHistory = [];
2995
3385
  this.localCwdBeforeRemote = null;
2996
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
+ });
2997
3398
  }
2998
3399
  /**
2999
3400
  * Update UI message history (called from App.tsx via callback)