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.
- package/dist/cli-adapter.d.ts +76 -3
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +593 -230
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/mcp-config-manager.d.ts +21 -0
- package/dist/config/mcp-config-manager.d.ts.map +1 -1
- package/dist/config/mcp-config-manager.js +184 -1
- package/dist/config/mcp-config-manager.js.map +1 -1
- package/dist/config/models.d.ts +1 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +9 -2
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.d.ts +3 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +39 -4
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/index.js +60 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp/mcp-command-handler.d.ts +34 -3
- package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
- package/dist/mcp/mcp-command-handler.js +171 -83
- package/dist/mcp/mcp-command-handler.js.map +1 -1
- package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
- package/dist/mcp/mcp-server-manager.js +9 -23
- package/dist/mcp/mcp-server-manager.js.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.js +42 -5
- package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
- package/dist/services/ai-autocomplete-agent.d.ts +39 -0
- package/dist/services/ai-autocomplete-agent.d.ts.map +1 -0
- package/dist/services/ai-autocomplete-agent.js +189 -0
- package/dist/services/ai-autocomplete-agent.js.map +1 -0
- package/dist/services/ai-service-client.d.ts +25 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +162 -1
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.d.ts +9 -0
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +25 -0
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/auth-handler.js +1 -1
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/input-detection-agent.d.ts +40 -0
- package/dist/services/input-detection-agent.d.ts.map +1 -0
- package/dist/services/input-detection-agent.js +213 -0
- package/dist/services/input-detection-agent.js.map +1 -0
- package/dist/services/input-requirement-detector.d.ts +28 -0
- package/dist/services/input-requirement-detector.d.ts.map +1 -0
- package/dist/services/input-requirement-detector.js +203 -0
- package/dist/services/input-requirement-detector.js.map +1 -0
- package/dist/services/local-chat-storage.d.ts +21 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -1
- package/dist/services/local-chat-storage.js +138 -43
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/monitored-shell-manager.d.ts +120 -0
- package/dist/services/monitored-shell-manager.d.ts.map +1 -0
- package/dist/services/monitored-shell-manager.js +239 -0
- package/dist/services/monitored-shell-manager.js.map +1 -0
- package/dist/services/ollama-service.d.ts +197 -0
- package/dist/services/ollama-service.d.ts.map +1 -0
- package/dist/services/ollama-service.js +324 -0
- package/dist/services/ollama-service.js.map +1 -0
- package/dist/services/shell-input-agent.d.ts +89 -0
- package/dist/services/shell-input-agent.d.ts.map +1 -0
- package/dist/services/shell-input-agent.js +361 -0
- package/dist/services/shell-input-agent.js.map +1 -0
- package/dist/services/sub-agent-manager.d.ts +139 -0
- package/dist/services/sub-agent-manager.d.ts.map +1 -0
- package/dist/services/sub-agent-manager.js +517 -0
- package/dist/services/sub-agent-manager.js.map +1 -0
- package/dist/tools/background-command.d.ts.map +1 -1
- package/dist/tools/background-command.js +33 -13
- package/dist/tools/background-command.js.map +1 -1
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +64 -1
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +33 -19
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/get-diff.js +1 -1
- package/dist/tools/get-diff.js.map +1 -1
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +41 -15
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/plan-mode.js +3 -3
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/registry.js +1 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/sub-agent.d.ts +9 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +232 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-complete.d.ts.map +1 -1
- package/dist/tools/task-complete.js +14 -26
- package/dist/tools/task-complete.js.map +1 -1
- package/dist/ui/components/App.d.ts +45 -2
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +605 -96
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/CircularSelectInput.d.ts +24 -0
- package/dist/ui/components/CircularSelectInput.d.ts.map +1 -0
- package/dist/ui/components/CircularSelectInput.js +71 -0
- package/dist/ui/components/CircularSelectInput.js.map +1 -0
- package/dist/ui/components/ErrorBoundary.d.ts +3 -2
- package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/ui/components/ErrorBoundary.js +29 -1
- package/dist/ui/components/ErrorBoundary.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +4 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +343 -21
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts +6 -0
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +57 -6
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MCPAddScreen.d.ts +13 -0
- package/dist/ui/components/MCPAddScreen.d.ts.map +1 -0
- package/dist/ui/components/MCPAddScreen.js +54 -0
- package/dist/ui/components/MCPAddScreen.js.map +1 -0
- package/dist/ui/components/MCPListScreen.d.ts +17 -0
- package/dist/ui/components/MCPListScreen.d.ts.map +1 -0
- package/dist/ui/components/MCPListScreen.js +50 -0
- package/dist/ui/components/MCPListScreen.js.map +1 -0
- package/dist/ui/components/MCPServerListScreen.d.ts +16 -0
- package/dist/ui/components/MCPServerListScreen.d.ts.map +1 -0
- package/dist/ui/components/MCPServerListScreen.js +59 -0
- package/dist/ui/components/MCPServerListScreen.js.map +1 -0
- package/dist/ui/components/MonitorModeAIPanel.d.ts +23 -0
- package/dist/ui/components/MonitorModeAIPanel.d.ts.map +1 -0
- package/dist/ui/components/MonitorModeAIPanel.js +69 -0
- package/dist/ui/components/MonitorModeAIPanel.js.map +1 -0
- package/dist/ui/components/MultiLineInput.d.ts +13 -0
- package/dist/ui/components/MultiLineInput.d.ts.map +1 -0
- package/dist/ui/components/MultiLineInput.js +289 -0
- package/dist/ui/components/MultiLineInput.js.map +1 -0
- package/dist/ui/components/StatusBar.d.ts +2 -0
- package/dist/ui/components/StatusBar.d.ts.map +1 -1
- package/dist/ui/components/StatusBar.js +33 -2
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +231 -13
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.js +3 -2
- package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
- package/dist/utils/command-history.d.ts +12 -2
- package/dist/utils/command-history.d.ts.map +1 -1
- package/dist/utils/command-history.js +57 -13
- package/dist/utils/command-history.js.map +1 -1
- package/dist/utils/input-classifier.js +1 -1
- package/dist/utils/input-classifier.js.map +1 -1
- package/package.json +2 -1
package/dist/cli-adapter.js
CHANGED
|
@@ -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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
477
|
-
*
|
|
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
|
-
|
|
483
|
-
let
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
503
|
-
if (toolCallIds.size > 0) {
|
|
504
|
-
// Log the cleanup for debugging
|
|
744
|
+
if (cleanedAny) {
|
|
505
745
|
try {
|
|
506
|
-
quickLog(`[${new Date().toISOString()}] [CLI]
|
|
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
|
-
|
|
569
|
-
this.
|
|
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
|
|
667
|
-
|
|
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
|
-
//
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
-
|
|
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!
|
|
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!
|
|
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
|
|
1622
|
+
// Notify UI: tool executing
|
|
1318
1623
|
if (this.onToolExecutionUpdate) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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.
|
|
1560
|
-
'**If you have completed the task**,
|
|
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
|
|
1910
|
+
// Case 2: Text-only response - accept it immediately as final
|
|
1569
1911
|
else {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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.
|
|
1691
|
-
'**If you have completed the task**,
|
|
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
|
-
'•
|
|
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
|
-
|
|
2162
|
-
|
|
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
|
-
|
|
2173
|
-
|
|
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
|
-
//
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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://
|
|
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)
|