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.
Files changed (142) hide show
  1. package/dist/cli-adapter.js +689 -155
  2. package/dist/cli-adapter.js.map +1 -1
  3. package/dist/config/defaultConfig.js +1 -4
  4. package/dist/config/defaultConfig.js.map +1 -1
  5. package/dist/config/models.js +6 -0
  6. package/dist/config/models.js.map +1 -1
  7. package/dist/config/slash-commands.js +66 -2
  8. package/dist/config/slash-commands.js.map +1 -1
  9. package/dist/config/types.js +4 -4
  10. package/dist/config/types.js.map +1 -1
  11. package/dist/index.js +36 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/services/ai-context-injector.js +109 -0
  14. package/dist/services/ai-context-injector.js.map +1 -1
  15. package/dist/services/ai-service-client.js +3 -2
  16. package/dist/services/ai-service-client.js.map +1 -1
  17. package/dist/services/api-client.js.map +1 -1
  18. package/dist/services/background-task-manager.js +59 -0
  19. package/dist/services/background-task-manager.js.map +1 -1
  20. package/dist/services/local-chat-storage.js +2 -0
  21. package/dist/services/local-chat-storage.js.map +1 -1
  22. package/dist/services/skill-storage.js +141 -0
  23. package/dist/services/skill-storage.js.map +1 -0
  24. package/dist/services/sub-agent-manager.js +49 -8
  25. package/dist/services/sub-agent-manager.js.map +1 -1
  26. package/dist/services/warpify-detector.js +17 -5
  27. package/dist/services/warpify-detector.js.map +1 -1
  28. package/dist/tools/background-command.js +5 -2
  29. package/dist/tools/background-command.js.map +1 -1
  30. package/dist/tools/command.js +367 -109
  31. package/dist/tools/command.js.map +1 -1
  32. package/dist/tools/file-ops.js +23 -6
  33. package/dist/tools/file-ops.js.map +1 -1
  34. package/dist/tools/plan-mode.js +184 -336
  35. package/dist/tools/plan-mode.js.map +1 -1
  36. package/dist/tools/sub-agent.js +24 -5
  37. package/dist/tools/sub-agent.js.map +1 -1
  38. package/dist/tools/todo-list.js +157 -0
  39. package/dist/tools/todo-list.js.map +1 -0
  40. package/dist/types/skill.js +30 -0
  41. package/dist/types/skill.js.map +1 -0
  42. package/dist/ui/components/App.js +956 -162
  43. package/dist/ui/components/App.js.map +1 -1
  44. package/dist/ui/components/AuthScreen.js +3 -1
  45. package/dist/ui/components/AuthScreen.js.map +1 -1
  46. package/dist/ui/components/AuthWelcomeScreen.js +3 -1
  47. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  48. package/dist/ui/components/CodeBlock.js +3 -1
  49. package/dist/ui/components/CodeBlock.js.map +1 -1
  50. package/dist/ui/components/CompactShellPreview.js +44 -0
  51. package/dist/ui/components/CompactShellPreview.js.map +1 -0
  52. package/dist/ui/components/ConfigViewer.js +3 -1
  53. package/dist/ui/components/ConfigViewer.js.map +1 -1
  54. package/dist/ui/components/ConfirmPrompt.js +3 -1
  55. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  56. package/dist/ui/components/ConnectionStatusMessage.js +3 -1
  57. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  58. package/dist/ui/components/DetailedPlanReviewScreen.js +84 -74
  59. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  60. package/dist/ui/components/DiffViewer.js +6 -3
  61. package/dist/ui/components/DiffViewer.js.map +1 -1
  62. package/dist/ui/components/FileCreationPreview.js.map +1 -1
  63. package/dist/ui/components/FileTagAutocomplete.js +4 -2
  64. package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
  65. package/dist/ui/components/InputBox.js +243 -40
  66. package/dist/ui/components/InputBox.js.map +1 -1
  67. package/dist/ui/components/InteractiveShell.js +5 -3
  68. package/dist/ui/components/InteractiveShell.js.map +1 -1
  69. package/dist/ui/components/KeyboardHelp.js +4 -1
  70. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  71. package/dist/ui/components/LoadingIndicator.js +3 -1
  72. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  73. package/dist/ui/components/MCPAddScreen.js +63 -13
  74. package/dist/ui/components/MCPAddScreen.js.map +1 -1
  75. package/dist/ui/components/MarkdownRenderer.js +3 -1
  76. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  77. package/dist/ui/components/MessageDisplay.js +9 -7
  78. package/dist/ui/components/MessageDisplay.js.map +1 -1
  79. package/dist/ui/components/ModelPicker.js +170 -0
  80. package/dist/ui/components/ModelPicker.js.map +1 -0
  81. package/dist/ui/components/MonitorModeAIPanel.js +3 -1
  82. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
  83. package/dist/ui/components/PlanAcceptedMessage.js +12 -6
  84. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  85. package/dist/ui/components/PlanQuestionMessage.js +37 -0
  86. package/dist/ui/components/PlanQuestionMessage.js.map +1 -0
  87. package/dist/ui/components/PlanQuestionScreen.js +138 -0
  88. package/dist/ui/components/PlanQuestionScreen.js.map +1 -0
  89. package/dist/ui/components/PlanReviewScreen.js +7 -9
  90. package/dist/ui/components/PlanReviewScreen.js.map +1 -1
  91. package/dist/ui/components/RulesEditorScreen.js +65 -28
  92. package/dist/ui/components/RulesEditorScreen.js.map +1 -1
  93. package/dist/ui/components/SelectPrompt.js +3 -1
  94. package/dist/ui/components/SelectPrompt.js.map +1 -1
  95. package/dist/ui/components/SkillCreatorScreen.js +217 -0
  96. package/dist/ui/components/SkillCreatorScreen.js.map +1 -0
  97. package/dist/ui/components/SlashCommandAutocomplete.js +4 -2
  98. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  99. package/dist/ui/components/StatusBar.js +4 -2
  100. package/dist/ui/components/StatusBar.js.map +1 -1
  101. package/dist/ui/components/StreamingMessageDisplay.js +5 -3
  102. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  103. package/dist/ui/components/SubAgentListScreen.js +65 -0
  104. package/dist/ui/components/SubAgentListScreen.js.map +1 -0
  105. package/dist/ui/components/SubAgentViewScreen.js +123 -0
  106. package/dist/ui/components/SubAgentViewScreen.js.map +1 -0
  107. package/dist/ui/components/TaskCompletedMessage.js +40 -8
  108. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  109. package/dist/ui/components/TaskProgressIndicator.js +6 -4
  110. package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
  111. package/dist/ui/components/TextEditor.js +297 -0
  112. package/dist/ui/components/TextEditor.js.map +1 -0
  113. package/dist/ui/components/TodoListMessage.js +59 -0
  114. package/dist/ui/components/TodoListMessage.js.map +1 -0
  115. package/dist/ui/components/ToolExecutionMessage.js +134 -84
  116. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  117. package/dist/ui/components/ToolExecutionStatus.js +3 -1
  118. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  119. package/dist/ui/components/WelcomeBanner.js +33 -33
  120. package/dist/ui/components/WelcomeBanner.js.map +1 -1
  121. package/dist/ui/components/WorkflowCreatorScreen.js +5 -3
  122. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
  123. package/dist/ui/theme.js +97 -0
  124. package/dist/ui/theme.js.map +1 -0
  125. package/dist/ui/utils/chat-history-limit.js +247 -0
  126. package/dist/ui/utils/chat-history-limit.js.map +1 -0
  127. package/dist/utils/chat-formatter.js +22 -9
  128. package/dist/utils/chat-formatter.js.map +1 -1
  129. package/dist/utils/git-stats.js +7 -5
  130. package/dist/utils/git-stats.js.map +1 -1
  131. package/dist/utils/input-classifier.js +11 -1
  132. package/dist/utils/input-classifier.js.map +1 -1
  133. package/dist/utils/output-truncation.js +175 -0
  134. package/dist/utils/output-truncation.js.map +1 -0
  135. package/dist/utils/rule-reference-resolver.js +3 -3
  136. package/dist/utils/rule-reference-resolver.js.map +1 -1
  137. package/dist/utils/tunnel-commands-manager.js +134 -0
  138. package/dist/utils/tunnel-commands-manager.js.map +1 -0
  139. package/package.json +91 -90
  140. package/postinstall.js +4 -11
  141. package/dist/ui/components/MultiLineInput.js +0 -255
  142. package/dist/ui/components/MultiLineInput.js.map +0 -1
@@ -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, getPhaseContextForPrompt, getCurrentPhase } from "./tools/plan-mode.js";
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, maxLength = 5e5) {
1037
- const resultStr = typeof result === "string" ? result : JSON.stringify(result);
1038
- if (resultStr.length > maxLength) {
1039
- const truncated = resultStr.substring(0, maxLength);
1040
- const linesRemoved = (resultStr.match(/\n/g) || []).length - (truncated.match(/\n/g) || []).length;
1041
- return truncated + `
1042
-
1043
- [... truncated ${resultStr.length - maxLength} characters, ~${linesRemoved} lines removed for brevity ...]`;
1044
- }
1045
- return result;
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, relPath) => {
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 first before any implementation tools]
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. First explore the codebase using view_file, list_dir, grep_search to understand the context
1671
- 3. Then call create_plan with a detailed plan including:
1672
- - designSummary: Your understanding of the codebase
1673
- - tasks: Hierarchical tasks with subtasks
1674
- 4. Wait for user approval before implementing
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
- const tools = this.toolRegistry.getSchemas();
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 phaseContext = getPhaseContextForPrompt();
2551
+ const planContext = getPlanContextForPrompt();
2191
2552
  const originalRequest = this.pendingPlanRequest || message;
2192
- const executionPrompt = `${phaseContext}
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
- Complete the current task. After finishing each subtask, call mark_task_complete with the subtask number (e.g., "1.1"). When all subtasks are done, the main task will automatically complete and you'll receive the next task.`;
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.steps.length} tasks. Waiting for user approval.`
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
- const currentPlanData = getCurrentPlan();
2287
- if (currentPlanData && this.onTaskCompleted) {
2288
- const taskNumParts = String(completion.taskNumber).split(".");
2289
- const mainTaskNum = parseInt(taskNumParts[0], 10) - 1;
2290
- const task = currentPlanData.steps[mainTaskNum];
2291
- if (task) {
2292
- this.onTaskCompleted(
2293
- task,
2294
- completion.taskNumber,
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
- `${displayType} ${completion.taskNumber} of ${completion.totalCount} completed: ${completion.taskDescription}`
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 tasks completed! Output your summary of what was accomplished, then call task_complete()." : completion.nextSubtask ? `Subtask ${completion.taskNumber} completed. Next subtask: ${completion.nextSubtask}` : completion.nextTask ? `Task ${completion.taskNumber} completed. Next: ${completion.nextTask}` : `Task ${completion.taskNumber} completed.`
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, enhancedQuality, autonomousMode`;
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 format.\n\nUsage:\n- `/settings auto-suggest on`\n- `/settings auto-suggest off`";
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: modelsConfig.models.map((modelConfig, index) => {
3260
- const isCurrent = isCurrentCloud && currentModelName === modelConfig.name;
3261
- return {
3262
- label: `${modelConfig.name} - ${modelConfig.description}${isCurrent ? " [CURRENT]" : ""}`,
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
- ${output}` : `[User ran shell command in ${cwd}]
4853
+ ${truncatedOutput}` : `[User ran shell command in ${cwd}]
4348
4854
  Command: ${command}
4349
4855
  Output:
4350
- ${output || "(no output)"}`;
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. **Call the \`create_plan\` tool** to present a structured plan to the user FIRST
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
- Analyze the user's request, then call \`create_plan\` with:
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
- Example:
5358
+ If you need clarification before planning, call \`plan_ask_question\`:
4853
5359
  \`\`\`
4854
- create_plan(
4855
- title: "Create Python CSV Filter Script",
4856
- summary: "Build a Python script that reads a CSV file, filters rows based on criteria, and writes output",
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 task in order
4869
- 2. After completing each task, call \`mark_task_complete(task_number: N)\`
4870
- 3. After all tasks are done, call \`task_complete\` with a summary
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 other tools.**`;
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
  }