centaurus-cli 3.1.2 → 3.1.4
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.js +689 -155
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/defaultConfig.js +1 -4
- package/dist/config/defaultConfig.js.map +1 -1
- package/dist/config/models.js +6 -0
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.js +66 -2
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/config/types.js +4 -4
- package/dist/config/types.js.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -1
- package/dist/services/ai-context-injector.js +109 -0
- package/dist/services/ai-context-injector.js.map +1 -1
- package/dist/services/ai-service-client.js +3 -2
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/background-task-manager.js +59 -0
- package/dist/services/background-task-manager.js.map +1 -1
- package/dist/services/local-chat-storage.js +2 -0
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/skill-storage.js +141 -0
- package/dist/services/skill-storage.js.map +1 -0
- package/dist/services/sub-agent-manager.js +49 -8
- package/dist/services/sub-agent-manager.js.map +1 -1
- package/dist/services/warpify-detector.js +17 -5
- package/dist/services/warpify-detector.js.map +1 -1
- package/dist/tools/background-command.js +5 -2
- package/dist/tools/background-command.js.map +1 -1
- package/dist/tools/command.js +367 -109
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.js +23 -6
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/plan-mode.js +184 -336
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/sub-agent.js +24 -5
- package/dist/tools/sub-agent.js.map +1 -1
- package/dist/tools/todo-list.js +157 -0
- package/dist/tools/todo-list.js.map +1 -0
- package/dist/types/skill.js +30 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/ui/components/App.js +956 -162
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/AuthScreen.js +3 -1
- package/dist/ui/components/AuthScreen.js.map +1 -1
- package/dist/ui/components/AuthWelcomeScreen.js +3 -1
- package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
- package/dist/ui/components/CodeBlock.js +3 -1
- package/dist/ui/components/CodeBlock.js.map +1 -1
- package/dist/ui/components/CompactShellPreview.js +44 -0
- package/dist/ui/components/CompactShellPreview.js.map +1 -0
- package/dist/ui/components/ConfigViewer.js +3 -1
- package/dist/ui/components/ConfigViewer.js.map +1 -1
- package/dist/ui/components/ConfirmPrompt.js +3 -1
- package/dist/ui/components/ConfirmPrompt.js.map +1 -1
- package/dist/ui/components/ConnectionStatusMessage.js +3 -1
- package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
- package/dist/ui/components/DetailedPlanReviewScreen.js +84 -74
- package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
- package/dist/ui/components/DiffViewer.js +6 -3
- package/dist/ui/components/DiffViewer.js.map +1 -1
- package/dist/ui/components/FileCreationPreview.js.map +1 -1
- package/dist/ui/components/FileTagAutocomplete.js +4 -2
- package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
- package/dist/ui/components/InputBox.js +243 -40
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.js +5 -3
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/KeyboardHelp.js +4 -1
- package/dist/ui/components/KeyboardHelp.js.map +1 -1
- package/dist/ui/components/LoadingIndicator.js +3 -1
- package/dist/ui/components/LoadingIndicator.js.map +1 -1
- package/dist/ui/components/MCPAddScreen.js +63 -13
- package/dist/ui/components/MCPAddScreen.js.map +1 -1
- package/dist/ui/components/MarkdownRenderer.js +3 -1
- package/dist/ui/components/MarkdownRenderer.js.map +1 -1
- package/dist/ui/components/MessageDisplay.js +9 -7
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/ModelPicker.js +170 -0
- package/dist/ui/components/ModelPicker.js.map +1 -0
- package/dist/ui/components/MonitorModeAIPanel.js +3 -1
- package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.js +12 -6
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
- package/dist/ui/components/PlanQuestionMessage.js +37 -0
- package/dist/ui/components/PlanQuestionMessage.js.map +1 -0
- package/dist/ui/components/PlanQuestionScreen.js +138 -0
- package/dist/ui/components/PlanQuestionScreen.js.map +1 -0
- package/dist/ui/components/PlanReviewScreen.js +7 -9
- package/dist/ui/components/PlanReviewScreen.js.map +1 -1
- package/dist/ui/components/RulesEditorScreen.js +65 -28
- package/dist/ui/components/RulesEditorScreen.js.map +1 -1
- package/dist/ui/components/SelectPrompt.js +3 -1
- package/dist/ui/components/SelectPrompt.js.map +1 -1
- package/dist/ui/components/SkillCreatorScreen.js +217 -0
- package/dist/ui/components/SkillCreatorScreen.js.map +1 -0
- package/dist/ui/components/SlashCommandAutocomplete.js +4 -2
- package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
- package/dist/ui/components/StatusBar.js +4 -2
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +5 -3
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/SubAgentListScreen.js +65 -0
- package/dist/ui/components/SubAgentListScreen.js.map +1 -0
- package/dist/ui/components/SubAgentViewScreen.js +123 -0
- package/dist/ui/components/SubAgentViewScreen.js.map +1 -0
- package/dist/ui/components/TaskCompletedMessage.js +40 -8
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
- package/dist/ui/components/TaskProgressIndicator.js +6 -4
- package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
- package/dist/ui/components/TextEditor.js +297 -0
- package/dist/ui/components/TextEditor.js.map +1 -0
- package/dist/ui/components/TodoListMessage.js +59 -0
- package/dist/ui/components/TodoListMessage.js.map +1 -0
- package/dist/ui/components/ToolExecutionMessage.js +134 -84
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/ToolExecutionStatus.js +3 -1
- package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
- package/dist/ui/components/WelcomeBanner.js +33 -33
- package/dist/ui/components/WelcomeBanner.js.map +1 -1
- package/dist/ui/components/WorkflowCreatorScreen.js +5 -3
- package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
- package/dist/ui/theme.js +97 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/utils/chat-history-limit.js +247 -0
- package/dist/ui/utils/chat-history-limit.js.map +1 -0
- package/dist/utils/chat-formatter.js +22 -9
- package/dist/utils/chat-formatter.js.map +1 -1
- package/dist/utils/git-stats.js +7 -5
- package/dist/utils/git-stats.js.map +1 -1
- package/dist/utils/input-classifier.js +11 -1
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/output-truncation.js +175 -0
- package/dist/utils/output-truncation.js.map +1 -0
- package/dist/utils/rule-reference-resolver.js +3 -3
- package/dist/utils/rule-reference-resolver.js.map +1 -1
- package/dist/utils/tunnel-commands-manager.js +134 -0
- package/dist/utils/tunnel-commands-manager.js.map +1 -0
- package/package.json +91 -90
- package/postinstall.js +4 -11
- package/dist/ui/components/MultiLineInput.js +0 -255
- package/dist/ui/components/MultiLineInput.js.map +0 -1
package/dist/cli-adapter.js
CHANGED
|
@@ -4,17 +4,19 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
import { dirname } from "path";
|
|
5
5
|
import * as shellUtils from "./utils/shell.js";
|
|
6
6
|
import { sanitizeForContext } from "./utils/context-sanitizer.js";
|
|
7
|
+
import { truncateToolOutput, cleanupOrphanedToolOutputs, cleanupStaleToolOutputs } from "./utils/output-truncation.js";
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = dirname(__filename);
|
|
9
10
|
import { ConfigManager } from "./config/manager.js";
|
|
10
11
|
import { ToolRegistry } from "./tools/registry.js";
|
|
11
12
|
import { viewFileTool, writeToFileTool, editFileTool, listDirTool, multiEditFileTool } from "./tools/file-ops.js";
|
|
12
|
-
import { runCommandTool } from "./tools/command.js";
|
|
13
|
+
import { runCommandTool, resolvePostCommandCwd } from "./tools/command.js";
|
|
13
14
|
import { grepSearchTool } from "./tools/grep-search.js";
|
|
14
15
|
import { findFilesTool } from "./tools/find-files.js";
|
|
15
16
|
import { getDiffTool } from "./tools/get-diff.js";
|
|
16
17
|
import { inspectSymbolTool } from "./tools/inspect-symbol.js";
|
|
17
|
-
import { createPlanTool, markTaskCompleteTool, getCurrentPlan, clearPlan, approvePlan,
|
|
18
|
+
import { createPlanTool, markTaskCompleteTool, planAskQuestionTool, getCurrentPlan, clearPlan, approvePlan, getPlanContextForPrompt } from "./tools/plan-mode.js";
|
|
19
|
+
import { todoWriteTool, getTodoContextForPrompt } from "./tools/todo-list.js";
|
|
18
20
|
import { webSearchTool, fetchUrlTool } from "./tools/web-search.js";
|
|
19
21
|
import { taskCompleteTool } from "./tools/task-complete.js";
|
|
20
22
|
import { readBinaryFileTool } from "./tools/read-binary-file.js";
|
|
@@ -30,7 +32,7 @@ import { ShellInputAgent } from "./services/shell-input-agent.js";
|
|
|
30
32
|
import { apiClient } from "./services/api-client.js";
|
|
31
33
|
import { conversationManager } from "./services/conversation-manager.js";
|
|
32
34
|
import { aiServiceClient } from "./services/ai-service-client.js";
|
|
33
|
-
import { isValidModel, getInvalidModelError, fetchModelsConfig, getModelConfigByIdAndName } from "./config/models.js";
|
|
35
|
+
import { isValidModel, getInvalidModelError, fetchModelsConfig, clearModelsCache, getModelConfigByIdAndName } from "./config/models.js";
|
|
34
36
|
import { authenticateWithGoogle } from "./services/auth-handler.js";
|
|
35
37
|
import { ContextManager } from "./context/context-manager.js";
|
|
36
38
|
import { CommandDetector } from "./context/command-detector.js";
|
|
@@ -52,6 +54,8 @@ import { workflowStorage } from "./services/workflow-storage.js";
|
|
|
52
54
|
import { CheckpointManager } from "./services/checkpoint-manager.js";
|
|
53
55
|
import { rulesStorage } from "./services/rules-storage.js";
|
|
54
56
|
import { resolveRuleReferences } from "./utils/rule-reference-resolver.js";
|
|
57
|
+
import { THEME_PALETTE, setGlobalAccentColor } from "./ui/theme.js";
|
|
58
|
+
import { skillStorage } from "./services/skill-storage.js";
|
|
55
59
|
class CentaurusCLI {
|
|
56
60
|
configManager;
|
|
57
61
|
toolRegistry;
|
|
@@ -84,9 +88,13 @@ class CentaurusCLI {
|
|
|
84
88
|
onPlanModeChange;
|
|
85
89
|
onPlanApprovalRequest;
|
|
86
90
|
onPlanCreated;
|
|
91
|
+
onPlanQuestionRequest;
|
|
87
92
|
onTaskCompleted;
|
|
93
|
+
onTodoListUpdate;
|
|
88
94
|
onPasswordRequest;
|
|
89
95
|
currentInteractiveProcess;
|
|
96
|
+
tunnelOutputBuffer = "";
|
|
97
|
+
// Accumulated PTY output for custom tunnel capture
|
|
90
98
|
conversationStarted = false;
|
|
91
99
|
contextManager;
|
|
92
100
|
commandDetector;
|
|
@@ -144,6 +152,14 @@ class CentaurusCLI {
|
|
|
144
152
|
// Callback to show the rules editor
|
|
145
153
|
onAiAutoSuggestChange;
|
|
146
154
|
// Callback for AI auto-suggest setting changes
|
|
155
|
+
onLimitChatHistoryChange;
|
|
156
|
+
onThemeColorChange = null;
|
|
157
|
+
customTunnelCommand = null;
|
|
158
|
+
// Active custom tunnel command (e.g. "minicom")
|
|
159
|
+
onSubAgentListScreenSetup = null;
|
|
160
|
+
onSubAgentViewScreenSetup = null;
|
|
161
|
+
onSubAgentTerminateScreenSetup = null;
|
|
162
|
+
onSkillEditorScreenSetup = null;
|
|
147
163
|
onRevertToCheckpointCallback;
|
|
148
164
|
// Callback for revert UI update
|
|
149
165
|
static AUTO_COMPACTION_TRIGGER_PERCENT = 80;
|
|
@@ -243,9 +259,15 @@ class CentaurusCLI {
|
|
|
243
259
|
setOnPlanCreated(callback) {
|
|
244
260
|
this.onPlanCreated = callback;
|
|
245
261
|
}
|
|
262
|
+
setOnPlanQuestionRequest(callback) {
|
|
263
|
+
this.onPlanQuestionRequest = callback;
|
|
264
|
+
}
|
|
246
265
|
setOnTaskCompleted(callback) {
|
|
247
266
|
this.onTaskCompleted = callback;
|
|
248
267
|
}
|
|
268
|
+
setOnTodoListUpdate(callback) {
|
|
269
|
+
this.onTodoListUpdate = callback;
|
|
270
|
+
}
|
|
249
271
|
setOnCommandModeChange(callback) {
|
|
250
272
|
this.onCommandModeChange = callback;
|
|
251
273
|
}
|
|
@@ -307,6 +329,82 @@ class CentaurusCLI {
|
|
|
307
329
|
setOnAiAutoSuggestChange(callback) {
|
|
308
330
|
this.onAiAutoSuggestChange = callback;
|
|
309
331
|
}
|
|
332
|
+
setOnLimitChatHistoryChange(callback) {
|
|
333
|
+
this.onLimitChatHistoryChange = callback;
|
|
334
|
+
}
|
|
335
|
+
setOnThemeColorChange(callback) {
|
|
336
|
+
this.onThemeColorChange = callback;
|
|
337
|
+
}
|
|
338
|
+
setCustomTunnelCommand(command) {
|
|
339
|
+
this.customTunnelCommand = command;
|
|
340
|
+
if (command) {
|
|
341
|
+
this.tunnelOutputBuffer = "";
|
|
342
|
+
}
|
|
343
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [CLI] Custom tunnel state: ${command || "none"}
|
|
344
|
+
`);
|
|
345
|
+
}
|
|
346
|
+
isCustomTunnelActive() {
|
|
347
|
+
return this.customTunnelCommand !== null;
|
|
348
|
+
}
|
|
349
|
+
getCustomTunnelCommand() {
|
|
350
|
+
return this.customTunnelCommand;
|
|
351
|
+
}
|
|
352
|
+
setOnSubAgentListScreenSetup(callback) {
|
|
353
|
+
this.onSubAgentListScreenSetup = callback;
|
|
354
|
+
}
|
|
355
|
+
setOnSubAgentViewScreenSetup(callback) {
|
|
356
|
+
this.onSubAgentViewScreenSetup = callback;
|
|
357
|
+
}
|
|
358
|
+
setOnSubAgentTerminateScreenSetup(callback) {
|
|
359
|
+
this.onSubAgentTerminateScreenSetup = callback;
|
|
360
|
+
}
|
|
361
|
+
setOnSkillEditorScreenSetup(callback) {
|
|
362
|
+
this.onSkillEditorScreenSetup = callback;
|
|
363
|
+
}
|
|
364
|
+
saveSkill(skill, previousName) {
|
|
365
|
+
return skillStorage.save(skill, previousName);
|
|
366
|
+
}
|
|
367
|
+
/** Execute a skill by name with user-provided parameters. Called from the UI when user types #skill-name. */
|
|
368
|
+
async executeSkill(skillName, params) {
|
|
369
|
+
const skill = skillStorage.load(skillName);
|
|
370
|
+
if (!skill) {
|
|
371
|
+
return `\u274C Skill "${skillName}" not found.`;
|
|
372
|
+
}
|
|
373
|
+
const cwd = this.cwd;
|
|
374
|
+
const result = await SubAgentManager.spawnSubAgent({
|
|
375
|
+
prompt: `${skill.prompt}
|
|
376
|
+
|
|
377
|
+
User parameters: ${params || "(none)"}
|
|
378
|
+
|
|
379
|
+
IMPORTANT: Your working directory is ${cwd}. Create all files and perform all operations ONLY within this directory. Do NOT use any other directory.`,
|
|
380
|
+
context: `This agent was invoked via the #${skill.name} skill.
|
|
381
|
+
Working directory: ${cwd}`,
|
|
382
|
+
workingDirectory: cwd,
|
|
383
|
+
complexity: 5,
|
|
384
|
+
allowedTools: skill.allowedTools,
|
|
385
|
+
modelOverride: skill.model || void 0
|
|
386
|
+
});
|
|
387
|
+
if (result.success) {
|
|
388
|
+
const msg = `\u{1F680} **Skill "${skill.name}" started**
|
|
389
|
+
|
|
390
|
+
**Agent ID:** ${result.agentId}
|
|
391
|
+
**Access:** ${skill.accessLevel}${skill.model ? `
|
|
392
|
+
**Model:** ${skill.model}` : ""}
|
|
393
|
+
**Parameters:** ${params || "(none)"}
|
|
394
|
+
|
|
395
|
+
The skill is running as a sub-agent. Use \`/sub-agent list\` to monitor.`;
|
|
396
|
+
if (this.onResponseCallback) {
|
|
397
|
+
this.onResponseCallback(msg);
|
|
398
|
+
}
|
|
399
|
+
return msg;
|
|
400
|
+
} else {
|
|
401
|
+
const msg = `\u274C Failed to start skill "${skill.name}": ${result.error}`;
|
|
402
|
+
if (this.onResponseCallback) {
|
|
403
|
+
this.onResponseCallback(msg);
|
|
404
|
+
}
|
|
405
|
+
return msg;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
310
408
|
setOnRevertToCheckpoint(callback) {
|
|
311
409
|
this.onRevertToCheckpointCallback = callback;
|
|
312
410
|
}
|
|
@@ -588,6 +686,11 @@ Begin executing now, starting with Step 1.`;
|
|
|
588
686
|
this.onCwdChange(this.cwd);
|
|
589
687
|
}
|
|
590
688
|
this.saveCurrentChat();
|
|
689
|
+
if (this.customTunnelCommand) {
|
|
690
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ExitRemoteSession] Clearing custom tunnel: ${this.customTunnelCommand}
|
|
691
|
+
`);
|
|
692
|
+
this.customTunnelCommand = null;
|
|
693
|
+
}
|
|
591
694
|
if (this.onConnectionStatusUpdate) {
|
|
592
695
|
if (newContext.type === "local") {
|
|
593
696
|
this.onConnectionStatusUpdate({
|
|
@@ -628,6 +731,11 @@ Begin executing now, starting with Step 1.`;
|
|
|
628
731
|
this.onCwdChange(this.cwd);
|
|
629
732
|
}
|
|
630
733
|
this.saveCurrentChat();
|
|
734
|
+
if (this.customTunnelCommand) {
|
|
735
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [CLI] Clearing custom tunnel on remote disconnect: ${this.customTunnelCommand}
|
|
736
|
+
`);
|
|
737
|
+
this.customTunnelCommand = null;
|
|
738
|
+
}
|
|
631
739
|
if (this.onConnectionStatusUpdate) {
|
|
632
740
|
if (currentContext.type === "local") {
|
|
633
741
|
this.onConnectionStatusUpdate({
|
|
@@ -944,6 +1052,7 @@ Begin executing now, starting with Step 1.`;
|
|
|
944
1052
|
this.onResponseCallback(responseMessage);
|
|
945
1053
|
}
|
|
946
1054
|
} else {
|
|
1055
|
+
clearModelsCache();
|
|
947
1056
|
const modelsConfig = await fetchModelsConfig();
|
|
948
1057
|
const modelIndex = parseInt(selection, 10);
|
|
949
1058
|
if (isNaN(modelIndex) || modelIndex < 0 || modelIndex >= modelsConfig.models.length) {
|
|
@@ -951,6 +1060,7 @@ Begin executing now, starting with Step 1.`;
|
|
|
951
1060
|
}
|
|
952
1061
|
const selectedModel = modelsConfig.models[modelIndex];
|
|
953
1062
|
this.configManager.set("model", selectedModel.id);
|
|
1063
|
+
this.configManager.set("modelUid", selectedModel.uid);
|
|
954
1064
|
this.configManager.set("modelName", selectedModel.name);
|
|
955
1065
|
this.configManager.set("isLocalModel", false);
|
|
956
1066
|
if (this.onModelChange) {
|
|
@@ -1031,18 +1141,22 @@ Begin executing now, starting with Step 1.`;
|
|
|
1031
1141
|
});
|
|
1032
1142
|
}
|
|
1033
1143
|
/**
|
|
1034
|
-
* Truncate large results for AI consumption
|
|
1144
|
+
* Truncate large results for AI consumption.
|
|
1145
|
+
* Uses head+tail truncation and saves full output to a file for AI retrieval.
|
|
1146
|
+
* Detects remote sessions (SSH/Docker/WSL) to add Environment="local" hints
|
|
1147
|
+
* in the truncation message so the AI can read the locally-saved output file.
|
|
1035
1148
|
*/
|
|
1036
|
-
truncateResult(result,
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1149
|
+
truncateResult(result, toolName) {
|
|
1150
|
+
const currentContext = this.contextManager.getCurrentContext();
|
|
1151
|
+
const isRemote = currentContext.type !== "local";
|
|
1152
|
+
return truncateToolOutput(
|
|
1153
|
+
toolName || "unknown",
|
|
1154
|
+
result,
|
|
1155
|
+
this.currentChatId,
|
|
1156
|
+
void 0,
|
|
1157
|
+
// use default threshold
|
|
1158
|
+
isRemote
|
|
1159
|
+
);
|
|
1046
1160
|
}
|
|
1047
1161
|
calculateUsagePercent(tokenCount, maxTokens) {
|
|
1048
1162
|
if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
|
|
@@ -1274,7 +1388,68 @@ ${CentaurusCLI.TERMINAL_COMPACTION_NOTICE}`;
|
|
|
1274
1388
|
};
|
|
1275
1389
|
}
|
|
1276
1390
|
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Force-compact ALL eligible candidates (read_file results, terminal outputs, shell outputs)
|
|
1393
|
+
* without any usage threshold or target limit. Called when user runs /compact manually.
|
|
1394
|
+
*/
|
|
1395
|
+
async forceCompactContext() {
|
|
1396
|
+
try {
|
|
1397
|
+
const model = this.configManager.get("modelName") || "gemini-2.5-flash";
|
|
1398
|
+
const { getModelContextWindowSync } = await import("./config/models.js");
|
|
1399
|
+
const maxTokens = getModelContextWindowSync(model);
|
|
1400
|
+
let tokenCount = await this.countTokensForMessages(model, this.conversationHistory);
|
|
1401
|
+
this.applyTokenCount(tokenCount);
|
|
1402
|
+
const beforeUsagePercent = this.calculateUsagePercent(tokenCount, maxTokens);
|
|
1403
|
+
this.notifyToolStatus(
|
|
1404
|
+
CentaurusCLI.AUTO_COMPACTION_TOOL_NAME,
|
|
1405
|
+
"executing",
|
|
1406
|
+
{ usagePercent: Number(beforeUsagePercent.toFixed(2)), manual: true }
|
|
1407
|
+
);
|
|
1408
|
+
const candidates = this.getAutoCompactionCandidates();
|
|
1409
|
+
let compactedCount = 0;
|
|
1410
|
+
for (const candidate of candidates) {
|
|
1411
|
+
const changed = this.applyAutoCompactionCandidate(candidate);
|
|
1412
|
+
if (changed) {
|
|
1413
|
+
compactedCount += 1;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (compactedCount > 0) {
|
|
1417
|
+
this.appendAutoCompactionNoticeForAgent();
|
|
1418
|
+
}
|
|
1419
|
+
tokenCount = await this.countTokensForMessages(model, this.conversationHistory);
|
|
1420
|
+
this.applyTokenCount(tokenCount);
|
|
1421
|
+
const afterUsagePercent = this.calculateUsagePercent(tokenCount, maxTokens);
|
|
1422
|
+
const summary = compactedCount > 0 ? `Compacted ${compactedCount} item(s) (file reads and terminal outputs).
|
|
1423
|
+
Usage: ${beforeUsagePercent.toFixed(1)}% \u2192 ${afterUsagePercent.toFixed(1)}%` : `No eligible file reads or terminal outputs found in the conversation.
|
|
1424
|
+
Current usage: ${beforeUsagePercent.toFixed(1)}%`;
|
|
1425
|
+
this.notifyToolStatus(
|
|
1426
|
+
CentaurusCLI.AUTO_COMPACTION_TOOL_NAME,
|
|
1427
|
+
"completed",
|
|
1428
|
+
{
|
|
1429
|
+
compactedCount,
|
|
1430
|
+
beforeUsagePercent: Number(beforeUsagePercent.toFixed(2)),
|
|
1431
|
+
afterUsagePercent: Number(afterUsagePercent.toFixed(2)),
|
|
1432
|
+
manual: true
|
|
1433
|
+
},
|
|
1434
|
+
summary
|
|
1435
|
+
);
|
|
1436
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ManualCompaction] Compacted ${compactedCount} item(s), usage ${beforeUsagePercent.toFixed(1)}% -> ${afterUsagePercent.toFixed(1)}%
|
|
1437
|
+
`);
|
|
1438
|
+
return summary;
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1441
|
+
this.notifyToolStatus(
|
|
1442
|
+
CentaurusCLI.AUTO_COMPACTION_TOOL_NAME,
|
|
1443
|
+
"error",
|
|
1444
|
+
void 0,
|
|
1445
|
+
void 0,
|
|
1446
|
+
`Manual compaction failed: ${errorMessage}`
|
|
1447
|
+
);
|
|
1448
|
+
return `\u274C Compaction failed: ${errorMessage}`;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1277
1451
|
async initialize() {
|
|
1452
|
+
cleanupStaleToolOutputs();
|
|
1278
1453
|
this.toolRegistry.register(viewFileTool);
|
|
1279
1454
|
this.toolRegistry.register(writeToFileTool);
|
|
1280
1455
|
this.toolRegistry.register(editFileTool);
|
|
@@ -1287,6 +1462,7 @@ ${CentaurusCLI.TERMINAL_COMPACTION_NOTICE}`;
|
|
|
1287
1462
|
this.toolRegistry.register(inspectSymbolTool);
|
|
1288
1463
|
this.toolRegistry.register(createPlanTool);
|
|
1289
1464
|
this.toolRegistry.register(markTaskCompleteTool);
|
|
1465
|
+
this.toolRegistry.register(planAskQuestionTool);
|
|
1290
1466
|
this.toolRegistry.register(webSearchTool);
|
|
1291
1467
|
this.toolRegistry.register(fetchUrlTool);
|
|
1292
1468
|
this.toolRegistry.register(taskCompleteTool);
|
|
@@ -1298,6 +1474,7 @@ ${CentaurusCLI.TERMINAL_COMPACTION_NOTICE}`;
|
|
|
1298
1474
|
this.toolRegistry.register(workflowTool);
|
|
1299
1475
|
this.toolRegistry.register(fastContextTool);
|
|
1300
1476
|
this.toolRegistry.register(addMcpTool);
|
|
1477
|
+
this.toolRegistry.register(todoWriteTool);
|
|
1301
1478
|
SubAgentManager.initialize(this.toolRegistry);
|
|
1302
1479
|
SubAgentManager.setOnSubAgentCountChange((count) => {
|
|
1303
1480
|
if (this.onSubAgentCountChange) {
|
|
@@ -1381,13 +1558,34 @@ Press Enter to continue...
|
|
|
1381
1558
|
return config.modelName || config.model || "gemini-2.5-flash";
|
|
1382
1559
|
}
|
|
1383
1560
|
/**
|
|
1384
|
-
* Cancel the current AI request
|
|
1561
|
+
* Cancel the current AI request.
|
|
1562
|
+
* If there are queued interrupt messages, dispatch the next one after a short
|
|
1563
|
+
* delay so the "Operation cancelled" message appears in the UI first.
|
|
1385
1564
|
*/
|
|
1386
1565
|
cancelCurrentRequest() {
|
|
1387
1566
|
if (this.currentAbortController) {
|
|
1388
1567
|
this.requestIntentionallyAborted = true;
|
|
1389
1568
|
this.currentAbortController.abort();
|
|
1390
1569
|
this.currentAbortController = void 0;
|
|
1570
|
+
if (this.interruptQueue.length > 0) {
|
|
1571
|
+
const nextInterrupt = this.interruptQueue.shift();
|
|
1572
|
+
if (this.onInterruptQueueUpdateCallback) {
|
|
1573
|
+
this.onInterruptQueueUpdateCallback(this.interruptQueue);
|
|
1574
|
+
}
|
|
1575
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [cancelCurrentRequest] Will dispatch queued interrupt after ESC.
|
|
1576
|
+
`);
|
|
1577
|
+
const wrappedMessage = `${CentaurusCLI.INTERRUPT_SYSTEM_NOTE_PREFIX}
|
|
1578
|
+
|
|
1579
|
+
${nextInterrupt}`;
|
|
1580
|
+
setTimeout(() => {
|
|
1581
|
+
if (this.onQueuedMessageDispatchedCallback) {
|
|
1582
|
+
this.onQueuedMessageDispatchedCallback(nextInterrupt);
|
|
1583
|
+
}
|
|
1584
|
+
this.handleMessage(wrappedMessage, { interruptCleanText: nextInterrupt }).catch((err) => {
|
|
1585
|
+
conversationLogger.logError("Queued interrupt handleMessage (from cancel)", err);
|
|
1586
|
+
});
|
|
1587
|
+
}, 150);
|
|
1588
|
+
}
|
|
1391
1589
|
}
|
|
1392
1590
|
}
|
|
1393
1591
|
/**
|
|
@@ -1590,7 +1788,8 @@ Press Enter to continue...
|
|
|
1590
1788
|
`
|
|
1591
1789
|
);
|
|
1592
1790
|
}
|
|
1593
|
-
const resolvedMessage = augmentedPrompt.replace(/@([^\s@]+)/g, (match,
|
|
1791
|
+
const resolvedMessage = augmentedPrompt.replace(/@"([^"]+)"|@([^\s@]+)/g, (match, quotedPath, unquotedPath) => {
|
|
1792
|
+
const relPath = quotedPath || unquotedPath;
|
|
1594
1793
|
if (relPath.startsWith("rules:")) return match;
|
|
1595
1794
|
const absPath = path.resolve(this.cwd, relPath);
|
|
1596
1795
|
if (fs.existsSync(absPath)) return absPath;
|
|
@@ -1661,17 +1860,21 @@ Use /session-limits to check your quota status.`;
|
|
|
1661
1860
|
}
|
|
1662
1861
|
let userMessageContent = resolvedMessage;
|
|
1663
1862
|
if (this.planMode && !getCurrentPlan()?.isActive) {
|
|
1664
|
-
const planModePrefix = `[PLAN MODE ACTIVE - You MUST call create_plan
|
|
1863
|
+
const planModePrefix = `[PLAN MODE ACTIVE - You MUST call create_plan to submit a detailed plan document]
|
|
1665
1864
|
|
|
1666
1865
|
User Request: ${resolvedMessage}
|
|
1667
1866
|
|
|
1668
1867
|
CRITICAL INSTRUCTIONS:
|
|
1669
1868
|
1. You are in PLANNING MODE - DO NOT implement anything directly
|
|
1670
|
-
2.
|
|
1671
|
-
3.
|
|
1672
|
-
|
|
1673
|
-
-
|
|
1674
|
-
|
|
1869
|
+
2. If the request is ambiguous or missing important details, use plan_ask_question to ask 1-2 clarifying questions FIRST
|
|
1870
|
+
3. Explore the codebase using view_file, list_dir, grep_search to understand the context
|
|
1871
|
+
4. Then call create_plan with:
|
|
1872
|
+
- title: A clear plan title
|
|
1873
|
+
- document: A comprehensive markdown plan document with analysis, tables, and numbered implementation steps
|
|
1874
|
+
- implementation_steps: Array of step descriptions extracted from the document
|
|
1875
|
+
5. Wait for user approval before implementing
|
|
1876
|
+
|
|
1877
|
+
The plan document should include: Overview, Current State Analysis, Proposed Changes (with tables if applicable), Implementation Steps (numbered sequentially), and Verification notes.
|
|
1675
1878
|
|
|
1676
1879
|
DO NOT use write_to_file, edit_file, or execute_command until the plan is approved.`;
|
|
1677
1880
|
userMessageContent = planModePrefix;
|
|
@@ -1786,7 +1989,22 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1786
1989
|
}
|
|
1787
1990
|
}
|
|
1788
1991
|
try {
|
|
1789
|
-
|
|
1992
|
+
let tools = this.toolRegistry.getSchemas();
|
|
1993
|
+
const TUNNEL_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
|
|
1994
|
+
"execute_command",
|
|
1995
|
+
// auto-routed to shell_input for the tunnel
|
|
1996
|
+
"task_complete",
|
|
1997
|
+
// always needed to finish tasks
|
|
1998
|
+
"todo_write",
|
|
1999
|
+
// task tracking still useful
|
|
2000
|
+
"web_search",
|
|
2001
|
+
// web search is independent of the tunnel
|
|
2002
|
+
"fetch_url"
|
|
2003
|
+
// fetching URLs is independent of the tunnel
|
|
2004
|
+
]);
|
|
2005
|
+
if (this.customTunnelCommand) {
|
|
2006
|
+
tools = tools.filter((t) => TUNNEL_ALLOWED_TOOLS.has(t.name));
|
|
2007
|
+
}
|
|
1790
2008
|
const context = {
|
|
1791
2009
|
cwd: this.cwd,
|
|
1792
2010
|
contextManager: this.contextManager,
|
|
@@ -1808,6 +2026,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1808
2026
|
return true;
|
|
1809
2027
|
},
|
|
1810
2028
|
onStreamingOutput: (chunk, type, toolName) => {
|
|
2029
|
+
if (this.customTunnelCommand && (toolName === "execute_command" || !toolName)) {
|
|
2030
|
+
this.tunnelOutputBuffer += chunk;
|
|
2031
|
+
}
|
|
1811
2032
|
if (this.onToolStreamingOutput) {
|
|
1812
2033
|
this.onToolStreamingOutput({ toolName: toolName || "execute_command", chunk, type });
|
|
1813
2034
|
}
|
|
@@ -1825,6 +2046,19 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1825
2046
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
1826
2047
|
const connectedMcpServers = this.getConnectedMCPServerNames();
|
|
1827
2048
|
messages = this.aiContextInjector.injectMCPContext(messages, connectedMcpServers);
|
|
2049
|
+
messages = this.aiContextInjector.injectTodoContext(messages, getTodoContextForPrompt());
|
|
2050
|
+
if (this.customTunnelCommand) {
|
|
2051
|
+
const tunnelParentCtx = this.contextManager.getCurrentContext();
|
|
2052
|
+
let parentInfo;
|
|
2053
|
+
if (tunnelParentCtx.type === "ssh" && tunnelParentCtx.metadata) {
|
|
2054
|
+
parentInfo = `${tunnelParentCtx.metadata.username || "user"}@${tunnelParentCtx.metadata.hostname || "remote"}`;
|
|
2055
|
+
} else if (tunnelParentCtx.type === "wsl" && tunnelParentCtx.metadata) {
|
|
2056
|
+
parentInfo = tunnelParentCtx.metadata.distroName || "WSL";
|
|
2057
|
+
} else if (tunnelParentCtx.type === "docker" && tunnelParentCtx.metadata) {
|
|
2058
|
+
parentInfo = tunnelParentCtx.metadata.containerId?.substring(0, 12) || "container";
|
|
2059
|
+
}
|
|
2060
|
+
messages = this.aiContextInjector.injectCustomTunnelContext(messages, this.customTunnelCommand, tunnelParentCtx.type, parentInfo);
|
|
2061
|
+
}
|
|
1828
2062
|
let environmentContext = this.getEnvironmentContext();
|
|
1829
2063
|
const mode = this.getMode();
|
|
1830
2064
|
let finalAssistantMessage = "";
|
|
@@ -1916,7 +2150,7 @@ Use /session-limits to check your quota status.`;
|
|
|
1916
2150
|
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [CLI] Failed to update token count: ${err}
|
|
1917
2151
|
`);
|
|
1918
2152
|
});
|
|
1919
|
-
for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, myAbortController.signal)) {
|
|
2153
|
+
for await (const chunk of aiServiceClient.streamChat(selectedModel, messages, tools, environmentContext, mode, selectedModelThinkingConfig, myAbortController.signal, config.modelName || "", selectedModelConfig?.uid || config.modelUid || "")) {
|
|
1920
2154
|
if (chunk.type === "file_descriptions") {
|
|
1921
2155
|
this.embedFileDescriptionsInHistory(chunk.descriptions);
|
|
1922
2156
|
continue;
|
|
@@ -1997,7 +2231,7 @@ Use /session-limits to check your quota status.`;
|
|
|
1997
2231
|
thoughtContent = "";
|
|
1998
2232
|
}
|
|
1999
2233
|
toolCalls.push(chunk.toolCall);
|
|
2000
|
-
const SPECIAL_TOOLS = ["task_complete", "create_plan", "mark_task_complete"];
|
|
2234
|
+
const SPECIAL_TOOLS = ["task_complete", "create_plan", "mark_task_complete", "plan_ask_question"];
|
|
2001
2235
|
if (SPECIAL_TOOLS.includes(toolCall.name)) {
|
|
2002
2236
|
if (this.onToolExecutionUpdate) {
|
|
2003
2237
|
this.onToolExecutionUpdate({
|
|
@@ -2011,6 +2245,62 @@ Use /session-limits to check your quota status.`;
|
|
|
2011
2245
|
isToolExecuting = true;
|
|
2012
2246
|
toolsExecutedInStream = true;
|
|
2013
2247
|
try {
|
|
2248
|
+
if (this.customTunnelCommand && toolCall.name === "execute_command" && !toolCall.arguments?.shell_input) {
|
|
2249
|
+
const tunnelCmd = toolCall.arguments?.CommandLine || toolCall.arguments?.commandLine || toolCall.arguments?.command;
|
|
2250
|
+
if (tunnelCmd && this.currentInteractiveProcess) {
|
|
2251
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [CLI] Executing in custom tunnel (in-stream): ${tunnelCmd}
|
|
2252
|
+
`);
|
|
2253
|
+
const rText = toolCall.arguments.reason_text;
|
|
2254
|
+
if (rText && this.onResponseStreamCallback) {
|
|
2255
|
+
this.onResponseStreamCallback(rText + "\n\n");
|
|
2256
|
+
}
|
|
2257
|
+
toolCall.arguments._customTunnelExec = true;
|
|
2258
|
+
this.notifyToolStatus(toolCall.name, "executing", toolCall.arguments);
|
|
2259
|
+
conversationLogger.logToolExecutionStart(toolCall.name, toolCall.id);
|
|
2260
|
+
const outputBefore = this.tunnelOutputBuffer.length;
|
|
2261
|
+
const inputWithEnter = tunnelCmd.endsWith("\n") || tunnelCmd.endsWith("\r") ? tunnelCmd : tunnelCmd + "\r";
|
|
2262
|
+
this.currentInteractiveProcess.write(inputWithEnter);
|
|
2263
|
+
let lastLen = outputBefore;
|
|
2264
|
+
const maxWaitMs = 5e3;
|
|
2265
|
+
const pollMs = 300;
|
|
2266
|
+
const stabilityMs = 800;
|
|
2267
|
+
let stableTime = 0;
|
|
2268
|
+
const waitStart = Date.now();
|
|
2269
|
+
while (Date.now() - waitStart < maxWaitMs) {
|
|
2270
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
2271
|
+
const currentLen = this.tunnelOutputBuffer.length;
|
|
2272
|
+
if (currentLen > lastLen) {
|
|
2273
|
+
lastLen = currentLen;
|
|
2274
|
+
stableTime = 0;
|
|
2275
|
+
} else {
|
|
2276
|
+
stableTime += pollMs;
|
|
2277
|
+
if (stableTime >= stabilityMs) break;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
const capturedOutput = this.tunnelOutputBuffer.substring(outputBefore);
|
|
2281
|
+
const { processTerminalOutput } = await import("./utils/terminal-output.js");
|
|
2282
|
+
const processedOutput = processTerminalOutput(capturedOutput).trim() || "(no output)";
|
|
2283
|
+
this.notifyToolStatus(toolCall.name, "completed", toolCall.arguments, processedOutput);
|
|
2284
|
+
conversationLogger.logToolResult(toolCall.name, toolCall.id, processedOutput, true);
|
|
2285
|
+
const sanitizedResult = sanitizeForContext(toolCall.name, processedOutput, toolCall.arguments);
|
|
2286
|
+
inStreamToolResults.push({
|
|
2287
|
+
tool_call_id: toolCall.id,
|
|
2288
|
+
name: toolCall.name,
|
|
2289
|
+
result: this.truncateResult(sanitizedResult, toolCall.name)
|
|
2290
|
+
});
|
|
2291
|
+
inStreamHandledIds.add(toolCall.id);
|
|
2292
|
+
isToolExecuting = false;
|
|
2293
|
+
if (pendingTextBuffer && this.onResponseStreamCallback) {
|
|
2294
|
+
this.onResponseStreamCallback(pendingTextBuffer);
|
|
2295
|
+
pendingTextBuffer = "";
|
|
2296
|
+
}
|
|
2297
|
+
continue;
|
|
2298
|
+
} else if (tunnelCmd && !this.currentInteractiveProcess) {
|
|
2299
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [CLI] Custom tunnel process exited, running execute_command normally (in-stream): ${tunnelCmd}
|
|
2300
|
+
`);
|
|
2301
|
+
this.customTunnelCommand = null;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2014
2304
|
const reasonText = toolCall.arguments.reason_text;
|
|
2015
2305
|
const isShellInput = toolCall.name === "execute_command" && toolCall.arguments.shell_input;
|
|
2016
2306
|
if (reasonText && !isShellInput && this.onResponseStreamCallback) {
|
|
@@ -2035,6 +2325,33 @@ Use /session-limits to check your quota status.`;
|
|
|
2035
2325
|
if (result.success) {
|
|
2036
2326
|
conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
|
|
2037
2327
|
this.notifyToolStatus(toolCall.name, "completed", toolCall.arguments, result.result);
|
|
2328
|
+
if (toolCall.name === "todo_write" && typeof result.result === "string" && result.result.startsWith("TODO_UPDATED:")) {
|
|
2329
|
+
try {
|
|
2330
|
+
const todoData = JSON.parse(result.result.substring("TODO_UPDATED:".length));
|
|
2331
|
+
if (this.onTodoListUpdate) {
|
|
2332
|
+
this.onTodoListUpdate(todoData.todos, todoData.completedCount, todoData.totalCount);
|
|
2333
|
+
}
|
|
2334
|
+
inStreamToolResults.push({
|
|
2335
|
+
tool_call_id: toolCall.id,
|
|
2336
|
+
name: toolCall.name,
|
|
2337
|
+
result: todoData.summary
|
|
2338
|
+
});
|
|
2339
|
+
} catch (parseError) {
|
|
2340
|
+
logWarning(`Failed to parse todo list update: ${parseError?.message || parseError}`);
|
|
2341
|
+
inStreamToolResults.push({
|
|
2342
|
+
tool_call_id: toolCall.id,
|
|
2343
|
+
name: toolCall.name,
|
|
2344
|
+
result: result.result
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
inStreamHandledIds.add(toolCall.id);
|
|
2348
|
+
isToolExecuting = false;
|
|
2349
|
+
if (pendingTextBuffer && this.onResponseStreamCallback) {
|
|
2350
|
+
this.onResponseStreamCallback(pendingTextBuffer);
|
|
2351
|
+
pendingTextBuffer = "";
|
|
2352
|
+
}
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2038
2355
|
if (["write_to_file", "edit_file", "multi_edit_file"].includes(toolCall.name)) {
|
|
2039
2356
|
this.updateFileChangeSummary();
|
|
2040
2357
|
}
|
|
@@ -2051,7 +2368,7 @@ Use /session-limits to check your quota status.`;
|
|
|
2051
2368
|
inStreamToolResults.push({
|
|
2052
2369
|
tool_call_id: toolCall.id,
|
|
2053
2370
|
name: toolCall.name,
|
|
2054
|
-
result: this.truncateResult(sanitizedResult)
|
|
2371
|
+
result: this.truncateResult(sanitizedResult, toolCall.name)
|
|
2055
2372
|
});
|
|
2056
2373
|
} else {
|
|
2057
2374
|
conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, result.error);
|
|
@@ -2150,6 +2467,50 @@ Use /session-limits to check your quota status.`;
|
|
|
2150
2467
|
clearPlan();
|
|
2151
2468
|
break;
|
|
2152
2469
|
}
|
|
2470
|
+
if (toolCall.name === "plan_ask_question") {
|
|
2471
|
+
const execResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
2472
|
+
const result2 = execResult.success ? String(execResult.result) : `Error: ${execResult.error}`;
|
|
2473
|
+
if (typeof result2 === "string" && result2.startsWith("ASK_QUESTION:")) {
|
|
2474
|
+
const questionJson = result2.substring("ASK_QUESTION:".length);
|
|
2475
|
+
try {
|
|
2476
|
+
const { question, options: options2 } = JSON.parse(questionJson);
|
|
2477
|
+
let userAnswer;
|
|
2478
|
+
if (this.onPlanQuestionRequest) {
|
|
2479
|
+
userAnswer = await this.onPlanQuestionRequest(question, options2);
|
|
2480
|
+
} else {
|
|
2481
|
+
userAnswer = "[User skipped this question. Proceed with the most appropriate approach.]";
|
|
2482
|
+
}
|
|
2483
|
+
const questionAssistantMsg = {
|
|
2484
|
+
role: "assistant",
|
|
2485
|
+
content: "",
|
|
2486
|
+
tool_calls: [toolCall]
|
|
2487
|
+
};
|
|
2488
|
+
if (currentTurnThinking) {
|
|
2489
|
+
questionAssistantMsg.thinking = currentTurnThinking;
|
|
2490
|
+
}
|
|
2491
|
+
if (currentTurnThinkingSignature) {
|
|
2492
|
+
questionAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
|
|
2493
|
+
}
|
|
2494
|
+
this.conversationHistory.push(questionAssistantMsg);
|
|
2495
|
+
this.conversationHistory.push({
|
|
2496
|
+
role: "tool",
|
|
2497
|
+
tool_call_id: toolCall.id,
|
|
2498
|
+
content: `User's answer: ${userAnswer}`
|
|
2499
|
+
});
|
|
2500
|
+
handledToolCallIds.add(toolCall.id);
|
|
2501
|
+
messages = getMessagesForContext();
|
|
2502
|
+
continue;
|
|
2503
|
+
} catch (parseError) {
|
|
2504
|
+
logWarning(`Failed to parse plan question: ${parseError?.message || parseError}`);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
toolResults.push({
|
|
2508
|
+
tool_call_id: toolCall.id,
|
|
2509
|
+
name: toolCall.name,
|
|
2510
|
+
result: result2 || "Question could not be displayed to user."
|
|
2511
|
+
});
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2153
2514
|
if (toolCall.name === "create_plan") {
|
|
2154
2515
|
const execResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
2155
2516
|
const result2 = execResult.success ? String(execResult.result) : `Error: ${execResult.error}`;
|
|
@@ -2187,13 +2548,22 @@ Use /session-limits to check your quota status.`;
|
|
|
2187
2548
|
content: "Plan approved by user. Now switching to execution mode."
|
|
2188
2549
|
});
|
|
2189
2550
|
handledToolCallIds.add(toolCall.id);
|
|
2190
|
-
const
|
|
2551
|
+
const planContext = getPlanContextForPrompt();
|
|
2191
2552
|
const originalRequest = this.pendingPlanRequest || message;
|
|
2192
|
-
const executionPrompt =
|
|
2553
|
+
const executionPrompt = `The user has approved your plan. Now implement it step by step.
|
|
2554
|
+
|
|
2555
|
+
${planContext}
|
|
2556
|
+
|
|
2557
|
+
## Full Plan Document (for reference):
|
|
2558
|
+
${plan.document}
|
|
2193
2559
|
|
|
2194
2560
|
Original Request: ${originalRequest}
|
|
2195
2561
|
|
|
2196
|
-
|
|
2562
|
+
INSTRUCTIONS:
|
|
2563
|
+
- Implement each step one at a time, in order
|
|
2564
|
+
- After completing each step, call mark_task_complete with step_number=N (e.g., step_number=1, step_number=2)
|
|
2565
|
+
- When all steps are complete, output your summary then call task_complete to end the session
|
|
2566
|
+
- Start with step 1 now.`;
|
|
2197
2567
|
this.conversationHistory.push({
|
|
2198
2568
|
role: "user",
|
|
2199
2569
|
content: executionPrompt
|
|
@@ -2223,7 +2593,7 @@ Complete the current task. After finishing each subtask, call mark_task_complete
|
|
|
2223
2593
|
this.conversationHistory.push({
|
|
2224
2594
|
role: "tool",
|
|
2225
2595
|
tool_call_id: toolCall.id,
|
|
2226
|
-
content: `Plan created: "${plan.title}" with ${plan.
|
|
2596
|
+
content: `Plan created: "${plan.title}" with ${plan.implementationSteps.length} steps. Waiting for user approval.`
|
|
2227
2597
|
});
|
|
2228
2598
|
handledToolCallIds.add(toolCall.id);
|
|
2229
2599
|
messages = [...this.conversationHistory];
|
|
@@ -2283,74 +2653,27 @@ Complete the current task. After finishing each subtask, call mark_task_complete
|
|
|
2283
2653
|
const completionJson = result2.substring("TASK_COMPLETED:".length);
|
|
2284
2654
|
try {
|
|
2285
2655
|
const completion = JSON.parse(completionJson);
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
completion.totalCount,
|
|
2296
|
-
completion.completionNote,
|
|
2297
|
-
completion.taskDescription
|
|
2298
|
-
// Pass the actual task/subtask description
|
|
2299
|
-
);
|
|
2300
|
-
}
|
|
2656
|
+
if (this.onTaskCompleted) {
|
|
2657
|
+
this.onTaskCompleted(
|
|
2658
|
+
completion.stepNumber,
|
|
2659
|
+
completion.stepDescription,
|
|
2660
|
+
completion.completedCount,
|
|
2661
|
+
completion.totalCount,
|
|
2662
|
+
completion.completionNote,
|
|
2663
|
+
completion.allSteps
|
|
2664
|
+
);
|
|
2301
2665
|
}
|
|
2302
|
-
const displayType = completion.type === "subtask" ? "Subtask" : "Task";
|
|
2303
2666
|
this.notifyToolStatus(
|
|
2304
2667
|
toolCall.name,
|
|
2305
2668
|
"completed",
|
|
2306
2669
|
toolCall.arguments,
|
|
2307
|
-
|
|
2670
|
+
`Step ${completion.stepNumber} of ${completion.totalCount} completed: ${completion.stepDescription}`
|
|
2308
2671
|
);
|
|
2309
|
-
if (completion.mainTaskComplete || completion.type === "task") {
|
|
2310
|
-
const nextPhase = getCurrentPhase();
|
|
2311
|
-
if (nextPhase && !completion.allComplete) {
|
|
2312
|
-
const phaseContext = getPhaseContextForPrompt();
|
|
2313
|
-
toolResults.push({
|
|
2314
|
-
tool_call_id: toolCall.id,
|
|
2315
|
-
name: toolCall.name,
|
|
2316
|
-
result: `Task completed. Moving to Task ${nextPhase.taskNumber}: ${nextPhase.task.description}`
|
|
2317
|
-
});
|
|
2318
|
-
const nextPhaseAssistantMsg = {
|
|
2319
|
-
role: "assistant",
|
|
2320
|
-
content: "",
|
|
2321
|
-
tool_calls: [toolCall]
|
|
2322
|
-
};
|
|
2323
|
-
if (currentTurnThinking) {
|
|
2324
|
-
nextPhaseAssistantMsg.thinking = currentTurnThinking;
|
|
2325
|
-
}
|
|
2326
|
-
if (currentTurnThinkingSignature) {
|
|
2327
|
-
nextPhaseAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
|
|
2328
|
-
}
|
|
2329
|
-
this.conversationHistory.push(nextPhaseAssistantMsg);
|
|
2330
|
-
this.conversationHistory.push({
|
|
2331
|
-
role: "tool",
|
|
2332
|
-
tool_call_id: toolCall.id,
|
|
2333
|
-
content: `Task completed. Now starting Task ${nextPhase.taskNumber}.`
|
|
2334
|
-
});
|
|
2335
|
-
this.conversationHistory.push({
|
|
2336
|
-
role: "user",
|
|
2337
|
-
content: `${phaseContext}
|
|
2338
|
-
|
|
2339
|
-
Continue with the next task. Complete each subtask and call mark_task_complete for each one.`
|
|
2340
|
-
});
|
|
2341
|
-
handledToolCallIds.add(toolCall.id);
|
|
2342
|
-
messages = getMessagesForContext();
|
|
2343
|
-
continue;
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
2672
|
toolResults.push({
|
|
2347
2673
|
tool_call_id: toolCall.id,
|
|
2348
2674
|
name: toolCall.name,
|
|
2349
|
-
result: completion.allComplete ? "All
|
|
2675
|
+
result: completion.allComplete ? "All steps in the plan are now completed! Output your summary of what was accomplished, then call task_complete()." : completion.nextStep ? `Step ${completion.stepNumber} completed (${completion.completedCount}/${completion.totalCount}). Next step ${completion.nextStepNumber}: ${completion.nextStep}` : `Step ${completion.stepNumber} completed.`
|
|
2350
2676
|
});
|
|
2351
|
-
if (completion.allComplete) {
|
|
2352
|
-
toolResults[toolResults.length - 1].result = "All tasks in the plan are now completed! Output your summary of what was accomplished, then call task_complete().";
|
|
2353
|
-
}
|
|
2354
2677
|
} catch (parseError) {
|
|
2355
2678
|
logWarning(`Failed to parse task completion: ${parseError?.message || parseError}`);
|
|
2356
2679
|
}
|
|
@@ -2386,6 +2709,41 @@ The system has stopped to prevent an infinite loop.
|
|
|
2386
2709
|
}
|
|
2387
2710
|
const currentCtx = this.contextManager.getCurrentContext();
|
|
2388
2711
|
const effectiveCwd = currentCtx.type !== "local" ? currentCtx.metadata?.workingDirectory || "~" : this.cwd;
|
|
2712
|
+
if (this.customTunnelCommand && toolCall.name === "execute_command" && !toolCall.arguments?.shell_input && !toolCall.arguments?._customTunnelExec) {
|
|
2713
|
+
const tunnelCommandLine = toolCall.arguments?.CommandLine || toolCall.arguments?.commandLine || toolCall.arguments?.command;
|
|
2714
|
+
if (tunnelCommandLine && this.currentInteractiveProcess) {
|
|
2715
|
+
quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [CLI] Executing in custom tunnel (post-stream fallback): ${tunnelCommandLine}
|
|
2716
|
+
`);
|
|
2717
|
+
toolCall.arguments._customTunnelExec = true;
|
|
2718
|
+
const outputBefore = this.tunnelOutputBuffer.length;
|
|
2719
|
+
const inputWithEnter = tunnelCommandLine.endsWith("\n") || tunnelCommandLine.endsWith("\r") ? tunnelCommandLine : tunnelCommandLine + "\r";
|
|
2720
|
+
this.currentInteractiveProcess.write(inputWithEnter);
|
|
2721
|
+
let lastLen2 = outputBefore;
|
|
2722
|
+
let stableTime2 = 0;
|
|
2723
|
+
const waitStart2 = Date.now();
|
|
2724
|
+
while (Date.now() - waitStart2 < 5e3) {
|
|
2725
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
2726
|
+
const curLen = this.tunnelOutputBuffer.length;
|
|
2727
|
+
if (curLen > lastLen2) {
|
|
2728
|
+
lastLen2 = curLen;
|
|
2729
|
+
stableTime2 = 0;
|
|
2730
|
+
} else {
|
|
2731
|
+
stableTime2 += 300;
|
|
2732
|
+
if (stableTime2 >= 800) break;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
const captured = this.tunnelOutputBuffer.substring(outputBefore);
|
|
2736
|
+
const { processTerminalOutput: pTO } = await import("./utils/terminal-output.js");
|
|
2737
|
+
const processed = pTO(captured).trim() || "(no output)";
|
|
2738
|
+
this.notifyToolStatus(toolCall.name, "completed", toolCall.arguments, processed);
|
|
2739
|
+
conversationLogger.logToolResult(toolCall.name, toolCall.id, processed, true);
|
|
2740
|
+
const sanitized = sanitizeForContext(toolCall.name, processed, toolCall.arguments);
|
|
2741
|
+
toolResults.push({ tool_call_id: toolCall.id, name: toolCall.name, result: this.truncateResult(sanitized, toolCall.name) });
|
|
2742
|
+
continue;
|
|
2743
|
+
} else if (tunnelCommandLine && !this.currentInteractiveProcess) {
|
|
2744
|
+
this.customTunnelCommand = null;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2389
2747
|
let remoteContext;
|
|
2390
2748
|
if (currentCtx.type !== "local") {
|
|
2391
2749
|
const metadata = currentCtx.metadata;
|
|
@@ -2459,7 +2817,7 @@ The system has stopped to prevent an infinite loop.
|
|
|
2459
2817
|
}
|
|
2460
2818
|
const sanitizedResult = sanitizeForContext(toolCall.name, parsedResult, toolCall.arguments);
|
|
2461
2819
|
conversationLogger.logToolResult(`${toolCall.name} (SANITIZED_CONTEXT)`, toolCall.id, sanitizedResult, true);
|
|
2462
|
-
const truncatedResult = this.truncateResult(sanitizedResult);
|
|
2820
|
+
const truncatedResult = this.truncateResult(sanitizedResult, toolCall.name);
|
|
2463
2821
|
toolResults.push({
|
|
2464
2822
|
tool_call_id: toolCall.id,
|
|
2465
2823
|
name: toolCall.name,
|
|
@@ -2845,13 +3203,11 @@ ${nextInterrupt}`;
|
|
|
2845
3203
|
/sync - Sync data to/from cloud (upload/restore)
|
|
2846
3204
|
/config - View current configuration
|
|
2847
3205
|
/model - Select from available Google models
|
|
2848
|
-
/plan - Toggle plan mode for complex implementations
|
|
3206
|
+
/plan - Toggle plan mode for complex implementations (or Ctrl+P)
|
|
2849
3207
|
/mcp - Manage configured MCP servers and tools
|
|
2850
3208
|
/docs - Open Centaurus documentation in browser
|
|
2851
3209
|
/copy-chat-context - Copy chat as readable text to clipboard
|
|
2852
3210
|
/session-limits - View session quota usage and limits
|
|
2853
|
-
/quality - Toggle enhanced quality features (thinking protocol, validation)
|
|
2854
|
-
/autonomous - Toggle autonomous mode (Silent Operator with task_complete)
|
|
2855
3211
|
/sign-in - Sign in with Google (if not already signed in)
|
|
2856
3212
|
/logout - Sign out, clear session, and exit CLI
|
|
2857
3213
|
/exit - Exit the application
|
|
@@ -3105,18 +3461,6 @@ Exiting CLI anyway...`;
|
|
|
3105
3461
|
this.onPlanModeChange(this.planMode);
|
|
3106
3462
|
}
|
|
3107
3463
|
return;
|
|
3108
|
-
case "quality":
|
|
3109
|
-
const currentQuality = this.configManager.get("enhancedQuality");
|
|
3110
|
-
const newQuality = currentQuality === false;
|
|
3111
|
-
this.configManager.set("enhancedQuality", newQuality);
|
|
3112
|
-
responseMessage = newQuality ? "\u2705 Enhanced quality features enabled\n\nThe AI will now use:\n\u2022 Structured thinking protocol before actions\n\u2022 Enhanced command execution hygiene\n\u2022 Intelligent file editing validation\n\u2022 Professional SRE/Architect persona" : "\u26A0\uFE0F Enhanced quality features disabled\n\nThe AI will use the basic system prompt without:\n\u2022 Thinking protocol\n\u2022 Advanced validation\n\u2022 Enhanced tool descriptions";
|
|
3113
|
-
break;
|
|
3114
|
-
case "autonomous":
|
|
3115
|
-
const currentAutonomous = this.configManager.get("autonomousMode");
|
|
3116
|
-
const newAutonomous = !currentAutonomous;
|
|
3117
|
-
this.configManager.set("autonomousMode", newAutonomous);
|
|
3118
|
-
responseMessage = newAutonomous ? "\u2705 Autonomous Mode enabled (Silent Operator)\n\nThe AI will now:\n\u2022 Work silently without narrating actions\n\u2022 Use Touch-First safety (never guess file paths)\n\u2022 Apply surgical precision to file edits\n\u2022 Output summary text, then call task_complete() when done\n\u2022 Inject intelligent error recovery hints\n\nThis is the industry-standard autonomous agent mode." : "\u26A0\uFE0F Autonomous Mode disabled\n\nThe AI will use the standard enhanced prompt with:\n\u2022 Verbose communication after each action\n\u2022 Standard tool descriptions\n\u2022 Manual completion detection";
|
|
3119
|
-
break;
|
|
3120
3464
|
case "clear":
|
|
3121
3465
|
const clearCancelMsg = this.cancelWorkflowLearning("Clearing chat session");
|
|
3122
3466
|
if (clearCancelMsg && this.onDirectMessageCallback) {
|
|
@@ -3142,17 +3486,9 @@ Usage: /config set <key> <value>`;
|
|
|
3142
3486
|
} catch (error) {
|
|
3143
3487
|
responseMessage = `\u274C Failed to update configuration: ${error.message}`;
|
|
3144
3488
|
}
|
|
3145
|
-
} else if (configKey === "enhancedQuality" || configKey === "autonomousMode") {
|
|
3146
|
-
const boolValue = configValue.toLowerCase() === "true" || configValue === "1";
|
|
3147
|
-
try {
|
|
3148
|
-
this.configManager.set(configKey, boolValue);
|
|
3149
|
-
responseMessage = `\u2705 Configuration updated: ${configKey} = ${boolValue}`;
|
|
3150
|
-
} catch (error) {
|
|
3151
|
-
responseMessage = `\u274C Failed to update configuration: ${error.message}`;
|
|
3152
|
-
}
|
|
3153
3489
|
} else {
|
|
3154
3490
|
responseMessage = `\u274C Error: Unknown config key: ${configKey}
|
|
3155
|
-
Valid keys: model
|
|
3491
|
+
Valid keys: model`;
|
|
3156
3492
|
}
|
|
3157
3493
|
} else {
|
|
3158
3494
|
const config = this.configManager.load();
|
|
@@ -3163,6 +3499,7 @@ Valid keys: model, enhancedQuality, autonomousMode`;
|
|
|
3163
3499
|
Version: ${currentVersion}
|
|
3164
3500
|
Model: ${config.model || "gemini-2.5-flash (default)"}
|
|
3165
3501
|
AI Auto-Suggest: ${config.aiAutoSuggest === true ? "\u2705 Enabled" : "\u274C Disabled"}
|
|
3502
|
+
Limit Chat History: ${config.limitChatHistory !== false ? "\u2705 Enabled" : "\u274C Disabled"}
|
|
3166
3503
|
Authentication: ${apiClient.isAuthenticated() ? "\u2705 Signed in" : "\u274C Not signed in"}`;
|
|
3167
3504
|
}
|
|
3168
3505
|
break;
|
|
@@ -3184,8 +3521,84 @@ Authentication: ${apiClient.isAuthenticated() ? "\u2705 Signed in" : "\u274C Not
|
|
|
3184
3521
|
} else {
|
|
3185
3522
|
responseMessage = "\u274C Invalid option. Usage: `/settings auto-suggest on` or `/settings auto-suggest off`";
|
|
3186
3523
|
}
|
|
3524
|
+
} else if (args.length >= 2 && args[0].toLowerCase() === "limit-chat-history") {
|
|
3525
|
+
const value = args[1].toLowerCase();
|
|
3526
|
+
if (value === "on") {
|
|
3527
|
+
this.configManager.set("limitChatHistory", true);
|
|
3528
|
+
if (this.onLimitChatHistoryChange) {
|
|
3529
|
+
this.onLimitChatHistoryChange(true);
|
|
3530
|
+
}
|
|
3531
|
+
responseMessage = "\u2705 **Chat History Limit Enabled**\n\nThe chat UI will now keep a recent window of rendered chat lines on screen, adjusted to your terminal width and available memory for a cleaner, more compact view.\n\nUse `/settings limit-chat-history off` to show the full UI history again.";
|
|
3532
|
+
} else if (value === "off") {
|
|
3533
|
+
this.configManager.set("limitChatHistory", false);
|
|
3534
|
+
if (this.onLimitChatHistoryChange) {
|
|
3535
|
+
this.onLimitChatHistoryChange(false);
|
|
3536
|
+
}
|
|
3537
|
+
responseMessage = "\u2705 **Chat History Limit Disabled**\n\nThe chat UI will now show the full conversation history on screen.";
|
|
3538
|
+
} else {
|
|
3539
|
+
responseMessage = "\u274C Invalid option. Usage: `/settings limit-chat-history on` or `/settings limit-chat-history off`";
|
|
3540
|
+
}
|
|
3541
|
+
} else if (args.length >= 1 && args[0].toLowerCase() === "add-tunnel-command") {
|
|
3542
|
+
const { TunnelCommandsManager } = await import("./utils/tunnel-commands-manager.js");
|
|
3543
|
+
const tunnelCommandsManager = TunnelCommandsManager.getInstance();
|
|
3544
|
+
await tunnelCommandsManager.initialize();
|
|
3545
|
+
const subArg = args[1]?.toLowerCase();
|
|
3546
|
+
if (!subArg || subArg === "list") {
|
|
3547
|
+
const commands = tunnelCommandsManager.listCommands();
|
|
3548
|
+
if (commands.length === 0) {
|
|
3549
|
+
responseMessage = "\u{1F4CB} No tunnel commands configured.\n\nAdd one with: `/settings add-tunnel-command <command>`\nExample: `/settings add-tunnel-command minicom`\nExample: `/settings add-tunnel-command source .venv/bin/activate`";
|
|
3550
|
+
} else {
|
|
3551
|
+
responseMessage = `\u{1F4CB} Custom Tunnel Commands (${commands.length}):
|
|
3552
|
+
|
|
3553
|
+
` + commands.map((cmd2) => ` \u2022 ${cmd2}`).join("\n") + "\n\nUsage:\n /settings add-tunnel-command <command>\n /settings add-tunnel-command delete <command>\n /settings add-tunnel-command list";
|
|
3554
|
+
}
|
|
3555
|
+
} else if (subArg === "delete" || subArg === "remove") {
|
|
3556
|
+
const cmdToDelete = args.slice(2).join(" ").trim();
|
|
3557
|
+
if (!cmdToDelete) {
|
|
3558
|
+
responseMessage = "\u274C Please specify a tunnel command to delete.\n\nUsage: `/settings add-tunnel-command delete <command>`";
|
|
3559
|
+
} else {
|
|
3560
|
+
const result = await tunnelCommandsManager.deleteCommand(cmdToDelete);
|
|
3561
|
+
responseMessage = result.success ? `\u2705 ${result.message}` : `\u274C ${result.message}`;
|
|
3562
|
+
}
|
|
3563
|
+
} else {
|
|
3564
|
+
const commandToAdd = args[1]?.toLowerCase() === "add" ? args.slice(2).join(" ").trim() : args.slice(1).join(" ").trim();
|
|
3565
|
+
if (!commandToAdd) {
|
|
3566
|
+
responseMessage = "\u274C Please specify a tunnel command to add.\n\nUsage: `/settings add-tunnel-command <command>`";
|
|
3567
|
+
} else {
|
|
3568
|
+
const result = await tunnelCommandsManager.addCommand(commandToAdd);
|
|
3569
|
+
if (result.success) {
|
|
3570
|
+
responseMessage = `\u2705 ${result.message}
|
|
3571
|
+
|
|
3572
|
+
Run the command in focus mode and press Alt+E (Cmd+E on macOS) to route plain chat input into that running process.`;
|
|
3573
|
+
} else {
|
|
3574
|
+
responseMessage = `\u274C ${result.message}`;
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
} else if (args.length >= 2 && args[0].toLowerCase() === "theme") {
|
|
3579
|
+
const colorName = args[1].toLowerCase();
|
|
3580
|
+
const themeOption = THEME_PALETTE.find((t) => t.name === colorName);
|
|
3581
|
+
if (themeOption) {
|
|
3582
|
+
this.configManager.set("themeColor", themeOption.value);
|
|
3583
|
+
setGlobalAccentColor(themeOption.value);
|
|
3584
|
+
if (this.onThemeColorChange) {
|
|
3585
|
+
this.onThemeColorChange(themeOption.value);
|
|
3586
|
+
}
|
|
3587
|
+
responseMessage = `\u2705 **Theme Updated**
|
|
3588
|
+
|
|
3589
|
+
Accent color changed to **${themeOption.label.replace("\u2588\u2588\u2588\u2588 ", "")}** (${themeOption.value}).
|
|
3590
|
+
|
|
3591
|
+
The new theme will apply across the UI.`;
|
|
3592
|
+
} else {
|
|
3593
|
+
const validNames = THEME_PALETTE.map((t) => t.name).join(", ");
|
|
3594
|
+
responseMessage = `\u274C Unknown theme color: "${args[1]}"
|
|
3595
|
+
|
|
3596
|
+
Available themes: ${validNames}
|
|
3597
|
+
|
|
3598
|
+
Usage: \`/settings theme <color>\``;
|
|
3599
|
+
}
|
|
3187
3600
|
} else {
|
|
3188
|
-
responseMessage = "\u274C Invalid command
|
|
3601
|
+
responseMessage = "\u274C Invalid settings command.\n\nAvailable settings:\n\u2022 `/settings auto-suggest on/off`\n\u2022 `/settings limit-chat-history on/off`\n\u2022 `/settings theme <color>`\n\u2022 `/settings add-tunnel-command <command>`";
|
|
3189
3602
|
}
|
|
3190
3603
|
break;
|
|
3191
3604
|
case "model":
|
|
@@ -3251,19 +3664,14 @@ Then try /models local again.`;
|
|
|
3251
3664
|
if (this.onShowPickerCallback) {
|
|
3252
3665
|
const config = this.configManager.load();
|
|
3253
3666
|
const currentModelName = config.modelName || "";
|
|
3254
|
-
const isCurrentCloud = config.isLocalModel !== true;
|
|
3255
3667
|
const modelsConfig = await fetchModelsConfig();
|
|
3256
3668
|
this.onShowPickerCallback({
|
|
3257
3669
|
message: "Select Cloud Model",
|
|
3258
3670
|
type: "model",
|
|
3259
|
-
choices:
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
value: `${index}`
|
|
3264
|
-
// Use index as unique identifier
|
|
3265
|
-
};
|
|
3266
|
-
})
|
|
3671
|
+
choices: [],
|
|
3672
|
+
// Not used - ModelPicker reads modelConfigs directly
|
|
3673
|
+
modelConfigs: modelsConfig.models,
|
|
3674
|
+
currentModelName
|
|
3267
3675
|
});
|
|
3268
3676
|
return;
|
|
3269
3677
|
}
|
|
@@ -3557,6 +3965,109 @@ Restored ${result.restored} files, removed ${result.removed} files.`;
|
|
|
3557
3965
|
}
|
|
3558
3966
|
break;
|
|
3559
3967
|
}
|
|
3968
|
+
case "sub-agent":
|
|
3969
|
+
case "subagent":
|
|
3970
|
+
if (args.length >= 1 && args[0].toLowerCase() === "list") {
|
|
3971
|
+
const allAgents = SubAgentManager.getAllSubAgents();
|
|
3972
|
+
if (allAgents.length === 0) {
|
|
3973
|
+
responseMessage = "\u{1F4ED} No sub-agents found.\n\nSub-agents are spawned by the AI when working on complex tasks.";
|
|
3974
|
+
} else if (this.onSubAgentListScreenSetup) {
|
|
3975
|
+
const agentList = allAgents.map((a) => ({
|
|
3976
|
+
id: a.id,
|
|
3977
|
+
status: a.status,
|
|
3978
|
+
prompt: a.prompt,
|
|
3979
|
+
model: a.model,
|
|
3980
|
+
startTime: a.startTime,
|
|
3981
|
+
turnCount: a.turnCount,
|
|
3982
|
+
fileOpsCount: a.fileOperations.length
|
|
3983
|
+
}));
|
|
3984
|
+
this.onSubAgentListScreenSetup(agentList);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
} else if (args.length >= 1 && args[0].toLowerCase() === "terminate") {
|
|
3988
|
+
const runningAgents = SubAgentManager.getRunningSubAgents();
|
|
3989
|
+
if (runningAgents.length === 0) {
|
|
3990
|
+
responseMessage = "\u{1F4ED} No running sub-agents to terminate.";
|
|
3991
|
+
} else if (this.onSubAgentTerminateScreenSetup) {
|
|
3992
|
+
const agentList = runningAgents.map((a) => ({
|
|
3993
|
+
id: a.id,
|
|
3994
|
+
status: a.status,
|
|
3995
|
+
prompt: a.prompt,
|
|
3996
|
+
model: a.model,
|
|
3997
|
+
startTime: a.startTime
|
|
3998
|
+
}));
|
|
3999
|
+
this.onSubAgentTerminateScreenSetup(agentList);
|
|
4000
|
+
return;
|
|
4001
|
+
}
|
|
4002
|
+
} else {
|
|
4003
|
+
responseMessage = "\u274C Invalid sub-agent command.\n\nAvailable commands:\n\u2022 `/sub-agent list` \u2014 View all sub-agents\n\u2022 `/sub-agent terminate` \u2014 Terminate a running sub-agent";
|
|
4004
|
+
}
|
|
4005
|
+
break;
|
|
4006
|
+
case "skill": {
|
|
4007
|
+
const subCmd = args[0]?.toLowerCase();
|
|
4008
|
+
if (!subCmd) {
|
|
4009
|
+
responseMessage = "\u{1F4DA} **Skills**\n\nAvailable commands:\n\u2022 `/skill new` \u2014 Create a new skill\n\u2022 `/skill list` \u2014 List saved skills\n\u2022 `/skill edit <name>` \u2014 Edit a skill\n\u2022 `/skill delete <name>` \u2014 Delete a skill\n\nInvoke a skill: `#<skill-name> <parameters>`";
|
|
4010
|
+
break;
|
|
4011
|
+
}
|
|
4012
|
+
if (subCmd === "new" || subCmd === "add" || subCmd === "create") {
|
|
4013
|
+
if (this.onSkillEditorScreenSetup) {
|
|
4014
|
+
this.onSkillEditorScreenSetup({ mode: "add" });
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
responseMessage = "\u274C Skill editor not available.";
|
|
4018
|
+
} else if (subCmd === "list" || subCmd === "ls") {
|
|
4019
|
+
const skills = skillStorage.list();
|
|
4020
|
+
if (skills.length === 0) {
|
|
4021
|
+
responseMessage = "\u{1F4ED} No skills saved yet.\n\nUse `/skill new` to create your first skill.";
|
|
4022
|
+
} else {
|
|
4023
|
+
const lines = skills.map((s) => {
|
|
4024
|
+
const date = new Date(s.updatedAt).toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
4025
|
+
return `\u2022 **${s.name}** (${s.accessLevel}) \u2014 ${s.promptPreview} _${date}_
|
|
4026
|
+
Invoke: \`#${s.name} <parameters>\``;
|
|
4027
|
+
});
|
|
4028
|
+
responseMessage = `\u{1F4DA} **Saved Skills (${skills.length})**
|
|
4029
|
+
|
|
4030
|
+
${lines.join("\n\n")}`;
|
|
4031
|
+
}
|
|
4032
|
+
} else if (subCmd === "edit") {
|
|
4033
|
+
const skillName = args.slice(1).join(" ").trim();
|
|
4034
|
+
if (!skillName) {
|
|
4035
|
+
responseMessage = "\u274C Usage: `/skill edit <name>`";
|
|
4036
|
+
break;
|
|
4037
|
+
}
|
|
4038
|
+
const skill = skillStorage.load(skillName);
|
|
4039
|
+
if (!skill) {
|
|
4040
|
+
responseMessage = `\u274C Skill "${skillName}" not found.
|
|
4041
|
+
|
|
4042
|
+
Use \`/skill list\` to see available skills.`;
|
|
4043
|
+
break;
|
|
4044
|
+
}
|
|
4045
|
+
if (this.onSkillEditorScreenSetup) {
|
|
4046
|
+
this.onSkillEditorScreenSetup({ mode: "edit", initialSkill: skill });
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
responseMessage = "\u274C Skill editor not available.";
|
|
4050
|
+
} else if (subCmd === "delete" || subCmd === "rm" || subCmd === "remove") {
|
|
4051
|
+
const skillName = args.slice(1).join(" ").trim();
|
|
4052
|
+
if (!skillName) {
|
|
4053
|
+
responseMessage = "\u274C Usage: `/skill delete <name>`";
|
|
4054
|
+
break;
|
|
4055
|
+
}
|
|
4056
|
+
const result = skillStorage.delete(skillName);
|
|
4057
|
+
if (result.success) {
|
|
4058
|
+
responseMessage = `\u2705 Skill "${skillName}" deleted.`;
|
|
4059
|
+
} else {
|
|
4060
|
+
responseMessage = `\u274C ${result.error}`;
|
|
4061
|
+
}
|
|
4062
|
+
} else {
|
|
4063
|
+
responseMessage = "\u274C Unknown skill subcommand.\n\nUse: `/skill new`, `/skill list`, `/skill edit <name>`, `/skill delete <name>`";
|
|
4064
|
+
}
|
|
4065
|
+
break;
|
|
4066
|
+
}
|
|
4067
|
+
case "compact":
|
|
4068
|
+
await this.forceCompactContext();
|
|
4069
|
+
return;
|
|
4070
|
+
// Don't send a response message — the tool UI element is sufficient
|
|
3560
4071
|
case "add-command":
|
|
3561
4072
|
case "add-command-auto-detect":
|
|
3562
4073
|
const { CustomCommandsManager } = await import("./utils/custom-commands-manager.js");
|
|
@@ -3899,9 +4410,7 @@ Usage:
|
|
|
3899
4410
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3900
4411
|
config: {
|
|
3901
4412
|
model: config.model,
|
|
3902
|
-
modelName: config.modelName
|
|
3903
|
-
enhancedQuality: config.enhancedQuality,
|
|
3904
|
-
autonomousMode: config.autonomousMode
|
|
4413
|
+
modelName: config.modelName
|
|
3905
4414
|
},
|
|
3906
4415
|
chats: fullChats,
|
|
3907
4416
|
metadata: {
|
|
@@ -3948,12 +4457,6 @@ Please try again later.`;
|
|
|
3948
4457
|
if (syncData.config.modelName) {
|
|
3949
4458
|
currentConfig.modelName = syncData.config.modelName;
|
|
3950
4459
|
}
|
|
3951
|
-
if (typeof syncData.config.enhancedQuality === "boolean") {
|
|
3952
|
-
currentConfig.enhancedQuality = syncData.config.enhancedQuality;
|
|
3953
|
-
}
|
|
3954
|
-
if (typeof syncData.config.autonomousMode === "boolean") {
|
|
3955
|
-
currentConfig.autonomousMode = syncData.config.autonomousMode;
|
|
3956
|
-
}
|
|
3957
4460
|
this.configManager.save(currentConfig);
|
|
3958
4461
|
}
|
|
3959
4462
|
let restoredChats = 0;
|
|
@@ -4337,17 +4840,20 @@ Use /background-task list to see running tasks.`);
|
|
|
4337
4840
|
}
|
|
4338
4841
|
/**
|
|
4339
4842
|
* Record a user shell command and its output to conversation history
|
|
4340
|
-
* This allows the AI to see what commands the user ran in command mode
|
|
4843
|
+
* This allows the AI to see what commands the user ran in command mode.
|
|
4844
|
+
* Large outputs are truncated using the same head+tail logic as AI tool
|
|
4845
|
+
* outputs, preventing context bloat from user-run commands.
|
|
4341
4846
|
*/
|
|
4342
4847
|
recordShellCommandToHistory(command, output, cwd, exitCode) {
|
|
4848
|
+
const truncatedOutput = this.truncateResult(output, "user_shell_command");
|
|
4343
4849
|
const shellCommandInfo = exitCode !== void 0 && exitCode !== 0 ? `[User ran shell command in ${cwd}]
|
|
4344
4850
|
Command: ${command}
|
|
4345
4851
|
Exit Code: ${exitCode}
|
|
4346
4852
|
Output:
|
|
4347
|
-
${
|
|
4853
|
+
${truncatedOutput}` : `[User ran shell command in ${cwd}]
|
|
4348
4854
|
Command: ${command}
|
|
4349
4855
|
Output:
|
|
4350
|
-
${
|
|
4856
|
+
${truncatedOutput || "(no output)"}`;
|
|
4351
4857
|
this.conversationHistory.push({
|
|
4352
4858
|
role: "user",
|
|
4353
4859
|
content: shellCommandInfo
|
|
@@ -4392,6 +4898,7 @@ ${output || "(no output)"}`;
|
|
|
4392
4898
|
thinkingDuration: msg.thinkingDuration,
|
|
4393
4899
|
taskCompletion: msg.taskCompletion,
|
|
4394
4900
|
planAccepted: msg.planAccepted,
|
|
4901
|
+
planQuestion: msg.planQuestion,
|
|
4395
4902
|
connectionStatus: msg.connectionStatus
|
|
4396
4903
|
// For SSH/WSL/Docker connection status boxes
|
|
4397
4904
|
}));
|
|
@@ -4477,6 +4984,7 @@ ${output || "(no output)"}`;
|
|
|
4477
4984
|
thinkingDuration: msg.thinkingDuration,
|
|
4478
4985
|
taskCompletion: msg.taskCompletion,
|
|
4479
4986
|
planAccepted: msg.planAccepted,
|
|
4987
|
+
planQuestion: msg.planQuestion,
|
|
4480
4988
|
connectionStatus: msg.connectionStatus
|
|
4481
4989
|
}));
|
|
4482
4990
|
}
|
|
@@ -4558,6 +5066,7 @@ ${output || "(no output)"}`;
|
|
|
4558
5066
|
thinkingDuration: msg.thinkingDuration,
|
|
4559
5067
|
taskCompletion: msg.taskCompletion,
|
|
4560
5068
|
planAccepted: msg.planAccepted,
|
|
5069
|
+
planQuestion: msg.planQuestion,
|
|
4561
5070
|
connectionStatus: msg.connectionStatus
|
|
4562
5071
|
// For SSH/WSL/Docker connection status boxes
|
|
4563
5072
|
}));
|
|
@@ -4770,6 +5279,7 @@ You have ${chat.messageCount} messages in AI context. Continue your conversation
|
|
|
4770
5279
|
if (this.checkpointManager) {
|
|
4771
5280
|
this.checkpointManager.setCurrentChatId(null);
|
|
4772
5281
|
}
|
|
5282
|
+
cleanupOrphanedToolOutputs();
|
|
4773
5283
|
if (currentContext.type === "local") {
|
|
4774
5284
|
this.cwdStack = [];
|
|
4775
5285
|
this.connectionCommandStack = [];
|
|
@@ -4840,36 +5350,47 @@ You have ${chat.messageCount} messages in AI context. Continue your conversation
|
|
|
4840
5350
|
You are currently in PLAN MODE. In this mode, you MUST:
|
|
4841
5351
|
|
|
4842
5352
|
1. **DO NOT execute any implementation tools** (no write_to_file, edit_file, execute_command, etc.)
|
|
4843
|
-
2.
|
|
4844
|
-
|
|
4845
|
-
### How to Create a Plan:
|
|
5353
|
+
2. If the request is ambiguous, use \`plan_ask_question\` to ask 1-2 clarifying questions first
|
|
5354
|
+
3. **Call the \`create_plan\` tool** to submit a detailed plan document
|
|
4846
5355
|
|
|
4847
|
-
|
|
4848
|
-
- A clear title describing what will be accomplished
|
|
4849
|
-
- A brief summary of the approach
|
|
4850
|
-
- An ordered list of specific, actionable tasks
|
|
5356
|
+
### Asking Clarifying Questions:
|
|
4851
5357
|
|
|
4852
|
-
|
|
5358
|
+
If you need clarification before planning, call \`plan_ask_question\`:
|
|
4853
5359
|
\`\`\`
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
tasks: [
|
|
4858
|
-
{ description: "Create sample input.csv with test data", complexity: "low" },
|
|
4859
|
-
{ description: "Write csv_filter.py with read/filter/write logic", complexity: "medium" },
|
|
4860
|
-
{ description: "Execute and verify the script works correctly", complexity: "low" }
|
|
4861
|
-
]
|
|
5360
|
+
plan_ask_question(
|
|
5361
|
+
question: "Which authentication approach do you prefer?",
|
|
5362
|
+
options: ["JWT tokens with refresh", "Session-based with Redis", "OAuth2 with third-party provider"]
|
|
4862
5363
|
)
|
|
4863
5364
|
\`\`\`
|
|
5365
|
+
The user can pick an option, type a custom answer, or skip. Only ask when it would significantly change the plan.
|
|
5366
|
+
|
|
5367
|
+
### How to Create a Plan:
|
|
5368
|
+
|
|
5369
|
+
Analyze the user's request, explore the codebase, then call \`create_plan\` with:
|
|
5370
|
+
- A clear title for the plan
|
|
5371
|
+
- A comprehensive markdown document with analysis, tables, and numbered implementation steps
|
|
5372
|
+
- An array of implementation step descriptions extracted from the document
|
|
4864
5373
|
|
|
4865
5374
|
### After Plan Approval:
|
|
4866
5375
|
|
|
4867
5376
|
Once the user approves the plan:
|
|
4868
|
-
1. Execute each
|
|
4869
|
-
2. After completing each
|
|
4870
|
-
3. After all
|
|
5377
|
+
1. Execute each step in order
|
|
5378
|
+
2. After completing each step, call \`mark_task_complete(step_number: N)\`
|
|
5379
|
+
3. After all steps are done, call \`task_complete\` with a summary
|
|
4871
5380
|
|
|
4872
|
-
**CRITICAL: In plan mode, ALWAYS call create_plan FIRST before any
|
|
5381
|
+
**CRITICAL: In plan mode, ALWAYS call create_plan FIRST before any implementation tools.**`;
|
|
5382
|
+
}
|
|
5383
|
+
/**
|
|
5384
|
+
* Toggle plan mode on/off (called by Ctrl+P shortcut)
|
|
5385
|
+
*/
|
|
5386
|
+
togglePlanMode() {
|
|
5387
|
+
if (this.commandMode) {
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
this.planMode = !this.planMode;
|
|
5391
|
+
if (this.onPlanModeChange) {
|
|
5392
|
+
this.onPlanModeChange(this.planMode);
|
|
5393
|
+
}
|
|
4873
5394
|
}
|
|
4874
5395
|
/**
|
|
4875
5396
|
* Toggle command mode on/off
|
|
@@ -5033,6 +5554,7 @@ Once the user approves the plan:
|
|
|
5033
5554
|
this.cwd,
|
|
5034
5555
|
(data) => {
|
|
5035
5556
|
output += data;
|
|
5557
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5036
5558
|
if (this.onToolStreamingOutput) {
|
|
5037
5559
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5038
5560
|
}
|
|
@@ -5054,6 +5576,14 @@ Once the user approves the plan:
|
|
|
5054
5576
|
result: output || "Command executed successfully",
|
|
5055
5577
|
arguments: { command, cwd: this.cwd, remoteContext, commandModeExecution: true }
|
|
5056
5578
|
});
|
|
5579
|
+
const newCwd = resolvePostCommandCwd(this.cwd, command);
|
|
5580
|
+
if (newCwd && newCwd !== this.cwd) {
|
|
5581
|
+
this.cwd = newCwd;
|
|
5582
|
+
this.contextManager.updateWorkingDirectory(newCwd);
|
|
5583
|
+
if (this.onCwdChange) {
|
|
5584
|
+
this.onCwdChange(newCwd);
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5057
5587
|
}
|
|
5058
5588
|
}
|
|
5059
5589
|
this.recordShellCommandToHistory(command, output, this.cwd, exitCode);
|
|
@@ -5073,6 +5603,7 @@ Once the user approves the plan:
|
|
|
5073
5603
|
remoteCwd,
|
|
5074
5604
|
(data) => {
|
|
5075
5605
|
output += data;
|
|
5606
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5076
5607
|
if (this.onToolStreamingOutput) {
|
|
5077
5608
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5078
5609
|
}
|
|
@@ -5137,6 +5668,7 @@ Once the user approves the plan:
|
|
|
5137
5668
|
parentContext.metadata.workingDirectory || "~",
|
|
5138
5669
|
(data) => {
|
|
5139
5670
|
output += data;
|
|
5671
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5140
5672
|
if (this.onToolStreamingOutput) {
|
|
5141
5673
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5142
5674
|
}
|
|
@@ -5190,6 +5722,7 @@ Once the user approves the plan:
|
|
|
5190
5722
|
remoteCwd,
|
|
5191
5723
|
(data) => {
|
|
5192
5724
|
output += data;
|
|
5725
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5193
5726
|
if (this.onToolStreamingOutput) {
|
|
5194
5727
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5195
5728
|
}
|
|
@@ -5250,6 +5783,7 @@ Once the user approves the plan:
|
|
|
5250
5783
|
remoteCwd,
|
|
5251
5784
|
(data) => {
|
|
5252
5785
|
output += data;
|
|
5786
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5253
5787
|
if (this.onToolStreamingOutput) {
|
|
5254
5788
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5255
5789
|
}
|