centaurus-cli 2.9.1 → 2.9.3

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 (156) hide show
  1. package/dist/cli-adapter.d.ts +76 -3
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +593 -230
  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 +9 -2
  12. package/dist/config/models.js.map +1 -1
  13. package/dist/config/slash-commands.d.ts +3 -0
  14. package/dist/config/slash-commands.d.ts.map +1 -1
  15. package/dist/config/slash-commands.js +39 -4
  16. package/dist/config/slash-commands.js.map +1 -1
  17. package/dist/config/types.d.ts +2 -0
  18. package/dist/config/types.d.ts.map +1 -1
  19. package/dist/config/types.js +1 -0
  20. package/dist/config/types.js.map +1 -1
  21. package/dist/index.js +60 -11
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp/mcp-command-handler.d.ts +34 -3
  24. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  25. package/dist/mcp/mcp-command-handler.js +171 -83
  26. package/dist/mcp/mcp-command-handler.js.map +1 -1
  27. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  28. package/dist/mcp/mcp-server-manager.js +9 -23
  29. package/dist/mcp/mcp-server-manager.js.map +1 -1
  30. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  31. package/dist/mcp/mcp-tool-wrapper.js +42 -5
  32. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  33. package/dist/services/ai-autocomplete-agent.d.ts +39 -0
  34. package/dist/services/ai-autocomplete-agent.d.ts.map +1 -0
  35. package/dist/services/ai-autocomplete-agent.js +189 -0
  36. package/dist/services/ai-autocomplete-agent.js.map +1 -0
  37. package/dist/services/ai-service-client.d.ts +25 -0
  38. package/dist/services/ai-service-client.d.ts.map +1 -1
  39. package/dist/services/ai-service-client.js +162 -1
  40. package/dist/services/ai-service-client.js.map +1 -1
  41. package/dist/services/api-client.d.ts +9 -0
  42. package/dist/services/api-client.d.ts.map +1 -1
  43. package/dist/services/api-client.js +25 -0
  44. package/dist/services/api-client.js.map +1 -1
  45. package/dist/services/auth-handler.js +1 -1
  46. package/dist/services/auth-handler.js.map +1 -1
  47. package/dist/services/input-detection-agent.d.ts +40 -0
  48. package/dist/services/input-detection-agent.d.ts.map +1 -0
  49. package/dist/services/input-detection-agent.js +213 -0
  50. package/dist/services/input-detection-agent.js.map +1 -0
  51. package/dist/services/input-requirement-detector.d.ts +28 -0
  52. package/dist/services/input-requirement-detector.d.ts.map +1 -0
  53. package/dist/services/input-requirement-detector.js +203 -0
  54. package/dist/services/input-requirement-detector.js.map +1 -0
  55. package/dist/services/local-chat-storage.d.ts +21 -0
  56. package/dist/services/local-chat-storage.d.ts.map +1 -1
  57. package/dist/services/local-chat-storage.js +138 -43
  58. package/dist/services/local-chat-storage.js.map +1 -1
  59. package/dist/services/monitored-shell-manager.d.ts +120 -0
  60. package/dist/services/monitored-shell-manager.d.ts.map +1 -0
  61. package/dist/services/monitored-shell-manager.js +239 -0
  62. package/dist/services/monitored-shell-manager.js.map +1 -0
  63. package/dist/services/ollama-service.d.ts +197 -0
  64. package/dist/services/ollama-service.d.ts.map +1 -0
  65. package/dist/services/ollama-service.js +324 -0
  66. package/dist/services/ollama-service.js.map +1 -0
  67. package/dist/services/shell-input-agent.d.ts +89 -0
  68. package/dist/services/shell-input-agent.d.ts.map +1 -0
  69. package/dist/services/shell-input-agent.js +361 -0
  70. package/dist/services/shell-input-agent.js.map +1 -0
  71. package/dist/services/sub-agent-manager.d.ts +139 -0
  72. package/dist/services/sub-agent-manager.d.ts.map +1 -0
  73. package/dist/services/sub-agent-manager.js +517 -0
  74. package/dist/services/sub-agent-manager.js.map +1 -0
  75. package/dist/tools/background-command.d.ts.map +1 -1
  76. package/dist/tools/background-command.js +33 -13
  77. package/dist/tools/background-command.js.map +1 -1
  78. package/dist/tools/command.d.ts.map +1 -1
  79. package/dist/tools/command.js +64 -1
  80. package/dist/tools/command.js.map +1 -1
  81. package/dist/tools/file-ops.d.ts.map +1 -1
  82. package/dist/tools/file-ops.js +33 -19
  83. package/dist/tools/file-ops.js.map +1 -1
  84. package/dist/tools/get-diff.js +1 -1
  85. package/dist/tools/get-diff.js.map +1 -1
  86. package/dist/tools/grep-search.d.ts.map +1 -1
  87. package/dist/tools/grep-search.js +41 -15
  88. package/dist/tools/grep-search.js.map +1 -1
  89. package/dist/tools/plan-mode.js +3 -3
  90. package/dist/tools/plan-mode.js.map +1 -1
  91. package/dist/tools/registry.js +1 -1
  92. package/dist/tools/registry.js.map +1 -1
  93. package/dist/tools/sub-agent.d.ts +9 -0
  94. package/dist/tools/sub-agent.d.ts.map +1 -0
  95. package/dist/tools/sub-agent.js +232 -0
  96. package/dist/tools/sub-agent.js.map +1 -0
  97. package/dist/tools/task-complete.d.ts.map +1 -1
  98. package/dist/tools/task-complete.js +14 -26
  99. package/dist/tools/task-complete.js.map +1 -1
  100. package/dist/ui/components/App.d.ts +45 -2
  101. package/dist/ui/components/App.d.ts.map +1 -1
  102. package/dist/ui/components/App.js +605 -96
  103. package/dist/ui/components/App.js.map +1 -1
  104. package/dist/ui/components/CircularSelectInput.d.ts +24 -0
  105. package/dist/ui/components/CircularSelectInput.d.ts.map +1 -0
  106. package/dist/ui/components/CircularSelectInput.js +71 -0
  107. package/dist/ui/components/CircularSelectInput.js.map +1 -0
  108. package/dist/ui/components/ErrorBoundary.d.ts +3 -2
  109. package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
  110. package/dist/ui/components/ErrorBoundary.js +29 -1
  111. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  112. package/dist/ui/components/InputBox.d.ts +4 -0
  113. package/dist/ui/components/InputBox.d.ts.map +1 -1
  114. package/dist/ui/components/InputBox.js +343 -21
  115. package/dist/ui/components/InputBox.js.map +1 -1
  116. package/dist/ui/components/InteractiveShell.d.ts +6 -0
  117. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  118. package/dist/ui/components/InteractiveShell.js +57 -6
  119. package/dist/ui/components/InteractiveShell.js.map +1 -1
  120. package/dist/ui/components/MCPAddScreen.d.ts +13 -0
  121. package/dist/ui/components/MCPAddScreen.d.ts.map +1 -0
  122. package/dist/ui/components/MCPAddScreen.js +54 -0
  123. package/dist/ui/components/MCPAddScreen.js.map +1 -0
  124. package/dist/ui/components/MCPListScreen.d.ts +17 -0
  125. package/dist/ui/components/MCPListScreen.d.ts.map +1 -0
  126. package/dist/ui/components/MCPListScreen.js +50 -0
  127. package/dist/ui/components/MCPListScreen.js.map +1 -0
  128. package/dist/ui/components/MCPServerListScreen.d.ts +16 -0
  129. package/dist/ui/components/MCPServerListScreen.d.ts.map +1 -0
  130. package/dist/ui/components/MCPServerListScreen.js +59 -0
  131. package/dist/ui/components/MCPServerListScreen.js.map +1 -0
  132. package/dist/ui/components/MonitorModeAIPanel.d.ts +23 -0
  133. package/dist/ui/components/MonitorModeAIPanel.d.ts.map +1 -0
  134. package/dist/ui/components/MonitorModeAIPanel.js +69 -0
  135. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -0
  136. package/dist/ui/components/MultiLineInput.d.ts +13 -0
  137. package/dist/ui/components/MultiLineInput.d.ts.map +1 -0
  138. package/dist/ui/components/MultiLineInput.js +289 -0
  139. package/dist/ui/components/MultiLineInput.js.map +1 -0
  140. package/dist/ui/components/StatusBar.d.ts +2 -0
  141. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  142. package/dist/ui/components/StatusBar.js +33 -2
  143. package/dist/ui/components/StatusBar.js.map +1 -1
  144. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  145. package/dist/ui/components/ToolExecutionMessage.js +231 -13
  146. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  147. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  148. package/dist/ui/components/VersionUpdatePrompt.js +3 -2
  149. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  150. package/dist/utils/command-history.d.ts +12 -2
  151. package/dist/utils/command-history.d.ts.map +1 -1
  152. package/dist/utils/command-history.js +57 -13
  153. package/dist/utils/command-history.js.map +1 -1
  154. package/dist/utils/input-classifier.js +1 -1
  155. package/dist/utils/input-classifier.js.map +1 -1
  156. 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';
@@ -39,6 +42,7 @@ 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';
41
44
  import { sessionQuotaManager } from './services/session-quota-manager.js';
45
+ import { ollamaService, OllamaService } from './services/ollama-service.js';
42
46
  export class CentaurusCLI {
43
47
  configManager;
44
48
  toolRegistry;
@@ -48,6 +52,7 @@ export class CentaurusCLI {
48
52
  pendingPlanRequest = null; // Stores original user request during planning phase
49
53
  commandMode = false;
50
54
  backgroundMode = false; // Background shell mode for running commands in background
55
+ shellIdCounter = 1;
51
56
  previousMode = 'execution';
52
57
  onResponseCallback;
53
58
  onDirectMessageCallback; // For slash commands - adds directly to history
@@ -94,7 +99,18 @@ export class CentaurusCLI {
94
99
  onShowBackgroundTaskCancelPickerCallback;
95
100
  onBackgroundTaskViewCallback;
96
101
  onTokenCountUpdate; // Report actual AI context token count to UI
102
+ currentTokenCount = 0; // Track current token count for context limit checking
103
+ contextLimitReached = false; // Track if context limit has been reached
104
+ onContextLimitReached; // Notify UI about context limit state
97
105
  onSessionQuotaUpdate;
106
+ // MCP screen callbacks
107
+ onShowMCPAddScreen;
108
+ onShowMCPRemoveScreen;
109
+ onShowMCPEnableScreen;
110
+ onShowMCPDisableScreen;
111
+ onShowMCPListScreen;
112
+ onSubAgentCountChange; // Callback for sub-agent count changes
113
+ onPromptAnswered; // Callback when AI answers a shell prompt
98
114
  constructor() {
99
115
  this.configManager = new ConfigManager();
100
116
  this.toolRegistry = new ToolRegistry();
@@ -115,6 +131,11 @@ export class CentaurusCLI {
115
131
  });
116
132
  // Initialize MCP
117
133
  this.initializeMCP();
134
+ // Initialize ShellInputAgent with tool registry and wire shell input callback
135
+ ShellInputAgent.initialize(this.toolRegistry);
136
+ ShellInputAgent.setOnShellInput((shellId, input) => {
137
+ this.writeToShellStdin(input);
138
+ });
118
139
  }
119
140
  setOnResponseCallback(callback) {
120
141
  this.onResponseCallback = callback;
@@ -186,9 +207,131 @@ export class CentaurusCLI {
186
207
  setOnTokenCountUpdate(callback) {
187
208
  this.onTokenCountUpdate = callback;
188
209
  }
210
+ setOnContextLimitReached(callback) {
211
+ this.onContextLimitReached = callback;
212
+ }
213
+ setOnSubAgentCountChange(callback) {
214
+ this.onSubAgentCountChange = callback;
215
+ }
216
+ setOnPromptAnswered(callback) {
217
+ this.onPromptAnswered = callback;
218
+ // Wire this callback to ShellInputAgent
219
+ ShellInputAgent.setOnPromptAnswered(callback);
220
+ }
221
+ /**
222
+ * Calculate and update token count based on current conversation history
223
+ * This ensures UI is always in sync with the actual AI context
224
+ * Uses backend's accurate token counting API (Vertex AI countTokens)
225
+ */
226
+ async updateTokenCount() {
227
+ if (!this.onTokenCountUpdate)
228
+ return;
229
+ try {
230
+ // Get current model
231
+ const currentModel = this.configManager.get('modelName') || 'gemini-2.5-flash';
232
+ // Prepare messages for token counting
233
+ // Backend will automatically include system prompt when counting
234
+ // We just send the conversation history
235
+ const messagesForCounting = [...this.conversationHistory];
236
+ // Call backend API for accurate token counting
237
+ const tokenCount = await apiClient.countTokens(currentModel, messagesForCounting);
238
+ // Store locally for context limit checking
239
+ this.currentTokenCount = tokenCount;
240
+ // Update UI with accurate count
241
+ this.onTokenCountUpdate(tokenCount);
242
+ quickLog(`[${new Date().toISOString()}] [updateTokenCount] Accurate count: ${tokenCount} tokens for ${messagesForCounting.length} messages\n`);
243
+ }
244
+ catch (error) {
245
+ // Fallback to character-based estimation if API fails
246
+ const SYSTEM_PROMPT_ESTIMATE = 14000; // Backend injects ~14K char system prompt
247
+ // Calculate total characters from conversation history
248
+ let totalCharacters = 0;
249
+ for (const msg of this.conversationHistory) {
250
+ // Content
251
+ if (typeof msg.content === 'string') {
252
+ totalCharacters += msg.content.length;
253
+ }
254
+ // Thinking content
255
+ if (msg.thinking) {
256
+ totalCharacters += msg.thinking.length;
257
+ }
258
+ // Tool calls
259
+ if (msg.tool_calls) {
260
+ for (const tc of msg.tool_calls) {
261
+ totalCharacters += tc.name.length;
262
+ if (tc.arguments) {
263
+ totalCharacters += JSON.stringify(tc.arguments).length;
264
+ }
265
+ }
266
+ }
267
+ // Tool call ID
268
+ if (msg.role === 'tool' && msg.tool_call_id) {
269
+ totalCharacters += msg.tool_call_id.length;
270
+ }
271
+ }
272
+ // Estimate tokens (1 token ≈ 4 chars)
273
+ // Only include system prompt estimate if there's conversation history
274
+ const systemPromptChars = this.conversationHistory.length > 0 ? SYSTEM_PROMPT_ESTIMATE : 0;
275
+ const estimatedTokens = Math.ceil((totalCharacters + systemPromptChars) / 4);
276
+ // Store locally for context limit checking
277
+ this.currentTokenCount = estimatedTokens;
278
+ this.onTokenCountUpdate(estimatedTokens);
279
+ quickLog(`[${new Date().toISOString()}] [updateTokenCount] Fallback estimate: ${estimatedTokens} tokens (API error: ${error})\n`);
280
+ }
281
+ }
282
+ /**
283
+ * Get current token count for context limit checking
284
+ */
285
+ getCurrentTokenCount() {
286
+ return this.currentTokenCount;
287
+ }
189
288
  setOnSessionQuotaUpdate(callback) {
190
289
  this.onSessionQuotaUpdate = callback;
191
290
  }
291
+ // MCP screen callback setters
292
+ setOnMCPAddScreenSetup(callback) {
293
+ this.onShowMCPAddScreen = callback;
294
+ }
295
+ setOnMCPRemoveScreenSetup(callback) {
296
+ this.onShowMCPRemoveScreen = callback;
297
+ }
298
+ setOnMCPEnableScreenSetup(callback) {
299
+ this.onShowMCPEnableScreen = callback;
300
+ }
301
+ setOnMCPDisableScreenSetup(callback) {
302
+ this.onShowMCPDisableScreen = callback;
303
+ }
304
+ setOnMCPListScreenSetup(callback) {
305
+ this.onShowMCPListScreen = callback;
306
+ }
307
+ // MCP server operation methods (called from UI)
308
+ mcpAddServer(config) {
309
+ if (this.mcpCommandHandler) {
310
+ return this.mcpCommandHandler.addServer(config);
311
+ }
312
+ return { success: false, error: 'MCP not initialized' };
313
+ }
314
+ mcpRemoveServer(name) {
315
+ if (this.mcpCommandHandler) {
316
+ this.mcpCommandHandler.removeServer(name);
317
+ }
318
+ }
319
+ mcpEnableServer(name) {
320
+ if (this.mcpCommandHandler) {
321
+ this.mcpCommandHandler.enableServer(name);
322
+ }
323
+ }
324
+ mcpDisableServer(name) {
325
+ if (this.mcpCommandHandler) {
326
+ this.mcpCommandHandler.disableServer(name);
327
+ }
328
+ }
329
+ mcpValidateConfig(jsonString) {
330
+ if (this.mcpCommandHandler) {
331
+ return this.mcpCommandHandler.validateServerConfig(jsonString);
332
+ }
333
+ return { valid: false, error: 'MCP not initialized' };
334
+ }
192
335
  /**
193
336
  * Notify UI about session quota status
194
337
  */
@@ -205,6 +348,32 @@ export class CentaurusCLI {
205
348
  const mcpConfigManager = new MCPConfigManager();
206
349
  const mcpServerManager = new MCPServerManager();
207
350
  this.mcpCommandHandler = new MCPCommandHandler(mcpConfigManager, mcpServerManager, this.toolRegistry);
351
+ // Wire MCP screen callbacks
352
+ this.mcpCommandHandler.setOnShowMCPAddScreen(() => {
353
+ if (this.onShowMCPAddScreen) {
354
+ this.onShowMCPAddScreen();
355
+ }
356
+ });
357
+ this.mcpCommandHandler.setOnShowMCPRemoveScreen((servers) => {
358
+ if (this.onShowMCPRemoveScreen) {
359
+ this.onShowMCPRemoveScreen(servers);
360
+ }
361
+ });
362
+ this.mcpCommandHandler.setOnShowMCPEnableScreen((servers) => {
363
+ if (this.onShowMCPEnableScreen) {
364
+ this.onShowMCPEnableScreen(servers);
365
+ }
366
+ });
367
+ this.mcpCommandHandler.setOnShowMCPDisableScreen((servers) => {
368
+ if (this.onShowMCPDisableScreen) {
369
+ this.onShowMCPDisableScreen(servers);
370
+ }
371
+ });
372
+ this.mcpCommandHandler.setOnShowMCPListScreen((servers) => {
373
+ if (this.onShowMCPListScreen) {
374
+ this.onShowMCPListScreen(servers);
375
+ }
376
+ });
208
377
  // Initialize MCP servers and tools
209
378
  await this.mcpCommandHandler.initializeMCP();
210
379
  }
@@ -249,12 +418,22 @@ export class CentaurusCLI {
249
418
  getCommandMode() {
250
419
  return this.commandMode;
251
420
  }
421
+ /**
422
+ * Get current conversation history for shell input agent context
423
+ * Returns a copy to prevent modification
424
+ */
425
+ getConversationHistory() {
426
+ return [...this.conversationHistory];
427
+ }
252
428
  getCurrentWorkingDirectory() {
253
429
  return this.cwd;
254
430
  }
255
431
  getCurrentSubshellContext() {
256
432
  return this.contextManager.getCurrentContext();
257
433
  }
434
+ getCurrentInteractiveProcess() {
435
+ return this.currentInteractiveProcess;
436
+ }
258
437
  /**
259
438
  * Get the current conversation ID for file uploads
260
439
  */
@@ -263,25 +442,48 @@ export class CentaurusCLI {
263
442
  }
264
443
  async handlePickerSelection(selection, pickerType) {
265
444
  try {
266
- // Selection is the index of the model in models array from backend
267
- const modelsConfig = await fetchModelsConfig();
268
- const modelIndex = parseInt(selection, 10);
269
- if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >= modelsConfig.models.length) {
270
- throw new Error('Invalid model selection');
271
- }
272
- const selectedModel = modelsConfig.models[modelIndex];
273
- // Store only the model ID and name (not the full config with thinkingConfig)
274
- // This prevents caching issues when we update model configs
275
- this.configManager.set('model', selectedModel.id);
276
- this.configManager.set('modelName', selectedModel.name);
277
- // Notify UI of model name change and contextWindow
278
- if (this.onModelChange) {
279
- this.onModelChange(selectedModel.name, selectedModel.contextWindow);
280
- }
281
- const responseMessage = `Model changed to: ${selectedModel.name}`;
282
- // Send response back to UI
283
- if (this.onResponseCallback) {
284
- this.onResponseCallback(responseMessage);
445
+ if (pickerType === 'local-model') {
446
+ // Local Ollama model selection
447
+ // Selection is the model name (e.g., "llama3:latest")
448
+ const modelName = selection;
449
+ // Store the local model configuration
450
+ this.configManager.set('model', modelName);
451
+ this.configManager.set('modelName', modelName);
452
+ this.configManager.set('isLocalModel', true);
453
+ // Notify UI of model name change
454
+ // Local models don't have a fixed context window, use a reasonable default
455
+ if (this.onModelChange) {
456
+ this.onModelChange(modelName, 128000); // Most local models have 128k context
457
+ }
458
+ const responseMessage = `✅ Switched to local Ollama model: ${modelName}`;
459
+ // Send response back to UI
460
+ if (this.onResponseCallback) {
461
+ this.onResponseCallback(responseMessage);
462
+ }
463
+ }
464
+ else {
465
+ // Cloud model selection (existing behavior)
466
+ // Selection is the index of the model in models array from backend
467
+ const modelsConfig = await fetchModelsConfig();
468
+ const modelIndex = parseInt(selection, 10);
469
+ if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >= modelsConfig.models.length) {
470
+ throw new Error('Invalid model selection');
471
+ }
472
+ const selectedModel = modelsConfig.models[modelIndex];
473
+ // Store only the model ID and name (not the full config with thinkingConfig)
474
+ // This prevents caching issues when we update model configs
475
+ this.configManager.set('model', selectedModel.id);
476
+ this.configManager.set('modelName', selectedModel.name);
477
+ this.configManager.set('isLocalModel', false);
478
+ // Notify UI of model name change and contextWindow
479
+ if (this.onModelChange) {
480
+ this.onModelChange(selectedModel.name, selectedModel.contextWindow);
481
+ }
482
+ const responseMessage = `✅ Switched to cloud model: ${selectedModel.name}`;
483
+ // Send response back to UI
484
+ if (this.onResponseCallback) {
485
+ this.onResponseCallback(responseMessage);
486
+ }
285
487
  }
286
488
  }
287
489
  catch (error) {
@@ -371,6 +573,14 @@ export class CentaurusCLI {
371
573
  this.toolRegistry.register(readBinaryFileTool);
372
574
  this.toolRegistry.register(createImageTool);
373
575
  this.toolRegistry.register(backgroundCommandTool);
576
+ this.toolRegistry.register(subAgentTool);
577
+ // Initialize SubAgentManager with tool registry
578
+ SubAgentManager.initialize(this.toolRegistry);
579
+ SubAgentManager.setOnSubAgentCountChange((count) => {
580
+ if (this.onSubAgentCountChange) {
581
+ this.onSubAgentCountChange(count);
582
+ }
583
+ });
374
584
  // Load configuration
375
585
  const config = this.configManager.load();
376
586
  // Enable backend sync if authenticated
@@ -473,41 +683,69 @@ Press Enter to continue...
473
683
  }
474
684
  /**
475
685
  * Clean up orphaned tool_calls from conversation history.
476
- * This is called when a request is aborted to ensure the history remains consistent.
477
- * Vertex AI requires that every assistant message with tool_calls has matching tool result messages.
686
+ * This validates the ENTIRE history and removes any assistant messages
687
+ * where tool_calls don't have matching tool result messages.
688
+ *
689
+ * Vertex AI / Claude APIs require that every assistant message with tool_calls
690
+ * has matching tool result messages immediately following it.
478
691
  */
479
692
  cleanupOrphanedToolCalls() {
480
693
  if (this.conversationHistory.length === 0)
481
694
  return;
482
- // Find the last assistant message with tool_calls
483
- let lastAssistantWithToolCallsIndex = -1;
484
- for (let i = this.conversationHistory.length - 1; i >= 0; i--) {
485
- const msg = this.conversationHistory[i];
486
- if (msg.role === 'assistant' && msg.tool_calls && msg.tool_calls.length > 0) {
487
- lastAssistantWithToolCallsIndex = i;
488
- break;
695
+ let cleanedAny = false;
696
+ let iterations = 0;
697
+ const maxIterations = 20; // Safety limit to prevent infinite loops
698
+ // Keep cleaning until no more orphans are found
699
+ // (removing one orphan may expose another)
700
+ while (iterations < maxIterations) {
701
+ iterations++;
702
+ let foundOrphan = false;
703
+ // Scan through history to find ALL assistant messages with tool_calls
704
+ for (let i = 0; i < this.conversationHistory.length; i++) {
705
+ const msg = this.conversationHistory[i];
706
+ if (msg.role !== 'assistant' || !msg.tool_calls || msg.tool_calls.length === 0) {
707
+ continue;
708
+ }
709
+ // Collect all tool_call IDs from this assistant message
710
+ const expectedToolCallIds = new Set(msg.tool_calls.map((tc) => tc.id));
711
+ // Check if ALL tool_calls have matching tool result messages after this message
712
+ // Tool results must come AFTER the assistant message, before the next user/assistant message
713
+ let j = i + 1;
714
+ while (j < this.conversationHistory.length) {
715
+ const nextMsg = this.conversationHistory[j];
716
+ // If we hit a user or assistant message, stop looking for tool results
717
+ if (nextMsg.role === 'user' || nextMsg.role === 'assistant') {
718
+ break;
719
+ }
720
+ // If it's a tool result, check if it matches one of our expected IDs
721
+ if (nextMsg.role === 'tool' && nextMsg.tool_call_id) {
722
+ expectedToolCallIds.delete(nextMsg.tool_call_id);
723
+ }
724
+ j++;
725
+ }
726
+ // If there are still unmatched tool_calls, this is an orphan
727
+ if (expectedToolCallIds.size > 0) {
728
+ try {
729
+ quickLog(`[${new Date().toISOString()}] [CLI] Found orphaned tool_calls at index ${i}: ${Array.from(expectedToolCallIds).join(', ')}\n`);
730
+ }
731
+ catch (e) { }
732
+ // Remove this assistant message and all tool results up to (but not including) the next user/assistant message
733
+ const removeCount = j - i;
734
+ this.conversationHistory.splice(i, removeCount);
735
+ foundOrphan = true;
736
+ cleanedAny = true;
737
+ break; // Restart scan from beginning since indices changed
738
+ }
489
739
  }
490
- }
491
- if (lastAssistantWithToolCallsIndex === -1)
492
- return; // No assistant messages with tool_calls
493
- const assistantMsg = this.conversationHistory[lastAssistantWithToolCallsIndex];
494
- const toolCallIds = new Set(assistantMsg.tool_calls.map((tc) => tc.id));
495
- // Check if all tool_calls have matching tool result messages after this assistant message
496
- for (let i = lastAssistantWithToolCallsIndex + 1; i < this.conversationHistory.length; i++) {
497
- const msg = this.conversationHistory[i];
498
- if (msg.role === 'tool' && msg.tool_call_id) {
499
- toolCallIds.delete(msg.tool_call_id);
740
+ if (!foundOrphan) {
741
+ break; // No more orphans found, we're done
500
742
  }
501
743
  }
502
- // If there are orphaned tool_calls (no matching tool results), remove from history
503
- if (toolCallIds.size > 0) {
504
- // Log the cleanup for debugging
744
+ if (cleanedAny) {
505
745
  try {
506
- quickLog(`[${new Date().toISOString()}] [CLI] Cleaning up orphaned tool_calls: ${Array.from(toolCallIds).join(', ')}\n`);
746
+ quickLog(`[${new Date().toISOString()}] [CLI] Completed history cleanup after ${iterations} iteration(s), ${this.conversationHistory.length} messages remaining\n`);
507
747
  }
508
748
  catch (e) { }
509
- // Remove the orphaned assistant message and any partial tool results after it
510
- this.conversationHistory.splice(lastAssistantWithToolCallsIndex);
511
749
  }
512
750
  }
513
751
  /**
@@ -562,11 +800,59 @@ Press Enter to continue...
562
800
  this.notifySessionQuotaStatus();
563
801
  return;
564
802
  }
803
+ // Check context window limit before accepting new messages
804
+ // Get current model's context window
805
+ const currentModel = this.configManager.get('modelName') || 'gemini-2.5-flash';
806
+ const { getModelContextWindowSync } = await import('./config/models.js');
807
+ const maxTokens = getModelContextWindowSync(currentModel);
808
+ // Calculate current token usage percentage
809
+ // We need to estimate tokens for the new message too
810
+ const newMessageChars = message.length;
811
+ const estimatedNewMessageTokens = Math.ceil(newMessageChars / 4);
812
+ // Get current token count from state (updated by updateTokenCount)
813
+ const currentTokens = this.getCurrentTokenCount();
814
+ const projectedTokens = currentTokens + estimatedNewMessageTokens;
815
+ const usagePercent = (projectedTokens / maxTokens) * 100;
816
+ // Block new messages if context is ≥80% full
817
+ if (usagePercent >= 80) {
818
+ // Set context limit reached state
819
+ if (!this.contextLimitReached) {
820
+ this.contextLimitReached = true;
821
+ if (this.onContextLimitReached) {
822
+ this.onContextLimitReached(true);
823
+ }
824
+ }
825
+ 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)`;
826
+ if (this.onDirectMessageCallback) {
827
+ this.onDirectMessageCallback(contextLimitMessage);
828
+ }
829
+ quickLog(`[${new Date().toISOString()}] [handleMessage] Context limit reached: ${usagePercent.toFixed(1)}% (${projectedTokens}/${maxTokens} tokens)\n`);
830
+ return;
831
+ }
565
832
  // Cancel any active request when a new message comes in
566
833
  // This enables "interrupt and replace" - new message takes priority
567
834
  if (this.currentAbortController) {
568
- this.currentAbortController.abort();
569
- this.currentAbortController = undefined;
835
+ // Mark as intentionally aborted so error handling knows not to throw or show message
836
+ this.requestIntentionallyAborted = true;
837
+ const oldController = this.currentAbortController;
838
+ // Create new controller BEFORE aborting old one to avoid race condition
839
+ // where new request tries to access undefined controller
840
+ this.currentAbortController = new AbortController();
841
+ oldController.abort();
842
+ // Clean up orphaned tool calls from the interrupted turn
843
+ this.cleanupOrphanedToolCalls();
844
+ // Remove the last user message from history (it's being replaced by the new message)
845
+ // Walk backwards and remove messages until we find and remove a user message
846
+ while (this.conversationHistory.length > 0) {
847
+ const lastMsg = this.conversationHistory[this.conversationHistory.length - 1];
848
+ this.conversationHistory.pop();
849
+ if (lastMsg.role === 'user') {
850
+ // Found and removed the interrupted user message, stop here
851
+ break;
852
+ }
853
+ // Continue removing assistant/tool messages that were part of the interrupted turn
854
+ }
855
+ quickLog(`[${new Date().toISOString()}] [handleMessage] Interrupted active request - cleaned up history for replacement\n`);
570
856
  }
571
857
  // Store original request if in planning mode (for execution phase after approval)
572
858
  if (this.planMode && !this.pendingPlanRequest) {
@@ -613,6 +899,12 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
613
899
  contextManager: this.contextManager,
614
900
  cliAdapter: this, // Pass CLI adapter reference for interactive process management
615
901
  requireApproval: async (message, risky, preview, operationType, operationDetails) => {
902
+ // Special bypass for shell input to running processes:
903
+ // If the AI is sending input to an existing shell (via shell_input), we bypass the separate approval step.
904
+ // The user already implicitly approved the interaction by running the command in agent control mode.
905
+ if (operationType === 'execute_command' && operationDetails?.shell_input) {
906
+ return true;
907
+ }
616
908
  if (this.onToolApprovalRequest) {
617
909
  return await this.onToolApprovalRequest({ message, risky, preview, operationType, operationDetails });
618
910
  }
@@ -636,6 +928,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
636
928
  // Build messages array WITHOUT system prompt - backend will inject it
637
929
  // The backend uses cli-system-prompt.md for CLI clients
638
930
  // We pass environmentContext and mode separately so backend can inject them
931
+ // SAFETY: Clean up any orphaned tool calls before making AI request
932
+ // This prevents "improperly formed request" errors from corrupted history
933
+ this.cleanupOrphanedToolCalls();
639
934
  let messages = [...this.conversationHistory];
640
935
  // Inject subshell context if in a subshell environment
641
936
  const currentContext = this.contextManager.getCurrentContext();
@@ -646,11 +941,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
646
941
  const mode = this.getMode();
647
942
  let finalAssistantMessage = '';
648
943
  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
944
  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
945
  let thoughtStartTime = null; // Track when thinking started
655
946
  let thoughtContent = ''; // Accumulate thought content during streaming
656
947
  let currentTurnThinking = ''; // Persist thinking for the current turn to attach to assistant message
@@ -662,9 +953,12 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
662
953
  // ANTI-LOOP: Track ALL duplicate tool calls (not just file ops)
663
954
  const toolCallTracker = new Map(); // Hash -> count
664
955
  const MAX_IDENTICAL_TOOL_CALLS = 3; // Max times exact same tool call allowed
665
- // Create AbortController for this request
666
- this.currentAbortController = new AbortController();
667
- this.requestIntentionallyAborted = false; // Reset abort flag for new request
956
+ // Create AbortController for this request (if not already created during interruption handling)
957
+ if (!this.currentAbortController) {
958
+ this.currentAbortController = new AbortController();
959
+ }
960
+ // Note: Don't reset requestIntentionallyAborted here - let the error handler reset it
961
+ // to avoid race condition where old request's error handler sees false
668
962
  // Clean up any orphaned tool_calls from a previous aborted request
669
963
  // This prevents 400 Bad Request errors when sending to the backend
670
964
  this.cleanupOrphanedToolCalls();
@@ -736,16 +1030,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
736
1030
  quickLog(`[${new Date().toISOString()}] [CLI] Assistant messages with tool_calls: ${messageStats.assistantWithToolCalls}\n`);
737
1031
  }
738
1032
  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
- }
1033
+ // Update token count using accurate API
1034
+ // This will use backend's Vertex AI countTokens for precision
1035
+ this.updateTokenCount().catch(err => {
1036
+ quickLog(`[${new Date().toISOString()}] [CLI] Failed to update token count: ${err}\n`);
1037
+ });
746
1038
  // Stream AI response from backend
747
1039
  // Backend will inject system prompt automatically with environment context
748
- for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController.signal)) {
1040
+ for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, this.currentAbortController?.signal)) {
749
1041
  // Handle error chunks
750
1042
  if (chunk.type === 'error') {
751
1043
  // Check if this is an abort situation (user cancelled or sent new message)
@@ -817,6 +1109,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
817
1109
  // Handle tool call chunks
818
1110
  if (chunk.type === 'tool_call') {
819
1111
  const toolCall = chunk.toolCall;
1112
+ // Kiro/Claude compatibility: Parse string arguments early so they are objects throughout the pipeline
1113
+ // This ensures logging, UI updates, and tool execution all see the parsed object
1114
+ if (toolCall.arguments && typeof toolCall.arguments === 'string') {
1115
+ try {
1116
+ toolCall.arguments = JSON.parse(toolCall.arguments);
1117
+ }
1118
+ catch (e) {
1119
+ // Ignore parsing error, will be handled by downstream logic
1120
+ }
1121
+ }
820
1122
  // Debug: Log every tool_call chunk received
821
1123
  try {
822
1124
  quickLog(`[${new Date().toISOString()}] [CLI] *** TOOL_CALL CHUNK RECEIVED (REAL-TIME): ${toolCall?.name || 'unknown'}\n`);
@@ -856,9 +1158,11 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
856
1158
  // REAL-TIME EXECUTION: Execute tool immediately during streaming
857
1159
  // This reduces latency by not waiting for the entire stream to finish
858
1160
  try {
859
- // Extract and display reason_text if present (skip for task_complete)
1161
+ // Extract and display reason_text if present (skip for task_complete and shell_input)
860
1162
  const reasonText = toolCall.arguments.reason_text;
861
- if (reasonText && this.onResponseStreamCallback) {
1163
+ // Don't show reason text for shell inputs (hidden from history per user request)
1164
+ const isShellInput = toolCall.name === 'execute_command' && toolCall.arguments.shell_input;
1165
+ if (reasonText && !isShellInput && this.onResponseStreamCallback) {
862
1166
  this.onResponseStreamCallback(reasonText + '\n\n');
863
1167
  }
864
1168
  // Show 'executing' status immediately
@@ -866,6 +1170,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
866
1170
  // Log tool execution start
867
1171
  conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
868
1172
  // Execute the tool (it will request approval if needed via requireApproval callback)
1173
+ // SPECIAL: Intercept sub_agent spawn to enforce approval
1174
+ if (toolCall.name === 'sub_agent' && toolCall.arguments?.action === 'spawn') {
1175
+ const approved = await context.requireApproval(`Spawn Sub-Agent`, true, // risky
1176
+ undefined, 'execute_command', { command: `spawn sub-agent` });
1177
+ if (!approved) {
1178
+ throw new Error('User rejected sub-agent spawn request');
1179
+ }
1180
+ }
869
1181
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
870
1182
  if (result.success) {
871
1183
  conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
@@ -947,8 +1259,6 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
947
1259
  assistantMessageLength: assistantMessage.length,
948
1260
  hasToolCalls: toolCalls.length > 0,
949
1261
  willContinue: toolCalls.length > 0,
950
- narrationAttempts,
951
- completionAttempts,
952
1262
  });
953
1263
  // If there are tool calls, execute them
954
1264
  if (toolCalls.length > 0) {
@@ -961,11 +1271,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
961
1271
  // Suppress text output - AI should only use reason_text
962
1272
  assistantMessage = ''; // Clear ALL text output - AI should only use reason_text
963
1273
  }
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
- }
1274
+ // Tool call limit removed - let AI use as many tools as needed per turn
969
1275
  const toolResults = [...inStreamToolResults]; // Start with in-stream results
970
1276
  const handledToolCallIds = new Set(); // Only for special tools (create_plan, mark_task_complete)
971
1277
  let userCancelledOperation = false;
@@ -989,25 +1295,24 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
989
1295
  try {
990
1296
  // Check if this is task_complete FIRST (before displaying anything)
991
1297
  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
- }
1298
+ // SUBAGENT BLOCKING: Check if any sub-agents are still running
1299
+ const runningSubAgents = SubAgentManager.getRunningSubAgents();
1300
+ if (runningSubAgents.length > 0) {
1301
+ // Block task_complete and provide feedback
1302
+ const agentIds = runningSubAgents.map(a => a.id).join(', ');
1303
+ toolResults.push({
1304
+ tool_call_id: toolCall.id,
1305
+ name: toolCall.name,
1306
+ 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.`,
1307
+ });
1308
+ handledToolCallIds.add(toolCall.id);
1309
+ continue; // Skip task_complete execution, keep loop running
1009
1310
  }
1010
- // If no summary, keep assistantMessage as-is (it was already streamed to UI)
1311
+ taskCompleted = true;
1312
+ conversationLogger.logTaskComplete('');
1313
+ // task_complete no longer has a summary parameter
1314
+ // The AI streams all response text BEFORE calling task_complete()
1315
+ // So we just preserve whatever assistantMessage was already streamed
1011
1316
  // Execute the tool for proper result handling
1012
1317
  await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
1013
1318
  // Clear the plan when task is complete
@@ -1240,7 +1545,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1240
1545
  tool_call_id: toolCall.id,
1241
1546
  name: toolCall.name,
1242
1547
  result: completion.allComplete
1243
- ? 'All tasks completed! Call task_complete with a summary of what was accomplished.'
1548
+ ? 'All tasks completed! Output your summary of what was accomplished, then call task_complete().'
1244
1549
  : completion.nextSubtask
1245
1550
  ? `Subtask ${completion.taskNumber} completed. Next subtask: ${completion.nextSubtask}`
1246
1551
  : completion.nextTask
@@ -1250,7 +1555,7 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1250
1555
  // If all tasks are complete, prompt AI to call task_complete
1251
1556
  if (completion.allComplete) {
1252
1557
  toolResults[toolResults.length - 1].result =
1253
- 'All tasks in the plan are now completed! Call task_complete with a summary of what was accomplished.';
1558
+ 'All tasks in the plan are now completed! Output your summary of what was accomplished, then call task_complete().';
1254
1559
  }
1255
1560
  }
1256
1561
  catch (parseError) {
@@ -1314,12 +1619,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1314
1619
  remoteContext = `docker:${metadata.containerId?.substring(0, 12) || 'container'}`;
1315
1620
  }
1316
1621
  }
1317
- // Notify UI: tool is executing (send before execution starts)
1622
+ // Notify UI: tool executing
1318
1623
  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 };
1624
+ let toolArgs = { ...toolCall.arguments, remoteContext };
1625
+ // Special handling for execute_command
1626
+ if (toolCall.name === 'execute_command') {
1627
+ // Add effective CWD
1628
+ toolArgs.cwd = effectiveCwd;
1629
+ }
1323
1630
  this.onToolExecutionUpdate({
1324
1631
  toolName: toolCall.name,
1325
1632
  status: 'executing',
@@ -1329,6 +1636,31 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1329
1636
  // Log tool execution start
1330
1637
  conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
1331
1638
  // Execute the tool (it will request approval if needed)
1639
+ // SPECIAL: Intercept sub_agent spawn to enforce approval
1640
+ if (toolCall.name === 'sub_agent' && toolCall.arguments?.action === 'spawn') {
1641
+ const approved = await context.requireApproval(`Spawn Sub-Agent`, true, // risky
1642
+ undefined, 'execute_command', { command: `spawn sub-agent` });
1643
+ if (!approved) {
1644
+ // User rejected - log result as error and skip execution
1645
+ conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, 'User rejected');
1646
+ // Notify UI: tool failed
1647
+ if (this.onToolExecutionUpdate) {
1648
+ this.onToolExecutionUpdate({
1649
+ toolName: toolCall.name,
1650
+ status: 'error',
1651
+ error: 'User rejected',
1652
+ arguments: toolCall.arguments
1653
+ });
1654
+ }
1655
+ toolResults.push({
1656
+ tool_call_id: toolCall.id,
1657
+ name: toolCall.name,
1658
+ result: 'User rejected sub-agent spawn request',
1659
+ error: 'User rejected'
1660
+ });
1661
+ continue;
1662
+ }
1663
+ }
1332
1664
  const result = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
1333
1665
  if (result.success) {
1334
1666
  // Log successful tool result
@@ -1442,6 +1774,16 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1442
1774
  }
1443
1775
  }
1444
1776
  }
1777
+ // STOP AGENT LOOP if shell_input was provided
1778
+ // Interactive shell input implies handing control back to the shell/user
1779
+ const hasShellInput = toolCalls.some(tc => tc.name === 'execute_command' && tc.arguments && tc.arguments.shell_input);
1780
+ if (hasShellInput) {
1781
+ try {
1782
+ quickLog(`[${new Date().toISOString()}] [CLI] Input sent to shell. Stopping agent loop to await output.\n`);
1783
+ }
1784
+ catch (e) { }
1785
+ taskCompleted = true;
1786
+ }
1445
1787
  // If task_complete was called, stop the agentic loop immediately
1446
1788
  if (taskCompleted) {
1447
1789
  // Set the final message: use summary if provided, otherwise use the streamed assistantMessage
@@ -1556,8 +1898,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1556
1898
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
1557
1899
  '**This is not allowed.** You must either:\n' +
1558
1900
  '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' +
1901
+ '2. Output your response text, then call task_complete()\n\n' +
1902
+ '**If you have completed the task**, output your summary now, then call task_complete().\n' +
1561
1903
  '**If more work is needed**, execute the next tool call immediately.';
1562
1904
  conversationLogger.logSystemPrompt('silent_stop_prompt', silentStopPrompt);
1563
1905
  this.conversationHistory.push({
@@ -1565,113 +1907,17 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1565
1907
  content: silentStopPrompt,
1566
1908
  });
1567
1909
  }
1568
- // Case 2: Text-only response (Narration or Summary)
1910
+ // Case 2: Text-only response - accept it immediately as final
1569
1911
  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
- }
1912
+ // Log that we're accepting this as a final answer
1913
+ conversationLogger.logNarrationDetection('final_answer', {
1914
+ turn: turnCount,
1915
+ messagePreview: assistantMessage.substring(0, 200),
1916
+ action: 'accepting_immediately',
1917
+ });
1918
+ // Accept the text as the final message and break
1919
+ finalAssistantMessage = assistantMessage;
1920
+ break;
1675
1921
  }
1676
1922
  // Rebuild messages array with updated history
1677
1923
  // Backend will inject system prompt
@@ -1687,8 +1933,8 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1687
1933
  const silentStopPrompt = '⚠️ **SILENT STOP DETECTED**: You ended your turn without any output or tool calls.\n\n' +
1688
1934
  '**This is not allowed.** You must either:\n' +
1689
1935
  '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' +
1936
+ '2. Output your response text, then call task_complete()\n\n' +
1937
+ '**If you have completed the task**, output your summary now, then call task_complete().\n' +
1692
1938
  '**If more work is needed**, execute the next tool call immediately.';
1693
1939
  this.conversationHistory.push({
1694
1940
  role: 'user',
@@ -1741,8 +1987,14 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
1741
1987
  catch (error) {
1742
1988
  // Log the error
1743
1989
  conversationLogger.logError('handleMessage', error);
1744
- // Check if this was an abort/cancellation
1745
- if (error.name === 'AbortError' || error.message?.includes('aborted')) {
1990
+ // Check if this was an abort/cancellation (including timeout errors from aborted requests)
1991
+ if (error.name === 'AbortError' || error.message?.includes('aborted') || error.message?.includes('timed out') || this.requestIntentionallyAborted) {
1992
+ // If intentionally aborted for replacement by new message, return silently
1993
+ // The new message will take over - no need to show cancellation message
1994
+ if (this.requestIntentionallyAborted) {
1995
+ this.requestIntentionallyAborted = false;
1996
+ return;
1997
+ }
1746
1998
  conversationLogger.logError('handleMessage', new Error('Request cancelled by user'));
1747
1999
  if (this.onResponseCallback) {
1748
2000
  this.onResponseCallback('⚠️ Request cancelled by user.');
@@ -2092,7 +2344,7 @@ Start by listing the directory structure to understand what you're working with.
2092
2344
  '• Work silently without narrating actions\n' +
2093
2345
  '• Use Touch-First safety (never guess file paths)\n' +
2094
2346
  '• Apply surgical precision to file edits\n' +
2095
- '• Call task_complete when done with comprehensive summary\n' +
2347
+ '• Output summary text, then call task_complete() when done\n' +
2096
2348
  '• Inject intelligent error recovery hints\n\n' +
2097
2349
  'This is the industry-standard autonomous agent mode.'
2098
2350
  : '⚠️ Autonomous Mode disabled\n\n' +
@@ -2154,43 +2406,126 @@ Start by listing the directory structure to understand what you're working with.
2154
2406
  `Model: ${config.model || 'gemini-2.5-flash (default)'}\n` +
2155
2407
  `Enhanced Quality: ${config.enhancedQuality !== false ? '✅ Enabled' : '❌ Disabled'}\n` +
2156
2408
  `Autonomous Mode: ${config.autonomousMode === true ? '✅ Enabled' : '❌ Disabled'}\n` +
2409
+ `AI Auto-Suggest: ${config.aiAutoSuggest === true ? '✅ Enabled' : '❌ Disabled'}\n` +
2157
2410
  `Authentication: ${apiClient.isAuthenticated() ? '✅ Signed in' : '❌ Not signed in'}`;
2158
2411
  }
2159
2412
  break;
2413
+ case 'settings':
2414
+ if (args.length >= 2 && args[0].toLowerCase() === 'auto-suggest') {
2415
+ // Handle /settings auto-suggest <on/off>
2416
+ const value = args[1].toLowerCase();
2417
+ if (value === 'on') {
2418
+ this.configManager.set('aiAutoSuggest', true);
2419
+ responseMessage = '✅ **AI Auto-Suggestions Enabled**\n\n' +
2420
+ 'From now on, I will suggest commands after 5 seconds of inactivity.\n' +
2421
+ 'Suggestions will appear in grey text. Use the **Right Arrow** key to accept them.';
2422
+ }
2423
+ else if (value === 'off') {
2424
+ this.configManager.set('aiAutoSuggest', false);
2425
+ responseMessage = '✅ **AI Auto-Suggestions Disabled**\n\n' +
2426
+ 'I will no longer provide AI-powered command suggestions.';
2427
+ }
2428
+ else {
2429
+ responseMessage = '❌ Invalid option. Usage: `/settings auto-suggest on` or `/settings auto-suggest off`';
2430
+ }
2431
+ }
2432
+ else {
2433
+ responseMessage = '❌ Invalid command format.\n\nUsage:\n- `/settings auto-suggest on`\n- `/settings auto-suggest off`';
2434
+ }
2435
+ break;
2160
2436
  case 'model':
2161
- if (args.length === 0) {
2162
- // Show picker for model selection
2437
+ case 'models':
2438
+ // Handle subcommands: local, cloud
2439
+ const modelSubCommand = args[0]?.toLowerCase();
2440
+ if (modelSubCommand === 'local') {
2441
+ // Local Ollama models
2442
+ try {
2443
+ // Check if Ollama is running
2444
+ const status = await ollamaService.isOllamaRunning();
2445
+ if (!status.available) {
2446
+ responseMessage = `❌ Cannot connect to Ollama
2447
+
2448
+ ${status.error || 'Ollama is not running.'}
2449
+
2450
+ To use local models:
2451
+ 1. Install Ollama from: https://ollama.ai
2452
+ 2. Start Ollama by running: ollama serve
2453
+ 3. Pull a model: ollama pull llama3
2454
+
2455
+ Then try /models local again.`;
2456
+ break;
2457
+ }
2458
+ // Get available local models
2459
+ const localModels = await ollamaService.getLocalModels();
2460
+ if (localModels.length === 0) {
2461
+ responseMessage = `📭 No local models found
2462
+
2463
+ Ollama is running (v${status.version}) but no models are downloaded.
2464
+
2465
+ To download models, run:
2466
+ ollama pull llama3
2467
+ ollama pull codellama
2468
+ ollama pull mistral
2469
+
2470
+ Then try /models local again.`;
2471
+ break;
2472
+ }
2473
+ // Show picker for local model selection
2474
+ if (this.onShowPickerCallback) {
2475
+ const config = this.configManager.load();
2476
+ const currentModelName = config.modelName || '';
2477
+ const isCurrentLocal = config.isLocalModel === true;
2478
+ this.onShowPickerCallback({
2479
+ message: 'Select Local Model (Ollama)',
2480
+ type: 'local-model', // Cast to bypass type check, will be handled in handlePickerSelection
2481
+ choices: localModels.map((model) => {
2482
+ const size = OllamaService.formatModelSize(model.size);
2483
+ const isCurrent = isCurrentLocal && currentModelName === model.name;
2484
+ const supportsTools = OllamaService.modelSupportsTools(model.name);
2485
+ const toolsBadge = supportsTools ? ' [Tools]' : '';
2486
+ return {
2487
+ label: `${model.name} (${size})${toolsBadge}${isCurrent ? ' [CURRENT]' : ''}`,
2488
+ value: model.name
2489
+ };
2490
+ })
2491
+ });
2492
+ return; // Don't send a text response, picker will handle it
2493
+ }
2494
+ }
2495
+ catch (error) {
2496
+ responseMessage = OllamaService.getHelpfulErrorMessage(error);
2497
+ }
2498
+ break;
2499
+ }
2500
+ if (modelSubCommand === 'cloud' || args.length === 0) {
2501
+ // Cloud models (default behavior when no subcommand or 'cloud' specified)
2163
2502
  if (this.onShowPickerCallback) {
2164
2503
  const config = this.configManager.load();
2165
2504
  const currentModelName = config.modelName || '';
2505
+ const isCurrentCloud = config.isLocalModel !== true;
2166
2506
  // Fetch models from backend
2167
2507
  const modelsConfig = await fetchModelsConfig();
2168
2508
  this.onShowPickerCallback({
2169
- message: 'Select Model',
2509
+ message: '☁️ Select Cloud Model',
2170
2510
  type: 'model',
2171
- choices: modelsConfig.models.map((modelConfig, index) => ({
2172
- label: `${modelConfig.name} - ${modelConfig.description}${currentModelName === modelConfig.name ? ' [CURRENT]' : ''}`,
2173
- value: `${index}` // Use index as unique identifier
2174
- }))
2511
+ choices: modelsConfig.models.map((modelConfig, index) => {
2512
+ const isCurrent = isCurrentCloud && currentModelName === modelConfig.name;
2513
+ return {
2514
+ label: `${modelConfig.name} - ${modelConfig.description}${isCurrent ? ' [CURRENT]' : ''}`,
2515
+ value: `${index}` // Use index as unique identifier
2516
+ };
2517
+ })
2175
2518
  });
2176
2519
  return; // Don't send a text response, picker will handle it
2177
2520
  }
2178
2521
  }
2179
2522
  else {
2180
- // Direct set via command argument
2181
- const newModel = args.join(' ');
2182
- // Validate model (async)
2183
- if (!(await isValidModel(newModel))) {
2184
- responseMessage = `❌ ${await getInvalidModelError(newModel)}`;
2185
- break;
2186
- }
2187
- try {
2188
- this.configManager.set('model', newModel);
2189
- responseMessage = `✅ Model changed to: ${newModel}`;
2190
- }
2191
- catch (error) {
2192
- responseMessage = `❌ Failed to set model: ${error.message}`;
2193
- }
2523
+ // Unrecognized subcommand - show help
2524
+ responseMessage = `Usage: /models [local|cloud]
2525
+
2526
+ /models local - Select from locally installed Ollama models
2527
+ /models cloud - Select from cloud models (Centaurus backend)
2528
+ /models - Default: show cloud models`;
2194
2529
  }
2195
2530
  break;
2196
2531
  case 'mcp':
@@ -2203,7 +2538,7 @@ Start by listing the directory structure to understand what you're working with.
2203
2538
  break;
2204
2539
  case 'docs':
2205
2540
  // Open documentation URL in default browser
2206
- const docsUrl = 'https://www.centauruslabs.in/docs';
2541
+ const docsUrl = 'https://centauruslabs.in/docs';
2207
2542
  const { exec } = await import('child_process');
2208
2543
  const platform = process.platform;
2209
2544
  if (platform === 'win32') {
@@ -3034,6 +3369,23 @@ Start by listing the directory structure to understand what you're working with.
3034
3369
  this.onCwdChange(chat.cwd);
3035
3370
  }
3036
3371
  }
3372
+ // Reset context limit state when loading a chat
3373
+ // We'll recalculate it based on the loaded conversation
3374
+ if (this.contextLimitReached) {
3375
+ this.contextLimitReached = false;
3376
+ if (this.onContextLimitReached) {
3377
+ this.onContextLimitReached(false);
3378
+ }
3379
+ }
3380
+ // Update token count to reflect loaded conversation
3381
+ // This will also check if the loaded chat is near the limit
3382
+ this.updateTokenCount().catch(err => {
3383
+ quickLog(`[${new Date().toISOString()}] [loadChatFromPicker] Failed to update token count: ${err}\n`);
3384
+ });
3385
+ // Clean up any orphaned tool calls from previous interrupted sessions
3386
+ // This prevents "improperly formed request" errors when continuing conversations
3387
+ this.cleanupOrphanedToolCalls();
3388
+ quickLog(`[${new Date().toISOString()}] [loadChat] Cleaned up conversation history after load\n`);
3037
3389
  return true;
3038
3390
  }
3039
3391
  /**
@@ -3202,6 +3554,17 @@ Start by listing the directory structure to understand what you're working with.
3202
3554
  this.uiMessageHistory = [];
3203
3555
  this.localCwdBeforeRemote = null;
3204
3556
  this.lastConnectionCommand = null;
3557
+ // Reset context limit state
3558
+ if (this.contextLimitReached) {
3559
+ this.contextLimitReached = false;
3560
+ if (this.onContextLimitReached) {
3561
+ this.onContextLimitReached(false);
3562
+ }
3563
+ }
3564
+ // Update token count to reflect empty conversation
3565
+ this.updateTokenCount().catch(err => {
3566
+ quickLog(`[${new Date().toISOString()}] [startNewChat] Failed to update token count: ${err}\n`);
3567
+ });
3205
3568
  }
3206
3569
  /**
3207
3570
  * Update UI message history (called from App.tsx via callback)