centaurus-cli 3.1.3 → 3.1.5

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