@kolisachint/hoocode-agent 0.5.30 → 0.5.32

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 (134) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/dist/core/agent-selection-eval.d.ts +91 -0
  3. package/dist/core/agent-selection-eval.d.ts.map +1 -0
  4. package/dist/core/agent-selection-eval.js +186 -0
  5. package/dist/core/agent-selection-eval.js.map +1 -0
  6. package/dist/core/agent-session-runtime.d.ts +28 -0
  7. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  8. package/dist/core/agent-session-runtime.js +61 -2
  9. package/dist/core/agent-session-runtime.js.map +1 -1
  10. package/dist/core/builtin-skills.d.ts +61 -0
  11. package/dist/core/builtin-skills.d.ts.map +1 -0
  12. package/dist/core/builtin-skills.js +106 -0
  13. package/dist/core/builtin-skills.js.map +1 -0
  14. package/dist/core/extensions/plugins/default-marketplace/.agents-plugin/marketplace.json +5 -1
  15. package/dist/core/extensions/plugins/trigger-judge.d.ts +42 -0
  16. package/dist/core/extensions/plugins/trigger-judge.d.ts.map +1 -0
  17. package/dist/core/extensions/plugins/trigger-judge.js +121 -0
  18. package/dist/core/extensions/plugins/trigger-judge.js.map +1 -0
  19. package/dist/core/external-tools.d.ts +67 -0
  20. package/dist/core/external-tools.d.ts.map +1 -0
  21. package/dist/core/external-tools.js +120 -0
  22. package/dist/core/external-tools.js.map +1 -0
  23. package/dist/core/keybindings.d.ts +102 -30
  24. package/dist/core/keybindings.d.ts.map +1 -1
  25. package/dist/core/keybindings.js +123 -29
  26. package/dist/core/keybindings.js.map +1 -1
  27. package/dist/core/mode-prompts.d.ts +15 -3
  28. package/dist/core/mode-prompts.d.ts.map +1 -1
  29. package/dist/core/mode-prompts.js +17 -29
  30. package/dist/core/mode-prompts.js.map +1 -1
  31. package/dist/core/settings-defaults.d.ts +1 -1
  32. package/dist/core/settings-defaults.d.ts.map +1 -1
  33. package/dist/core/settings-defaults.js +1 -1
  34. package/dist/core/settings-defaults.js.map +1 -1
  35. package/dist/core/settings-manager.d.ts +10 -3
  36. package/dist/core/settings-manager.d.ts.map +1 -1
  37. package/dist/core/settings-manager.js +19 -8
  38. package/dist/core/settings-manager.js.map +1 -1
  39. package/dist/core/settings-types.d.ts +1 -1
  40. package/dist/core/settings-types.d.ts.map +1 -1
  41. package/dist/core/settings-types.js.map +1 -1
  42. package/dist/core/slash-commands.d.ts.map +1 -1
  43. package/dist/core/slash-commands.js +5 -1
  44. package/dist/core/slash-commands.js.map +1 -1
  45. package/dist/core/tool-output-view.d.ts +33 -0
  46. package/dist/core/tool-output-view.d.ts.map +1 -0
  47. package/dist/core/tool-output-view.js +31 -0
  48. package/dist/core/tool-output-view.js.map +1 -0
  49. package/dist/core/tools/edit.d.ts.map +1 -1
  50. package/dist/core/tools/edit.js +4 -0
  51. package/dist/core/tools/edit.js.map +1 -1
  52. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  53. package/dist/core/tools/propose-plugin.js +7 -7
  54. package/dist/core/tools/propose-plugin.js.map +1 -1
  55. package/dist/core/tools/subagent.d.ts +7 -1
  56. package/dist/core/tools/subagent.d.ts.map +1 -1
  57. package/dist/core/tools/subagent.js +12 -24
  58. package/dist/core/tools/subagent.js.map +1 -1
  59. package/dist/extensions/core/modes.d.ts.map +1 -1
  60. package/dist/extensions/core/modes.js +21 -23
  61. package/dist/extensions/core/modes.js.map +1 -1
  62. package/dist/extensions/core/scaffold.d.ts.map +1 -1
  63. package/dist/extensions/core/scaffold.js +28 -24
  64. package/dist/extensions/core/scaffold.js.map +1 -1
  65. package/dist/init-templates.generated.d.ts +3 -0
  66. package/dist/init-templates.generated.d.ts.map +1 -1
  67. package/dist/init-templates.generated.js +12 -0
  68. package/dist/init-templates.generated.js.map +1 -1
  69. package/dist/main.d.ts.map +1 -1
  70. package/dist/main.js +14 -1
  71. package/dist/main.js.map +1 -1
  72. package/dist/modes/interactive/command-executor.d.ts +20 -0
  73. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  74. package/dist/modes/interactive/command-executor.js +138 -9
  75. package/dist/modes/interactive/command-executor.js.map +1 -1
  76. package/dist/modes/interactive/components/footer.d.ts +4 -0
  77. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/footer.js +31 -2
  79. package/dist/modes/interactive/components/footer.js.map +1 -1
  80. package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
  81. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  82. package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
  83. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  84. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  85. package/dist/modes/interactive/components/session-selector.js +2 -2
  86. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  87. package/dist/modes/interactive/components/settings-selector.d.ts +10 -2
  88. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  89. package/dist/modes/interactive/components/settings-selector.js +165 -23
  90. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  91. package/dist/modes/interactive/components/tool-chain-summary.d.ts +65 -0
  92. package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
  93. package/dist/modes/interactive/components/tool-chain-summary.js +216 -0
  94. package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
  95. package/dist/modes/interactive/components/tool-chain.d.ts +61 -0
  96. package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
  97. package/dist/modes/interactive/components/tool-chain.js +156 -0
  98. package/dist/modes/interactive/components/tool-chain.js.map +1 -0
  99. package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
  100. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  101. package/dist/modes/interactive/components/tool-execution.js +173 -33
  102. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  103. package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
  104. package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
  105. package/dist/modes/interactive/components/tool-signal.js +151 -0
  106. package/dist/modes/interactive/components/tool-signal.js.map +1 -0
  107. package/dist/modes/interactive/interactive-mode.d.ts +50 -1
  108. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/dist/modes/interactive/interactive-mode.js +244 -29
  110. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/dist/modes/interactive/resource-display.d.ts +9 -0
  112. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  113. package/dist/modes/interactive/resource-display.js +13 -0
  114. package/dist/modes/interactive/resource-display.js.map +1 -1
  115. package/dist/utils/tools-manager.d.ts +30 -0
  116. package/dist/utils/tools-manager.d.ts.map +1 -1
  117. package/dist/utils/tools-manager.js +35 -0
  118. package/dist/utils/tools-manager.js.map +1 -1
  119. package/docs/modes.md +4 -0
  120. package/docs/plugins.md +10 -0
  121. package/docs/settings.md +42 -0
  122. package/docs/skills.md +30 -0
  123. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  124. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  125. package/examples/extensions/sandbox/package.json +1 -1
  126. package/examples/extensions/with-deps/package.json +1 -1
  127. package/package.json +5 -4
  128. package/templates/prompts/grill-bridge.md +1 -0
  129. package/templates/prompts/grill-me.md +7 -0
  130. package/templates/prompts/grill-plan.md +9 -0
  131. package/templates/prompts/task-background-agents.md +2 -0
  132. package/templates/prompts/task-background-none.md +1 -0
  133. package/templates/prompts/task-main.md +19 -0
  134. package/templates/skills/plugin-authoring/SKILL.md +81 -0
@@ -18,6 +18,7 @@ import { canvasSearchRoots, discoverCanvasExtensions } from "../../core/canvas/d
18
18
  import { pluginCanvasExtensions } from "../../core/canvas/plugin-canvases.js";
19
19
  import { gateCanvasExtensions } from "../../core/canvas/trust.js";
20
20
  import { getWorkspacePlatforms, setPlatforms } from "../../core/extensions/plugins/formats/platform-targets.js";
21
+ import { describeExternalTools } from "../../core/external-tools.js";
21
22
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
22
23
  import { formatDurationSecs } from "../../core/format-duration.js";
23
24
  import { formatTokens } from "../../core/format-tokens.js";
@@ -28,6 +29,7 @@ import { SessionManager } from "../../core/session-manager.js";
28
29
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
29
30
  import { startupProgress } from "../../core/startup-progress.js";
30
31
  import { taskStore } from "../../core/task-store.js";
32
+ import { cycleToolOutputView } from "../../core/tool-output-view.js";
31
33
  import { settleDanglingMainTasks } from "../../core/tools/todo.js";
32
34
  import { buildCompactWordmark } from "../../core/wordmark.js";
33
35
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
@@ -52,6 +54,7 @@ import { SessionSelectorComponent } from "./components/session-selector.js";
52
54
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
53
55
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
54
56
  import { TaskPanelComponent } from "./components/task-panel.js";
57
+ import { ToolChainComponent } from "./components/tool-chain.js";
55
58
  import { ToolExecutionComponent } from "./components/tool-execution.js";
56
59
  import { TreeSelectorComponent } from "./components/tree-selector.js";
57
60
  import { UserMessageComponent } from "./components/user-message.js";
@@ -196,8 +199,13 @@ export class InteractiveMode {
196
199
  pendingTools = new Map();
197
200
  // Tool output expansion state
198
201
  toolOutputExpanded = false;
199
- // Persisted tool-output display level (collapsed / peek / standard).
200
- toolOutputDisplay = "standard";
202
+ // Persisted view dial (radar / glance / full). See core/tool-output-view.ts.
203
+ toolOutputView = "glance";
204
+ // The chain currently collecting tool calls, if the agent is mid-run.
205
+ openChain;
206
+ // Whether the assistant message being streamed has said anything yet. The
207
+ // first words it speaks close the chain the previous message's calls built.
208
+ sawTextInCurrentMessage = false;
201
209
  // Thinking block visibility state
202
210
  hideThinkingBlock = false;
203
211
  // Skill commands: command name -> skill file path
@@ -371,6 +379,7 @@ export class InteractiveMode {
371
379
  this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
372
380
  this.footer = new FooterComponent(this.session, this.footerDataProvider);
373
381
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
382
+ this.footer.setToolOutputView(this.toolOutputView);
374
383
  this.footerDataProvider.setSubagentEnabled(this.session.getActiveToolNames().includes("Task"));
375
384
  this.chrome = new ExtensionChrome({
376
385
  ui: this.ui,
@@ -459,8 +468,8 @@ export class InteractiveMode {
459
468
  this.taskPanel.onExitFocus = () => this.teamFocus.exitFocus();
460
469
  // Load hide thinking block setting
461
470
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
462
- // Load tool-output display level
463
- this.toolOutputDisplay = this.settingsManager.getToolOutputDisplay();
471
+ // Load the tool-output view dial
472
+ this.toolOutputView = this.settingsManager.getToolOutputView();
464
473
  // Completion chime: rings the terminal bell when a long turn finishes or the
465
474
  // agent blocks awaiting input. Enable is read fresh so a live toggle applies.
466
475
  this.chime = new CompletionChime({
@@ -542,6 +551,14 @@ export class InteractiveMode {
542
551
  }));
543
552
  };
544
553
  }
554
+ const cdCommand = slashCommands.find((command) => command.name === "cd");
555
+ if (cdCommand) {
556
+ cdCommand.argumentHint = "<path>";
557
+ cdCommand.getArgumentCompletions = (prefix) => {
558
+ const completions = this.commandExecutor.getChangeDirectoryCompletions(prefix);
559
+ return completions.length > 0 ? completions : null;
560
+ };
561
+ }
545
562
  // Convert prompt templates to SlashCommand format for autocomplete
546
563
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
547
564
  name: cmd.name,
@@ -657,7 +674,9 @@ export class InteractiveMode {
657
674
  // Add header with keybindings from config (unless silenced)
658
675
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
659
676
  const termW = this.ui.terminal.columns;
660
- const logo = termW >= 40
677
+ // A function, not a value: the banner names the working directory, and
678
+ // `/cd` moves it. `refreshBuiltInHeader` re-reads this after a move.
679
+ const logo = () => termW >= 40
661
680
  ? buildCompactWordmark({
662
681
  appName: APP_NAME,
663
682
  version: this.version,
@@ -681,10 +700,16 @@ export class InteractiveMode {
681
700
  hint("app.thinking.cycle", "to cycle thinking level"),
682
701
  rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
683
702
  hint("app.model.select", "to select model"),
684
- hint("app.tools.expand", "to expand tools"),
703
+ hint("app.tools.expand", "to expand all tool output"),
704
+ hint("app.tools.unfoldOne", "to open one chain or block (repeat to peel back)"),
705
+ hint("app.view.cycleForward", "to cycle tool output (radar/glance/full)"),
685
706
  hint("app.thinking.toggle", "to expand thinking"),
686
707
  hint("app.tasks.cycleView", "to cycle task panel view"),
687
708
  ...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
709
+ hint("app.mode.cycle", "to cycle agent mode"),
710
+ hint("app.session.changeDirectory", "to change working directory"),
711
+ hint("app.settings.open", "for settings"),
712
+ hint("app.hotkeys.open", "for all shortcuts"),
688
713
  hint("app.editor.external", "for external editor"),
689
714
  rawKeyHint("/", "for commands"),
690
715
  rawKeyHint("!", "to run bash"),
@@ -695,7 +720,7 @@ export class InteractiveMode {
695
720
  rawKeyHint("drop files", "to attach"),
696
721
  ].join("\n");
697
722
  const onboarding = theme.fg("dim", `${APP_NAME} can explain its own features and look up its docs. Ask it how to use or extend ${APP_NAME}.`);
698
- this.builtInHeader = new ExpandableText(() => logo, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
723
+ this.builtInHeader = new ExpandableText(() => logo(), () => `${logo()}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
699
724
  // Setup UI layout
700
725
  this.headerContainer.addChild(this.builtInHeader);
701
726
  }
@@ -1001,6 +1026,11 @@ export class InteractiveMode {
1001
1026
  applyRuntimeSettings() {
1002
1027
  this.footer.setSession(this.session);
1003
1028
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
1029
+ this.footer.setToolOutputView(this.toolOutputView);
1030
+ // The startup banner names the cwd, which `/cd` changes under it.
1031
+ if (this.builtInHeader instanceof ExpandableText) {
1032
+ this.builtInHeader.refresh();
1033
+ }
1004
1034
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1005
1035
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1006
1036
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
@@ -1047,6 +1077,7 @@ export class InteractiveMode {
1047
1077
  */
1048
1078
  renderCurrentSessionState() {
1049
1079
  this.chatContainer.clear();
1080
+ this.openChain = undefined;
1050
1081
  this.pendingMessagesContainer.clear();
1051
1082
  this.messageQueue.resetCompactionQueue();
1052
1083
  this.streamingComponent = undefined;
@@ -1063,12 +1094,7 @@ export class InteractiveMode {
1063
1094
  * later full rebuild (theme toggle / reload) restores full fidelity.
1064
1095
  */
1065
1096
  trimTranscriptMemory() {
1066
- const freezable = [];
1067
- for (const child of this.chatContainer.children) {
1068
- if (child instanceof ToolExecutionComponent && child.isFreezable()) {
1069
- freezable.push(child);
1070
- }
1071
- }
1097
+ const freezable = this.transcriptToolBlocks().filter((block) => block.isFreezable());
1072
1098
  const excess = freezable.length - LIVE_TOOL_WINDOW;
1073
1099
  for (let i = 0; i < excess; i++) {
1074
1100
  freezable[i].freeze();
@@ -1443,6 +1469,10 @@ export class InteractiveMode {
1443
1469
  this.ui.onDebug = () => this.commandExecutor.handleDebug();
1444
1470
  this.defaultEditor.onAction("app.model.select", () => this.modelController.showModelSelector());
1445
1471
  this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1472
+ this.defaultEditor.onAction("app.view.cycleForward", () => this.cycleToolOutputView("forward"));
1473
+ this.defaultEditor.onAction("app.view.cycleBackward", () => this.cycleToolOutputView("backward"));
1474
+ this.defaultEditor.onAction("app.tools.unfoldOne", () => this.stepToolOutput("unfold"));
1475
+ this.defaultEditor.onAction("app.tools.foldOne", () => this.stepToolOutput("fold"));
1446
1476
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1447
1477
  this.defaultEditor.onAction("app.tasks.cycleView", () => {
1448
1478
  this.taskPanel.cycleView();
@@ -1456,6 +1486,15 @@ export class InteractiveMode {
1456
1486
  this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
1457
1487
  this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
1458
1488
  this.defaultEditor.onAction("app.session.resume", () => this.showSessionSelector());
1489
+ this.defaultEditor.onAction("app.session.changeDirectory", () => {
1490
+ // Prefill rather than act: a directory change needs a target, and the
1491
+ // editor's own path completion is already the best way to name one.
1492
+ this.editor.setText("/cd ");
1493
+ this.ui.requestRender();
1494
+ });
1495
+ this.defaultEditor.onAction("app.settings.open", () => this.showSettingsSelector());
1496
+ this.defaultEditor.onAction("app.hotkeys.open", () => this.commandExecutor.handleHotkeys());
1497
+ this.defaultEditor.onAction("app.mode.cycle", () => void this.cycleAgentMode());
1459
1498
  this.defaultEditor.onChange = (text) => {
1460
1499
  const wasBashMode = this.isBashMode;
1461
1500
  this.isBashMode = text.trimStart().startsWith("!");
@@ -1644,6 +1683,13 @@ export class InteractiveMode {
1644
1683
  clearEditor();
1645
1684
  },
1646
1685
  },
1686
+ "/cd": {
1687
+ withArgs: true,
1688
+ run: async (text) => {
1689
+ clearEditor();
1690
+ await this.commandExecutor.handleChangeDirectory(text);
1691
+ },
1692
+ },
1647
1693
  "/quit": {
1648
1694
  run: async () => {
1649
1695
  clearEditor();
@@ -1742,6 +1788,10 @@ export class InteractiveMode {
1742
1788
  if (this.chimePendingRetry || this.session.isStreaming || this.session.isCompacting) {
1743
1789
  return;
1744
1790
  }
1791
+ // The last chain of a turn has nothing after it to close it, so it
1792
+ // settles here. A turn that did not end cleanly leaves its chain in the
1793
+ // running rendering plus a marker rather than claiming an outcome.
1794
+ this.closeOpenChain(this.turnStopReason === "stop" ? "done" : "interrupted");
1745
1795
  this.showTurnCost();
1746
1796
  this.settleDanglingPlanItems();
1747
1797
  this.chime?.onTurnComplete({ aborted: this.turnStopReason === "aborted" });
@@ -1893,6 +1943,7 @@ export class InteractiveMode {
1893
1943
  else if (event.message.role === "assistant") {
1894
1944
  this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
1895
1945
  this.streamingMessage = event.message;
1946
+ this.sawTextInCurrentMessage = false;
1896
1947
  this.chatContainer.addChild(this.streamingComponent);
1897
1948
  this.streamingComponent.updateContent(this.streamingMessage);
1898
1949
  this.ui.requestRender();
@@ -1902,16 +1953,25 @@ export class InteractiveMode {
1902
1953
  if (this.streamingComponent && event.message.role === "assistant") {
1903
1954
  this.streamingMessage = event.message;
1904
1955
  this.scheduleStreamingRender();
1956
+ // The agent speaking ends the run its previous calls formed. Tool
1957
+ // calls later in this same message open a fresh chain.
1958
+ if (!this.sawTextInCurrentMessage) {
1959
+ const spoke = this.streamingMessage.content.some((content) => content.type === "text" && content.text.trim() !== "");
1960
+ if (spoke) {
1961
+ this.sawTextInCurrentMessage = true;
1962
+ this.closeOpenChain("done");
1963
+ }
1964
+ }
1905
1965
  for (const content of this.streamingMessage.content) {
1906
1966
  if (content.type === "toolCall") {
1907
1967
  if (!this.pendingTools.has(content.id)) {
1908
1968
  const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
1909
1969
  showImages: this.settingsManager.getShowImages(),
1910
1970
  imageWidthCells: this.settingsManager.getImageWidthCells(),
1911
- displayLevel: this.toolOutputDisplay,
1971
+ view: this.toolOutputView,
1912
1972
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
1913
1973
  component.setExpanded(this.toolOutputExpanded);
1914
- this.chatContainer.addChild(component);
1974
+ this.attachToolBlock(component);
1915
1975
  this.pendingTools.set(content.id, component);
1916
1976
  }
1917
1977
  else {
@@ -1975,10 +2035,10 @@ export class InteractiveMode {
1975
2035
  component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
1976
2036
  showImages: this.settingsManager.getShowImages(),
1977
2037
  imageWidthCells: this.settingsManager.getImageWidthCells(),
1978
- displayLevel: this.toolOutputDisplay,
2038
+ view: this.toolOutputView,
1979
2039
  }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
1980
2040
  component.setExpanded(this.toolOutputExpanded);
1981
- this.chatContainer.addChild(component);
2041
+ this.attachToolBlock(component);
1982
2042
  this.pendingTools.set(event.toolCallId, component);
1983
2043
  }
1984
2044
  component.markExecutionStarted();
@@ -2255,9 +2315,18 @@ export class InteractiveMode {
2255
2315
  this.footer.invalidate();
2256
2316
  this.updateEditorBorderColor();
2257
2317
  }
2318
+ let lastAssistantStopReason;
2258
2319
  for (const message of sessionContext.messages) {
2259
2320
  // Assistant messages need special handling for tool calls
2260
2321
  if (message.role === "assistant") {
2322
+ // Same boundary the live path uses: the agent speaking ends the run
2323
+ // its previous calls formed. Rebuilding history has to reproduce it,
2324
+ // or a resumed session would show one chain where it lived through
2325
+ // several.
2326
+ const spoke = message.content.some((content) => content.type === "text" && content.text.trim() !== "");
2327
+ if (spoke)
2328
+ this.closeOpenChain("done");
2329
+ lastAssistantStopReason = message.stopReason;
2261
2330
  this.addMessageToChat(message);
2262
2331
  // Render tool call components
2263
2332
  for (const content of message.content) {
@@ -2265,10 +2334,10 @@ export class InteractiveMode {
2265
2334
  const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2266
2335
  showImages: this.settingsManager.getShowImages(),
2267
2336
  imageWidthCells: this.settingsManager.getImageWidthCells(),
2268
- displayLevel: this.toolOutputDisplay,
2337
+ view: this.toolOutputView,
2269
2338
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2270
2339
  component.setExpanded(this.toolOutputExpanded);
2271
- this.chatContainer.addChild(component);
2340
+ this.attachToolBlock(component);
2272
2341
  if (message.stopReason === "aborted" || message.stopReason === "error") {
2273
2342
  let errorMessage;
2274
2343
  if (message.stopReason === "aborted") {
@@ -2302,6 +2371,13 @@ export class InteractiveMode {
2302
2371
  this.addMessageToChat(message, options);
2303
2372
  }
2304
2373
  }
2374
+ // History has no live state: a chain left open at the end of the rebuild is
2375
+ // finished, and how the last assistant message ended says whether it
2376
+ // finished cleanly. Tool calls still awaiting results are the exception —
2377
+ // this is a resumed run in flight, so its chain stays open for them.
2378
+ if (renderedPendingTools.size === 0) {
2379
+ this.closeOpenChain(lastAssistantStopReason === "stop" ? "done" : "interrupted");
2380
+ }
2305
2381
  for (const [toolCallId, component] of renderedPendingTools) {
2306
2382
  this.pendingTools.set(toolCallId, component);
2307
2383
  }
@@ -2528,9 +2604,124 @@ export class InteractiveMode {
2528
2604
  this.showStatus(`Thinking level: ${newLevel}`);
2529
2605
  }
2530
2606
  }
2607
+ /**
2608
+ * Put a tool block into the transcript, extending the open chain or starting
2609
+ * one. Every path that renders a tool call comes through here — live
2610
+ * streaming, execution start, and rebuilding history — so a chain cannot be
2611
+ * assembled correctly in one place and forgotten in another.
2612
+ */
2613
+ attachToolBlock(block) {
2614
+ if (!this.openChain || !this.openChain.isOpen) {
2615
+ this.openChain = new ToolChainComponent(this.toolOutputView);
2616
+ this.chatContainer.addChild(this.openChain);
2617
+ }
2618
+ this.openChain.add(block);
2619
+ }
2620
+ /**
2621
+ * Settle the open chain.
2622
+ *
2623
+ * Called when the agent speaks (the run it was doing is over, whatever comes
2624
+ * next is a new one) and when the turn settles. Closing on the agent's next
2625
+ * words rather than only at turn end is what keeps the flip cheap: the line
2626
+ * is still at the bottom of the screen, so the TUI rewrites it in place
2627
+ * instead of taking the full-redraw path that clears terminal scrollback.
2628
+ */
2629
+ closeOpenChain(outcome) {
2630
+ if (!this.openChain)
2631
+ return;
2632
+ if (this.openChain.isEmpty) {
2633
+ this.chatContainer.removeChild(this.openChain);
2634
+ }
2635
+ else {
2636
+ this.openChain.close(outcome);
2637
+ }
2638
+ this.openChain = undefined;
2639
+ this.ui.requestRender();
2640
+ }
2641
+ /** Every tool block in the transcript, in order, across all chains. */
2642
+ transcriptToolBlocks() {
2643
+ const blocks = [];
2644
+ for (const child of this.chatContainer.children) {
2645
+ if (child instanceof ToolChainComponent)
2646
+ blocks.push(...child.toolBlocks);
2647
+ else if (child instanceof ToolExecutionComponent)
2648
+ blocks.push(child);
2649
+ }
2650
+ return blocks;
2651
+ }
2652
+ /** Every chain in the transcript, in order. */
2653
+ transcriptChains() {
2654
+ return this.chatContainer.children.filter((child) => child instanceof ToolChainComponent);
2655
+ }
2531
2656
  toggleToolOutputExpansion() {
2532
2657
  this.setToolsExpanded(!this.toolOutputExpanded);
2533
2658
  }
2659
+ /**
2660
+ * Open or close one tool block instead of all of them.
2661
+ *
2662
+ * `ctrl+o` is all-or-nothing, which left the ▸ caret on every glance and
2663
+ * radar row advertising something no key could do. This is the missing half:
2664
+ * unfold walks back from the newest block to the first one still folded,
2665
+ * fold walks back to the most recently opened one.
2666
+ *
2667
+ * Working from the tail is not a shortcut, it is the only thing this view can
2668
+ * honestly offer. The transcript is bottom-anchored and has no app-level
2669
+ * scrolling — anything far enough up is in the terminal's own scrollback,
2670
+ * where this process cannot put a cursor or scroll to a selection. So the
2671
+ * key peels backwards through what is actually on screen, and each block it
2672
+ * opens is its own marker for where you have got to.
2673
+ */
2674
+ stepToolOutput(direction) {
2675
+ const wantOpen = direction === "unfold";
2676
+ // Radar's unit is the chain: one press turns the newest summary line back
2677
+ // into the calls it stands for. Its per-call bodies are a glance/full
2678
+ // question, so the step stops there rather than cascading.
2679
+ if (this.toolOutputView === "radar") {
2680
+ const chains = this.transcriptChains();
2681
+ for (let i = chains.length - 1; i >= 0; i--) {
2682
+ if (chains[i].isEmpty || chains[i].isOpened === wantOpen)
2683
+ continue;
2684
+ chains[i].setOpened(wantOpen);
2685
+ this.ui.requestRender();
2686
+ return;
2687
+ }
2688
+ this.showStatus(wantOpen ? "No collapsed chains below this point" : "No open chains below this point");
2689
+ return;
2690
+ }
2691
+ const blocks = this.transcriptToolBlocks();
2692
+ for (let i = blocks.length - 1; i >= 0; i--) {
2693
+ const block = blocks[i];
2694
+ if (block.isRevealed() === wantOpen)
2695
+ continue;
2696
+ block.setExpanded(wantOpen);
2697
+ this.ui.requestRender();
2698
+ return;
2699
+ }
2700
+ this.showStatus(wantOpen ? "No folded tool output below this point" : "No unfolded tool output below this point");
2701
+ }
2702
+ /**
2703
+ * Move the view dial one stop and report where it landed.
2704
+ *
2705
+ * The dial is the persistent decision ("how much do I ever want to see"),
2706
+ * which is why it saves; `app.tools.expand` stays the momentary one ("open
2707
+ * what is in front of me"), which is why it does not.
2708
+ */
2709
+ cycleToolOutputView(direction) {
2710
+ const next = cycleToolOutputView(this.toolOutputView, direction);
2711
+ this.applyToolOutputView(next);
2712
+ }
2713
+ applyToolOutputView(view) {
2714
+ this.toolOutputView = view;
2715
+ this.settingsManager.setToolOutputView(view);
2716
+ this.footer.setToolOutputView(view);
2717
+ for (const child of this.chatContainer.children) {
2718
+ if (child instanceof ToolChainComponent)
2719
+ child.setView(view);
2720
+ else if (child instanceof ToolExecutionComponent)
2721
+ child.setView(view);
2722
+ }
2723
+ this.ui.requestRender();
2724
+ }
2534
2725
  setToolsExpanded(expanded) {
2535
2726
  this.toolOutputExpanded = expanded;
2536
2727
  const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
@@ -2544,6 +2735,33 @@ export class InteractiveMode {
2544
2735
  }
2545
2736
  this.ui.requestRender();
2546
2737
  }
2738
+ /**
2739
+ * Step the agent mode one along (ask → plan → build → debug → ask).
2740
+ *
2741
+ * Mode is the product's central guardrail and sits first in the footer, yet
2742
+ * it was reachable only by typing `/mode <name>`. The list comes from the
2743
+ * mode command's own argument completions rather than a copy kept here, so
2744
+ * a project that adds a mode gets it in the rotation for free; if the command
2745
+ * is missing (a `--light` run strips the extension), the key says so instead
2746
+ * of guessing.
2747
+ */
2748
+ async cycleAgentMode() {
2749
+ const command = this.session.extensionRunner.getCommand("mode");
2750
+ if (!command) {
2751
+ this.showWarning("Modes are not available in this session");
2752
+ return;
2753
+ }
2754
+ const completions = (await command.getArgumentCompletions?.("")) ?? [];
2755
+ const modes = completions.map((item) => item.value);
2756
+ if (modes.length === 0) {
2757
+ this.showWarning("No modes are configured");
2758
+ return;
2759
+ }
2760
+ const current = this.footerDataProvider.getActiveMode();
2761
+ const index = modes.indexOf(current);
2762
+ const next = modes[(index + 1) % modes.length];
2763
+ await this.session.prompt(`/mode ${next}`);
2764
+ }
2547
2765
  toggleThinkingBlockVisibility() {
2548
2766
  this.hideThinkingBlock = !this.hideThinkingBlock;
2549
2767
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
@@ -2742,8 +2960,12 @@ export class InteractiveMode {
2742
2960
  autoCompact: this.session.autoCompactionEnabled,
2743
2961
  tools: toolToggles,
2744
2962
  toolGroups,
2963
+ // Resolved fresh on every open, never downloaded: a binary that
2964
+ // arrived since the last open (a startup fetch, a `brew install`)
2965
+ // shows as present without a restart.
2966
+ externalTools: describeExternalTools(),
2745
2967
  flags,
2746
- toolOutputDisplay: this.toolOutputDisplay,
2968
+ toolOutputView: this.toolOutputView,
2747
2969
  toolOutputMaxBytes: this.settingsManager.getToolOutputMaxBytes(),
2748
2970
  toolOutputMaxLines: this.settingsManager.getToolOutputMaxLines(),
2749
2971
  contextGc: this.settingsManager.getContextGcEnabled(),
@@ -2843,15 +3065,8 @@ export class InteractiveMode {
2843
3065
  break;
2844
3066
  }
2845
3067
  },
2846
- onToolOutputDisplayChange: (level) => {
2847
- this.toolOutputDisplay = level;
2848
- this.settingsManager.setToolOutputDisplay(level);
2849
- for (const child of this.chatContainer.children) {
2850
- if (child instanceof ToolExecutionComponent) {
2851
- child.setDisplayLevel(level);
2852
- }
2853
- }
2854
- this.ui.requestRender();
3068
+ onToolOutputViewChange: (view) => {
3069
+ this.applyToolOutputView(view);
2855
3070
  },
2856
3071
  onToolOutputMaxBytesChange: (bytes) => {
2857
3072
  this.settingsManager.setToolOutputMaxBytes(bytes);