centaurus-cli 3.1.3 → 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 +685 -153
- 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 +4 -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/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/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";
|
|
@@ -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({
|
|
@@ -1033,18 +1141,22 @@ Begin executing now, starting with Step 1.`;
|
|
|
1033
1141
|
});
|
|
1034
1142
|
}
|
|
1035
1143
|
/**
|
|
1036
|
-
* 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.
|
|
1037
1148
|
*/
|
|
1038
|
-
truncateResult(result,
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
+
);
|
|
1048
1160
|
}
|
|
1049
1161
|
calculateUsagePercent(tokenCount, maxTokens) {
|
|
1050
1162
|
if (!Number.isFinite(maxTokens) || maxTokens <= 0) {
|
|
@@ -1276,7 +1388,68 @@ ${CentaurusCLI.TERMINAL_COMPACTION_NOTICE}`;
|
|
|
1276
1388
|
};
|
|
1277
1389
|
}
|
|
1278
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
|
+
}
|
|
1279
1451
|
async initialize() {
|
|
1452
|
+
cleanupStaleToolOutputs();
|
|
1280
1453
|
this.toolRegistry.register(viewFileTool);
|
|
1281
1454
|
this.toolRegistry.register(writeToFileTool);
|
|
1282
1455
|
this.toolRegistry.register(editFileTool);
|
|
@@ -1289,6 +1462,7 @@ ${CentaurusCLI.TERMINAL_COMPACTION_NOTICE}`;
|
|
|
1289
1462
|
this.toolRegistry.register(inspectSymbolTool);
|
|
1290
1463
|
this.toolRegistry.register(createPlanTool);
|
|
1291
1464
|
this.toolRegistry.register(markTaskCompleteTool);
|
|
1465
|
+
this.toolRegistry.register(planAskQuestionTool);
|
|
1292
1466
|
this.toolRegistry.register(webSearchTool);
|
|
1293
1467
|
this.toolRegistry.register(fetchUrlTool);
|
|
1294
1468
|
this.toolRegistry.register(taskCompleteTool);
|
|
@@ -1300,6 +1474,7 @@ ${CentaurusCLI.TERMINAL_COMPACTION_NOTICE}`;
|
|
|
1300
1474
|
this.toolRegistry.register(workflowTool);
|
|
1301
1475
|
this.toolRegistry.register(fastContextTool);
|
|
1302
1476
|
this.toolRegistry.register(addMcpTool);
|
|
1477
|
+
this.toolRegistry.register(todoWriteTool);
|
|
1303
1478
|
SubAgentManager.initialize(this.toolRegistry);
|
|
1304
1479
|
SubAgentManager.setOnSubAgentCountChange((count) => {
|
|
1305
1480
|
if (this.onSubAgentCountChange) {
|
|
@@ -1383,13 +1558,34 @@ Press Enter to continue...
|
|
|
1383
1558
|
return config.modelName || config.model || "gemini-2.5-flash";
|
|
1384
1559
|
}
|
|
1385
1560
|
/**
|
|
1386
|
-
* 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.
|
|
1387
1564
|
*/
|
|
1388
1565
|
cancelCurrentRequest() {
|
|
1389
1566
|
if (this.currentAbortController) {
|
|
1390
1567
|
this.requestIntentionallyAborted = true;
|
|
1391
1568
|
this.currentAbortController.abort();
|
|
1392
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
|
+
}
|
|
1393
1589
|
}
|
|
1394
1590
|
}
|
|
1395
1591
|
/**
|
|
@@ -1592,7 +1788,8 @@ Press Enter to continue...
|
|
|
1592
1788
|
`
|
|
1593
1789
|
);
|
|
1594
1790
|
}
|
|
1595
|
-
const resolvedMessage = augmentedPrompt.replace(/@([^\s@]+)/g, (match,
|
|
1791
|
+
const resolvedMessage = augmentedPrompt.replace(/@"([^"]+)"|@([^\s@]+)/g, (match, quotedPath, unquotedPath) => {
|
|
1792
|
+
const relPath = quotedPath || unquotedPath;
|
|
1596
1793
|
if (relPath.startsWith("rules:")) return match;
|
|
1597
1794
|
const absPath = path.resolve(this.cwd, relPath);
|
|
1598
1795
|
if (fs.existsSync(absPath)) return absPath;
|
|
@@ -1663,17 +1860,21 @@ Use /session-limits to check your quota status.`;
|
|
|
1663
1860
|
}
|
|
1664
1861
|
let userMessageContent = resolvedMessage;
|
|
1665
1862
|
if (this.planMode && !getCurrentPlan()?.isActive) {
|
|
1666
|
-
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]
|
|
1667
1864
|
|
|
1668
1865
|
User Request: ${resolvedMessage}
|
|
1669
1866
|
|
|
1670
1867
|
CRITICAL INSTRUCTIONS:
|
|
1671
1868
|
1. You are in PLANNING MODE - DO NOT implement anything directly
|
|
1672
|
-
2.
|
|
1673
|
-
3.
|
|
1674
|
-
|
|
1675
|
-
-
|
|
1676
|
-
|
|
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.
|
|
1677
1878
|
|
|
1678
1879
|
DO NOT use write_to_file, edit_file, or execute_command until the plan is approved.`;
|
|
1679
1880
|
userMessageContent = planModePrefix;
|
|
@@ -1788,7 +1989,22 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1788
1989
|
}
|
|
1789
1990
|
}
|
|
1790
1991
|
try {
|
|
1791
|
-
|
|
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
|
+
}
|
|
1792
2008
|
const context = {
|
|
1793
2009
|
cwd: this.cwd,
|
|
1794
2010
|
contextManager: this.contextManager,
|
|
@@ -1810,6 +2026,9 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1810
2026
|
return true;
|
|
1811
2027
|
},
|
|
1812
2028
|
onStreamingOutput: (chunk, type, toolName) => {
|
|
2029
|
+
if (this.customTunnelCommand && (toolName === "execute_command" || !toolName)) {
|
|
2030
|
+
this.tunnelOutputBuffer += chunk;
|
|
2031
|
+
}
|
|
1813
2032
|
if (this.onToolStreamingOutput) {
|
|
1814
2033
|
this.onToolStreamingOutput({ toolName: toolName || "execute_command", chunk, type });
|
|
1815
2034
|
}
|
|
@@ -1827,6 +2046,19 @@ DO NOT use write_to_file, edit_file, or execute_command until the plan is approv
|
|
|
1827
2046
|
messages = this.aiContextInjector.injectSubshellContext(messages, currentContext);
|
|
1828
2047
|
const connectedMcpServers = this.getConnectedMCPServerNames();
|
|
1829
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
|
+
}
|
|
1830
2062
|
let environmentContext = this.getEnvironmentContext();
|
|
1831
2063
|
const mode = this.getMode();
|
|
1832
2064
|
let finalAssistantMessage = "";
|
|
@@ -1999,7 +2231,7 @@ Use /session-limits to check your quota status.`;
|
|
|
1999
2231
|
thoughtContent = "";
|
|
2000
2232
|
}
|
|
2001
2233
|
toolCalls.push(chunk.toolCall);
|
|
2002
|
-
const SPECIAL_TOOLS = ["task_complete", "create_plan", "mark_task_complete"];
|
|
2234
|
+
const SPECIAL_TOOLS = ["task_complete", "create_plan", "mark_task_complete", "plan_ask_question"];
|
|
2003
2235
|
if (SPECIAL_TOOLS.includes(toolCall.name)) {
|
|
2004
2236
|
if (this.onToolExecutionUpdate) {
|
|
2005
2237
|
this.onToolExecutionUpdate({
|
|
@@ -2013,6 +2245,62 @@ Use /session-limits to check your quota status.`;
|
|
|
2013
2245
|
isToolExecuting = true;
|
|
2014
2246
|
toolsExecutedInStream = true;
|
|
2015
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
|
+
}
|
|
2016
2304
|
const reasonText = toolCall.arguments.reason_text;
|
|
2017
2305
|
const isShellInput = toolCall.name === "execute_command" && toolCall.arguments.shell_input;
|
|
2018
2306
|
if (reasonText && !isShellInput && this.onResponseStreamCallback) {
|
|
@@ -2037,6 +2325,33 @@ Use /session-limits to check your quota status.`;
|
|
|
2037
2325
|
if (result.success) {
|
|
2038
2326
|
conversationLogger.logToolResult(toolCall.name, toolCall.id, result.result, true);
|
|
2039
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
|
+
}
|
|
2040
2355
|
if (["write_to_file", "edit_file", "multi_edit_file"].includes(toolCall.name)) {
|
|
2041
2356
|
this.updateFileChangeSummary();
|
|
2042
2357
|
}
|
|
@@ -2053,7 +2368,7 @@ Use /session-limits to check your quota status.`;
|
|
|
2053
2368
|
inStreamToolResults.push({
|
|
2054
2369
|
tool_call_id: toolCall.id,
|
|
2055
2370
|
name: toolCall.name,
|
|
2056
|
-
result: this.truncateResult(sanitizedResult)
|
|
2371
|
+
result: this.truncateResult(sanitizedResult, toolCall.name)
|
|
2057
2372
|
});
|
|
2058
2373
|
} else {
|
|
2059
2374
|
conversationLogger.logToolResult(toolCall.name, toolCall.id, null, false, result.error);
|
|
@@ -2152,6 +2467,50 @@ Use /session-limits to check your quota status.`;
|
|
|
2152
2467
|
clearPlan();
|
|
2153
2468
|
break;
|
|
2154
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
|
+
}
|
|
2155
2514
|
if (toolCall.name === "create_plan") {
|
|
2156
2515
|
const execResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
2157
2516
|
const result2 = execResult.success ? String(execResult.result) : `Error: ${execResult.error}`;
|
|
@@ -2189,13 +2548,22 @@ Use /session-limits to check your quota status.`;
|
|
|
2189
2548
|
content: "Plan approved by user. Now switching to execution mode."
|
|
2190
2549
|
});
|
|
2191
2550
|
handledToolCallIds.add(toolCall.id);
|
|
2192
|
-
const
|
|
2551
|
+
const planContext = getPlanContextForPrompt();
|
|
2193
2552
|
const originalRequest = this.pendingPlanRequest || message;
|
|
2194
|
-
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}
|
|
2195
2559
|
|
|
2196
2560
|
Original Request: ${originalRequest}
|
|
2197
2561
|
|
|
2198
|
-
|
|
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.`;
|
|
2199
2567
|
this.conversationHistory.push({
|
|
2200
2568
|
role: "user",
|
|
2201
2569
|
content: executionPrompt
|
|
@@ -2225,7 +2593,7 @@ Complete the current task. After finishing each subtask, call mark_task_complete
|
|
|
2225
2593
|
this.conversationHistory.push({
|
|
2226
2594
|
role: "tool",
|
|
2227
2595
|
tool_call_id: toolCall.id,
|
|
2228
|
-
content: `Plan created: "${plan.title}" with ${plan.
|
|
2596
|
+
content: `Plan created: "${plan.title}" with ${plan.implementationSteps.length} steps. Waiting for user approval.`
|
|
2229
2597
|
});
|
|
2230
2598
|
handledToolCallIds.add(toolCall.id);
|
|
2231
2599
|
messages = [...this.conversationHistory];
|
|
@@ -2285,74 +2653,27 @@ Complete the current task. After finishing each subtask, call mark_task_complete
|
|
|
2285
2653
|
const completionJson = result2.substring("TASK_COMPLETED:".length);
|
|
2286
2654
|
try {
|
|
2287
2655
|
const completion = JSON.parse(completionJson);
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
completion.totalCount,
|
|
2298
|
-
completion.completionNote,
|
|
2299
|
-
completion.taskDescription
|
|
2300
|
-
// Pass the actual task/subtask description
|
|
2301
|
-
);
|
|
2302
|
-
}
|
|
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
|
+
);
|
|
2303
2665
|
}
|
|
2304
|
-
const displayType = completion.type === "subtask" ? "Subtask" : "Task";
|
|
2305
2666
|
this.notifyToolStatus(
|
|
2306
2667
|
toolCall.name,
|
|
2307
2668
|
"completed",
|
|
2308
2669
|
toolCall.arguments,
|
|
2309
|
-
|
|
2670
|
+
`Step ${completion.stepNumber} of ${completion.totalCount} completed: ${completion.stepDescription}`
|
|
2310
2671
|
);
|
|
2311
|
-
if (completion.mainTaskComplete || completion.type === "task") {
|
|
2312
|
-
const nextPhase = getCurrentPhase();
|
|
2313
|
-
if (nextPhase && !completion.allComplete) {
|
|
2314
|
-
const phaseContext = getPhaseContextForPrompt();
|
|
2315
|
-
toolResults.push({
|
|
2316
|
-
tool_call_id: toolCall.id,
|
|
2317
|
-
name: toolCall.name,
|
|
2318
|
-
result: `Task completed. Moving to Task ${nextPhase.taskNumber}: ${nextPhase.task.description}`
|
|
2319
|
-
});
|
|
2320
|
-
const nextPhaseAssistantMsg = {
|
|
2321
|
-
role: "assistant",
|
|
2322
|
-
content: "",
|
|
2323
|
-
tool_calls: [toolCall]
|
|
2324
|
-
};
|
|
2325
|
-
if (currentTurnThinking) {
|
|
2326
|
-
nextPhaseAssistantMsg.thinking = currentTurnThinking;
|
|
2327
|
-
}
|
|
2328
|
-
if (currentTurnThinkingSignature) {
|
|
2329
|
-
nextPhaseAssistantMsg.thinkingSignature = currentTurnThinkingSignature;
|
|
2330
|
-
}
|
|
2331
|
-
this.conversationHistory.push(nextPhaseAssistantMsg);
|
|
2332
|
-
this.conversationHistory.push({
|
|
2333
|
-
role: "tool",
|
|
2334
|
-
tool_call_id: toolCall.id,
|
|
2335
|
-
content: `Task completed. Now starting Task ${nextPhase.taskNumber}.`
|
|
2336
|
-
});
|
|
2337
|
-
this.conversationHistory.push({
|
|
2338
|
-
role: "user",
|
|
2339
|
-
content: `${phaseContext}
|
|
2340
|
-
|
|
2341
|
-
Continue with the next task. Complete each subtask and call mark_task_complete for each one.`
|
|
2342
|
-
});
|
|
2343
|
-
handledToolCallIds.add(toolCall.id);
|
|
2344
|
-
messages = getMessagesForContext();
|
|
2345
|
-
continue;
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
2672
|
toolResults.push({
|
|
2349
2673
|
tool_call_id: toolCall.id,
|
|
2350
2674
|
name: toolCall.name,
|
|
2351
|
-
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.`
|
|
2352
2676
|
});
|
|
2353
|
-
if (completion.allComplete) {
|
|
2354
|
-
toolResults[toolResults.length - 1].result = "All tasks in the plan are now completed! Output your summary of what was accomplished, then call task_complete().";
|
|
2355
|
-
}
|
|
2356
2677
|
} catch (parseError) {
|
|
2357
2678
|
logWarning(`Failed to parse task completion: ${parseError?.message || parseError}`);
|
|
2358
2679
|
}
|
|
@@ -2388,6 +2709,41 @@ The system has stopped to prevent an infinite loop.
|
|
|
2388
2709
|
}
|
|
2389
2710
|
const currentCtx = this.contextManager.getCurrentContext();
|
|
2390
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
|
+
}
|
|
2391
2747
|
let remoteContext;
|
|
2392
2748
|
if (currentCtx.type !== "local") {
|
|
2393
2749
|
const metadata = currentCtx.metadata;
|
|
@@ -2461,7 +2817,7 @@ The system has stopped to prevent an infinite loop.
|
|
|
2461
2817
|
}
|
|
2462
2818
|
const sanitizedResult = sanitizeForContext(toolCall.name, parsedResult, toolCall.arguments);
|
|
2463
2819
|
conversationLogger.logToolResult(`${toolCall.name} (SANITIZED_CONTEXT)`, toolCall.id, sanitizedResult, true);
|
|
2464
|
-
const truncatedResult = this.truncateResult(sanitizedResult);
|
|
2820
|
+
const truncatedResult = this.truncateResult(sanitizedResult, toolCall.name);
|
|
2465
2821
|
toolResults.push({
|
|
2466
2822
|
tool_call_id: toolCall.id,
|
|
2467
2823
|
name: toolCall.name,
|
|
@@ -2847,13 +3203,11 @@ ${nextInterrupt}`;
|
|
|
2847
3203
|
/sync - Sync data to/from cloud (upload/restore)
|
|
2848
3204
|
/config - View current configuration
|
|
2849
3205
|
/model - Select from available Google models
|
|
2850
|
-
/plan - Toggle plan mode for complex implementations
|
|
3206
|
+
/plan - Toggle plan mode for complex implementations (or Ctrl+P)
|
|
2851
3207
|
/mcp - Manage configured MCP servers and tools
|
|
2852
3208
|
/docs - Open Centaurus documentation in browser
|
|
2853
3209
|
/copy-chat-context - Copy chat as readable text to clipboard
|
|
2854
3210
|
/session-limits - View session quota usage and limits
|
|
2855
|
-
/quality - Toggle enhanced quality features (thinking protocol, validation)
|
|
2856
|
-
/autonomous - Toggle autonomous mode (Silent Operator with task_complete)
|
|
2857
3211
|
/sign-in - Sign in with Google (if not already signed in)
|
|
2858
3212
|
/logout - Sign out, clear session, and exit CLI
|
|
2859
3213
|
/exit - Exit the application
|
|
@@ -3107,18 +3461,6 @@ Exiting CLI anyway...`;
|
|
|
3107
3461
|
this.onPlanModeChange(this.planMode);
|
|
3108
3462
|
}
|
|
3109
3463
|
return;
|
|
3110
|
-
case "quality":
|
|
3111
|
-
const currentQuality = this.configManager.get("enhancedQuality");
|
|
3112
|
-
const newQuality = currentQuality === false;
|
|
3113
|
-
this.configManager.set("enhancedQuality", newQuality);
|
|
3114
|
-
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";
|
|
3115
|
-
break;
|
|
3116
|
-
case "autonomous":
|
|
3117
|
-
const currentAutonomous = this.configManager.get("autonomousMode");
|
|
3118
|
-
const newAutonomous = !currentAutonomous;
|
|
3119
|
-
this.configManager.set("autonomousMode", newAutonomous);
|
|
3120
|
-
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";
|
|
3121
|
-
break;
|
|
3122
3464
|
case "clear":
|
|
3123
3465
|
const clearCancelMsg = this.cancelWorkflowLearning("Clearing chat session");
|
|
3124
3466
|
if (clearCancelMsg && this.onDirectMessageCallback) {
|
|
@@ -3144,17 +3486,9 @@ Usage: /config set <key> <value>`;
|
|
|
3144
3486
|
} catch (error) {
|
|
3145
3487
|
responseMessage = `\u274C Failed to update configuration: ${error.message}`;
|
|
3146
3488
|
}
|
|
3147
|
-
} else if (configKey === "enhancedQuality" || configKey === "autonomousMode") {
|
|
3148
|
-
const boolValue = configValue.toLowerCase() === "true" || configValue === "1";
|
|
3149
|
-
try {
|
|
3150
|
-
this.configManager.set(configKey, boolValue);
|
|
3151
|
-
responseMessage = `\u2705 Configuration updated: ${configKey} = ${boolValue}`;
|
|
3152
|
-
} catch (error) {
|
|
3153
|
-
responseMessage = `\u274C Failed to update configuration: ${error.message}`;
|
|
3154
|
-
}
|
|
3155
3489
|
} else {
|
|
3156
3490
|
responseMessage = `\u274C Error: Unknown config key: ${configKey}
|
|
3157
|
-
Valid keys: model
|
|
3491
|
+
Valid keys: model`;
|
|
3158
3492
|
}
|
|
3159
3493
|
} else {
|
|
3160
3494
|
const config = this.configManager.load();
|
|
@@ -3165,6 +3499,7 @@ Valid keys: model, enhancedQuality, autonomousMode`;
|
|
|
3165
3499
|
Version: ${currentVersion}
|
|
3166
3500
|
Model: ${config.model || "gemini-2.5-flash (default)"}
|
|
3167
3501
|
AI Auto-Suggest: ${config.aiAutoSuggest === true ? "\u2705 Enabled" : "\u274C Disabled"}
|
|
3502
|
+
Limit Chat History: ${config.limitChatHistory !== false ? "\u2705 Enabled" : "\u274C Disabled"}
|
|
3168
3503
|
Authentication: ${apiClient.isAuthenticated() ? "\u2705 Signed in" : "\u274C Not signed in"}`;
|
|
3169
3504
|
}
|
|
3170
3505
|
break;
|
|
@@ -3186,8 +3521,84 @@ Authentication: ${apiClient.isAuthenticated() ? "\u2705 Signed in" : "\u274C Not
|
|
|
3186
3521
|
} else {
|
|
3187
3522
|
responseMessage = "\u274C Invalid option. Usage: `/settings auto-suggest on` or `/settings auto-suggest off`";
|
|
3188
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
|
+
}
|
|
3189
3600
|
} else {
|
|
3190
|
-
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>`";
|
|
3191
3602
|
}
|
|
3192
3603
|
break;
|
|
3193
3604
|
case "model":
|
|
@@ -3253,19 +3664,14 @@ Then try /models local again.`;
|
|
|
3253
3664
|
if (this.onShowPickerCallback) {
|
|
3254
3665
|
const config = this.configManager.load();
|
|
3255
3666
|
const currentModelName = config.modelName || "";
|
|
3256
|
-
const isCurrentCloud = config.isLocalModel !== true;
|
|
3257
3667
|
const modelsConfig = await fetchModelsConfig();
|
|
3258
3668
|
this.onShowPickerCallback({
|
|
3259
3669
|
message: "Select Cloud Model",
|
|
3260
3670
|
type: "model",
|
|
3261
|
-
choices:
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
value: `${index}`
|
|
3266
|
-
// Use index as unique identifier
|
|
3267
|
-
};
|
|
3268
|
-
})
|
|
3671
|
+
choices: [],
|
|
3672
|
+
// Not used - ModelPicker reads modelConfigs directly
|
|
3673
|
+
modelConfigs: modelsConfig.models,
|
|
3674
|
+
currentModelName
|
|
3269
3675
|
});
|
|
3270
3676
|
return;
|
|
3271
3677
|
}
|
|
@@ -3559,6 +3965,109 @@ Restored ${result.restored} files, removed ${result.removed} files.`;
|
|
|
3559
3965
|
}
|
|
3560
3966
|
break;
|
|
3561
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
|
|
3562
4071
|
case "add-command":
|
|
3563
4072
|
case "add-command-auto-detect":
|
|
3564
4073
|
const { CustomCommandsManager } = await import("./utils/custom-commands-manager.js");
|
|
@@ -3901,9 +4410,7 @@ Usage:
|
|
|
3901
4410
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3902
4411
|
config: {
|
|
3903
4412
|
model: config.model,
|
|
3904
|
-
modelName: config.modelName
|
|
3905
|
-
enhancedQuality: config.enhancedQuality,
|
|
3906
|
-
autonomousMode: config.autonomousMode
|
|
4413
|
+
modelName: config.modelName
|
|
3907
4414
|
},
|
|
3908
4415
|
chats: fullChats,
|
|
3909
4416
|
metadata: {
|
|
@@ -3950,12 +4457,6 @@ Please try again later.`;
|
|
|
3950
4457
|
if (syncData.config.modelName) {
|
|
3951
4458
|
currentConfig.modelName = syncData.config.modelName;
|
|
3952
4459
|
}
|
|
3953
|
-
if (typeof syncData.config.enhancedQuality === "boolean") {
|
|
3954
|
-
currentConfig.enhancedQuality = syncData.config.enhancedQuality;
|
|
3955
|
-
}
|
|
3956
|
-
if (typeof syncData.config.autonomousMode === "boolean") {
|
|
3957
|
-
currentConfig.autonomousMode = syncData.config.autonomousMode;
|
|
3958
|
-
}
|
|
3959
4460
|
this.configManager.save(currentConfig);
|
|
3960
4461
|
}
|
|
3961
4462
|
let restoredChats = 0;
|
|
@@ -4339,17 +4840,20 @@ Use /background-task list to see running tasks.`);
|
|
|
4339
4840
|
}
|
|
4340
4841
|
/**
|
|
4341
4842
|
* Record a user shell command and its output to conversation history
|
|
4342
|
-
* 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.
|
|
4343
4846
|
*/
|
|
4344
4847
|
recordShellCommandToHistory(command, output, cwd, exitCode) {
|
|
4848
|
+
const truncatedOutput = this.truncateResult(output, "user_shell_command");
|
|
4345
4849
|
const shellCommandInfo = exitCode !== void 0 && exitCode !== 0 ? `[User ran shell command in ${cwd}]
|
|
4346
4850
|
Command: ${command}
|
|
4347
4851
|
Exit Code: ${exitCode}
|
|
4348
4852
|
Output:
|
|
4349
|
-
${
|
|
4853
|
+
${truncatedOutput}` : `[User ran shell command in ${cwd}]
|
|
4350
4854
|
Command: ${command}
|
|
4351
4855
|
Output:
|
|
4352
|
-
${
|
|
4856
|
+
${truncatedOutput || "(no output)"}`;
|
|
4353
4857
|
this.conversationHistory.push({
|
|
4354
4858
|
role: "user",
|
|
4355
4859
|
content: shellCommandInfo
|
|
@@ -4394,6 +4898,7 @@ ${output || "(no output)"}`;
|
|
|
4394
4898
|
thinkingDuration: msg.thinkingDuration,
|
|
4395
4899
|
taskCompletion: msg.taskCompletion,
|
|
4396
4900
|
planAccepted: msg.planAccepted,
|
|
4901
|
+
planQuestion: msg.planQuestion,
|
|
4397
4902
|
connectionStatus: msg.connectionStatus
|
|
4398
4903
|
// For SSH/WSL/Docker connection status boxes
|
|
4399
4904
|
}));
|
|
@@ -4479,6 +4984,7 @@ ${output || "(no output)"}`;
|
|
|
4479
4984
|
thinkingDuration: msg.thinkingDuration,
|
|
4480
4985
|
taskCompletion: msg.taskCompletion,
|
|
4481
4986
|
planAccepted: msg.planAccepted,
|
|
4987
|
+
planQuestion: msg.planQuestion,
|
|
4482
4988
|
connectionStatus: msg.connectionStatus
|
|
4483
4989
|
}));
|
|
4484
4990
|
}
|
|
@@ -4560,6 +5066,7 @@ ${output || "(no output)"}`;
|
|
|
4560
5066
|
thinkingDuration: msg.thinkingDuration,
|
|
4561
5067
|
taskCompletion: msg.taskCompletion,
|
|
4562
5068
|
planAccepted: msg.planAccepted,
|
|
5069
|
+
planQuestion: msg.planQuestion,
|
|
4563
5070
|
connectionStatus: msg.connectionStatus
|
|
4564
5071
|
// For SSH/WSL/Docker connection status boxes
|
|
4565
5072
|
}));
|
|
@@ -4772,6 +5279,7 @@ You have ${chat.messageCount} messages in AI context. Continue your conversation
|
|
|
4772
5279
|
if (this.checkpointManager) {
|
|
4773
5280
|
this.checkpointManager.setCurrentChatId(null);
|
|
4774
5281
|
}
|
|
5282
|
+
cleanupOrphanedToolOutputs();
|
|
4775
5283
|
if (currentContext.type === "local") {
|
|
4776
5284
|
this.cwdStack = [];
|
|
4777
5285
|
this.connectionCommandStack = [];
|
|
@@ -4842,36 +5350,47 @@ You have ${chat.messageCount} messages in AI context. Continue your conversation
|
|
|
4842
5350
|
You are currently in PLAN MODE. In this mode, you MUST:
|
|
4843
5351
|
|
|
4844
5352
|
1. **DO NOT execute any implementation tools** (no write_to_file, edit_file, execute_command, etc.)
|
|
4845
|
-
2.
|
|
4846
|
-
|
|
4847
|
-
### 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
|
|
4848
5355
|
|
|
4849
|
-
|
|
4850
|
-
- A clear title describing what will be accomplished
|
|
4851
|
-
- A brief summary of the approach
|
|
4852
|
-
- An ordered list of specific, actionable tasks
|
|
5356
|
+
### Asking Clarifying Questions:
|
|
4853
5357
|
|
|
4854
|
-
|
|
5358
|
+
If you need clarification before planning, call \`plan_ask_question\`:
|
|
4855
5359
|
\`\`\`
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
tasks: [
|
|
4860
|
-
{ description: "Create sample input.csv with test data", complexity: "low" },
|
|
4861
|
-
{ description: "Write csv_filter.py with read/filter/write logic", complexity: "medium" },
|
|
4862
|
-
{ description: "Execute and verify the script works correctly", complexity: "low" }
|
|
4863
|
-
]
|
|
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"]
|
|
4864
5363
|
)
|
|
4865
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
|
|
4866
5373
|
|
|
4867
5374
|
### After Plan Approval:
|
|
4868
5375
|
|
|
4869
5376
|
Once the user approves the plan:
|
|
4870
|
-
1. Execute each
|
|
4871
|
-
2. After completing each
|
|
4872
|
-
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
|
|
4873
5380
|
|
|
4874
|
-
**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
|
+
}
|
|
4875
5394
|
}
|
|
4876
5395
|
/**
|
|
4877
5396
|
* Toggle command mode on/off
|
|
@@ -5035,6 +5554,7 @@ Once the user approves the plan:
|
|
|
5035
5554
|
this.cwd,
|
|
5036
5555
|
(data) => {
|
|
5037
5556
|
output += data;
|
|
5557
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5038
5558
|
if (this.onToolStreamingOutput) {
|
|
5039
5559
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5040
5560
|
}
|
|
@@ -5056,6 +5576,14 @@ Once the user approves the plan:
|
|
|
5056
5576
|
result: output || "Command executed successfully",
|
|
5057
5577
|
arguments: { command, cwd: this.cwd, remoteContext, commandModeExecution: true }
|
|
5058
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
|
+
}
|
|
5059
5587
|
}
|
|
5060
5588
|
}
|
|
5061
5589
|
this.recordShellCommandToHistory(command, output, this.cwd, exitCode);
|
|
@@ -5075,6 +5603,7 @@ Once the user approves the plan:
|
|
|
5075
5603
|
remoteCwd,
|
|
5076
5604
|
(data) => {
|
|
5077
5605
|
output += data;
|
|
5606
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5078
5607
|
if (this.onToolStreamingOutput) {
|
|
5079
5608
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5080
5609
|
}
|
|
@@ -5139,6 +5668,7 @@ Once the user approves the plan:
|
|
|
5139
5668
|
parentContext.metadata.workingDirectory || "~",
|
|
5140
5669
|
(data) => {
|
|
5141
5670
|
output += data;
|
|
5671
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5142
5672
|
if (this.onToolStreamingOutput) {
|
|
5143
5673
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5144
5674
|
}
|
|
@@ -5192,6 +5722,7 @@ Once the user approves the plan:
|
|
|
5192
5722
|
remoteCwd,
|
|
5193
5723
|
(data) => {
|
|
5194
5724
|
output += data;
|
|
5725
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5195
5726
|
if (this.onToolStreamingOutput) {
|
|
5196
5727
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5197
5728
|
}
|
|
@@ -5252,6 +5783,7 @@ Once the user approves the plan:
|
|
|
5252
5783
|
remoteCwd,
|
|
5253
5784
|
(data) => {
|
|
5254
5785
|
output += data;
|
|
5786
|
+
if (this.customTunnelCommand) this.tunnelOutputBuffer += data;
|
|
5255
5787
|
if (this.onToolStreamingOutput) {
|
|
5256
5788
|
this.onToolStreamingOutput({ toolName: "execute_command", chunk: data, type: "stdout" });
|
|
5257
5789
|
}
|