@kolisachint/hoocode-agent 0.5.31 → 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 (78) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/dist/core/agent-session-runtime.d.ts +28 -0
  3. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  4. package/dist/core/agent-session-runtime.js +61 -2
  5. package/dist/core/agent-session-runtime.js.map +1 -1
  6. package/dist/core/keybindings.d.ts +102 -30
  7. package/dist/core/keybindings.d.ts.map +1 -1
  8. package/dist/core/keybindings.js +123 -29
  9. package/dist/core/keybindings.js.map +1 -1
  10. package/dist/core/settings-defaults.d.ts +1 -1
  11. package/dist/core/settings-defaults.d.ts.map +1 -1
  12. package/dist/core/settings-defaults.js +1 -1
  13. package/dist/core/settings-defaults.js.map +1 -1
  14. package/dist/core/settings-manager.d.ts +10 -3
  15. package/dist/core/settings-manager.d.ts.map +1 -1
  16. package/dist/core/settings-manager.js +19 -8
  17. package/dist/core/settings-manager.js.map +1 -1
  18. package/dist/core/settings-types.d.ts +1 -1
  19. package/dist/core/settings-types.d.ts.map +1 -1
  20. package/dist/core/settings-types.js.map +1 -1
  21. package/dist/core/slash-commands.d.ts.map +1 -1
  22. package/dist/core/slash-commands.js +5 -1
  23. package/dist/core/slash-commands.js.map +1 -1
  24. package/dist/core/tool-output-view.d.ts +33 -0
  25. package/dist/core/tool-output-view.d.ts.map +1 -0
  26. package/dist/core/tool-output-view.js +31 -0
  27. package/dist/core/tool-output-view.js.map +1 -0
  28. package/dist/core/tools/edit.d.ts.map +1 -1
  29. package/dist/core/tools/edit.js +4 -0
  30. package/dist/core/tools/edit.js.map +1 -1
  31. package/dist/modes/interactive/command-executor.d.ts +20 -0
  32. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  33. package/dist/modes/interactive/command-executor.js +138 -9
  34. package/dist/modes/interactive/command-executor.js.map +1 -1
  35. package/dist/modes/interactive/components/footer.d.ts +4 -0
  36. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  37. package/dist/modes/interactive/components/footer.js +31 -2
  38. package/dist/modes/interactive/components/footer.js.map +1 -1
  39. package/dist/modes/interactive/components/scoped-models-selector.d.ts +2 -2
  40. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  41. package/dist/modes/interactive/components/scoped-models-selector.js +2 -2
  42. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  43. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  44. package/dist/modes/interactive/components/session-selector.js +2 -2
  45. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  46. package/dist/modes/interactive/components/settings-selector.d.ts +3 -2
  47. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/settings-selector.js +13 -12
  49. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  50. package/dist/modes/interactive/components/tool-chain-summary.d.ts +65 -0
  51. package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -0
  52. package/dist/modes/interactive/components/tool-chain-summary.js +216 -0
  53. package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -0
  54. package/dist/modes/interactive/components/tool-chain.d.ts +61 -0
  55. package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -0
  56. package/dist/modes/interactive/components/tool-chain.js +156 -0
  57. package/dist/modes/interactive/components/tool-chain.js.map +1 -0
  58. package/dist/modes/interactive/components/tool-execution.d.ts +55 -9
  59. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/tool-execution.js +173 -33
  61. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  62. package/dist/modes/interactive/components/tool-signal.d.ts +50 -0
  63. package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -0
  64. package/dist/modes/interactive/components/tool-signal.js +151 -0
  65. package/dist/modes/interactive/components/tool-signal.js.map +1 -0
  66. package/dist/modes/interactive/interactive-mode.d.ts +50 -1
  67. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.js +239 -29
  69. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/dist/modes/interactive/resource-display.d.ts +9 -0
  71. package/dist/modes/interactive/resource-display.d.ts.map +1 -1
  72. package/dist/modes/interactive/resource-display.js +13 -0
  73. package/dist/modes/interactive/resource-display.js.map +1 -1
  74. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  75. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  76. package/examples/extensions/sandbox/package.json +1 -1
  77. package/examples/extensions/with-deps/package.json +1 -1
  78. package/package.json +4 -4
@@ -29,6 +29,7 @@ import { SessionManager } from "../../core/session-manager.js";
29
29
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
30
30
  import { startupProgress } from "../../core/startup-progress.js";
31
31
  import { taskStore } from "../../core/task-store.js";
32
+ import { cycleToolOutputView } from "../../core/tool-output-view.js";
32
33
  import { settleDanglingMainTasks } from "../../core/tools/todo.js";
33
34
  import { buildCompactWordmark } from "../../core/wordmark.js";
34
35
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
@@ -53,6 +54,7 @@ import { SessionSelectorComponent } from "./components/session-selector.js";
53
54
  import { SettingsSelectorComponent } from "./components/settings-selector.js";
54
55
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
55
56
  import { TaskPanelComponent } from "./components/task-panel.js";
57
+ import { ToolChainComponent } from "./components/tool-chain.js";
56
58
  import { ToolExecutionComponent } from "./components/tool-execution.js";
57
59
  import { TreeSelectorComponent } from "./components/tree-selector.js";
58
60
  import { UserMessageComponent } from "./components/user-message.js";
@@ -197,8 +199,13 @@ export class InteractiveMode {
197
199
  pendingTools = new Map();
198
200
  // Tool output expansion state
199
201
  toolOutputExpanded = false;
200
- // Persisted tool-output display level (collapsed / peek / standard).
201
- 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;
202
209
  // Thinking block visibility state
203
210
  hideThinkingBlock = false;
204
211
  // Skill commands: command name -> skill file path
@@ -372,6 +379,7 @@ export class InteractiveMode {
372
379
  this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
373
380
  this.footer = new FooterComponent(this.session, this.footerDataProvider);
374
381
  this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
382
+ this.footer.setToolOutputView(this.toolOutputView);
375
383
  this.footerDataProvider.setSubagentEnabled(this.session.getActiveToolNames().includes("Task"));
376
384
  this.chrome = new ExtensionChrome({
377
385
  ui: this.ui,
@@ -460,8 +468,8 @@ export class InteractiveMode {
460
468
  this.taskPanel.onExitFocus = () => this.teamFocus.exitFocus();
461
469
  // Load hide thinking block setting
462
470
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
463
- // Load tool-output display level
464
- this.toolOutputDisplay = this.settingsManager.getToolOutputDisplay();
471
+ // Load the tool-output view dial
472
+ this.toolOutputView = this.settingsManager.getToolOutputView();
465
473
  // Completion chime: rings the terminal bell when a long turn finishes or the
466
474
  // agent blocks awaiting input. Enable is read fresh so a live toggle applies.
467
475
  this.chime = new CompletionChime({
@@ -543,6 +551,14 @@ export class InteractiveMode {
543
551
  }));
544
552
  };
545
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
+ }
546
562
  // Convert prompt templates to SlashCommand format for autocomplete
547
563
  const templateCommands = this.session.promptTemplates.map((cmd) => ({
548
564
  name: cmd.name,
@@ -658,7 +674,9 @@ export class InteractiveMode {
658
674
  // Add header with keybindings from config (unless silenced)
659
675
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
660
676
  const termW = this.ui.terminal.columns;
661
- 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
662
680
  ? buildCompactWordmark({
663
681
  appName: APP_NAME,
664
682
  version: this.version,
@@ -682,10 +700,16 @@ export class InteractiveMode {
682
700
  hint("app.thinking.cycle", "to cycle thinking level"),
683
701
  rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
684
702
  hint("app.model.select", "to select model"),
685
- 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)"),
686
706
  hint("app.thinking.toggle", "to expand thinking"),
687
707
  hint("app.tasks.cycleView", "to cycle task panel view"),
688
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"),
689
713
  hint("app.editor.external", "for external editor"),
690
714
  rawKeyHint("/", "for commands"),
691
715
  rawKeyHint("!", "to run bash"),
@@ -696,7 +720,7 @@ export class InteractiveMode {
696
720
  rawKeyHint("drop files", "to attach"),
697
721
  ].join("\n");
698
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}.`);
699
- 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);
700
724
  // Setup UI layout
701
725
  this.headerContainer.addChild(this.builtInHeader);
702
726
  }
@@ -1002,6 +1026,11 @@ export class InteractiveMode {
1002
1026
  applyRuntimeSettings() {
1003
1027
  this.footer.setSession(this.session);
1004
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
+ }
1005
1034
  this.footerDataProvider.setCwd(this.sessionManager.getCwd());
1006
1035
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
1007
1036
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
@@ -1048,6 +1077,7 @@ export class InteractiveMode {
1048
1077
  */
1049
1078
  renderCurrentSessionState() {
1050
1079
  this.chatContainer.clear();
1080
+ this.openChain = undefined;
1051
1081
  this.pendingMessagesContainer.clear();
1052
1082
  this.messageQueue.resetCompactionQueue();
1053
1083
  this.streamingComponent = undefined;
@@ -1064,12 +1094,7 @@ export class InteractiveMode {
1064
1094
  * later full rebuild (theme toggle / reload) restores full fidelity.
1065
1095
  */
1066
1096
  trimTranscriptMemory() {
1067
- const freezable = [];
1068
- for (const child of this.chatContainer.children) {
1069
- if (child instanceof ToolExecutionComponent && child.isFreezable()) {
1070
- freezable.push(child);
1071
- }
1072
- }
1097
+ const freezable = this.transcriptToolBlocks().filter((block) => block.isFreezable());
1073
1098
  const excess = freezable.length - LIVE_TOOL_WINDOW;
1074
1099
  for (let i = 0; i < excess; i++) {
1075
1100
  freezable[i].freeze();
@@ -1444,6 +1469,10 @@ export class InteractiveMode {
1444
1469
  this.ui.onDebug = () => this.commandExecutor.handleDebug();
1445
1470
  this.defaultEditor.onAction("app.model.select", () => this.modelController.showModelSelector());
1446
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"));
1447
1476
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1448
1477
  this.defaultEditor.onAction("app.tasks.cycleView", () => {
1449
1478
  this.taskPanel.cycleView();
@@ -1457,6 +1486,15 @@ export class InteractiveMode {
1457
1486
  this.defaultEditor.onAction("app.session.tree", () => this.showTreeSelector());
1458
1487
  this.defaultEditor.onAction("app.session.fork", () => this.showUserMessageSelector());
1459
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());
1460
1498
  this.defaultEditor.onChange = (text) => {
1461
1499
  const wasBashMode = this.isBashMode;
1462
1500
  this.isBashMode = text.trimStart().startsWith("!");
@@ -1645,6 +1683,13 @@ export class InteractiveMode {
1645
1683
  clearEditor();
1646
1684
  },
1647
1685
  },
1686
+ "/cd": {
1687
+ withArgs: true,
1688
+ run: async (text) => {
1689
+ clearEditor();
1690
+ await this.commandExecutor.handleChangeDirectory(text);
1691
+ },
1692
+ },
1648
1693
  "/quit": {
1649
1694
  run: async () => {
1650
1695
  clearEditor();
@@ -1743,6 +1788,10 @@ export class InteractiveMode {
1743
1788
  if (this.chimePendingRetry || this.session.isStreaming || this.session.isCompacting) {
1744
1789
  return;
1745
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");
1746
1795
  this.showTurnCost();
1747
1796
  this.settleDanglingPlanItems();
1748
1797
  this.chime?.onTurnComplete({ aborted: this.turnStopReason === "aborted" });
@@ -1894,6 +1943,7 @@ export class InteractiveMode {
1894
1943
  else if (event.message.role === "assistant") {
1895
1944
  this.streamingComponent = new AssistantMessageComponent(undefined, this.hideThinkingBlock, this.getMarkdownThemeWithSettings(), this.hiddenThinkingLabel);
1896
1945
  this.streamingMessage = event.message;
1946
+ this.sawTextInCurrentMessage = false;
1897
1947
  this.chatContainer.addChild(this.streamingComponent);
1898
1948
  this.streamingComponent.updateContent(this.streamingMessage);
1899
1949
  this.ui.requestRender();
@@ -1903,16 +1953,25 @@ export class InteractiveMode {
1903
1953
  if (this.streamingComponent && event.message.role === "assistant") {
1904
1954
  this.streamingMessage = event.message;
1905
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
+ }
1906
1965
  for (const content of this.streamingMessage.content) {
1907
1966
  if (content.type === "toolCall") {
1908
1967
  if (!this.pendingTools.has(content.id)) {
1909
1968
  const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
1910
1969
  showImages: this.settingsManager.getShowImages(),
1911
1970
  imageWidthCells: this.settingsManager.getImageWidthCells(),
1912
- displayLevel: this.toolOutputDisplay,
1971
+ view: this.toolOutputView,
1913
1972
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
1914
1973
  component.setExpanded(this.toolOutputExpanded);
1915
- this.chatContainer.addChild(component);
1974
+ this.attachToolBlock(component);
1916
1975
  this.pendingTools.set(content.id, component);
1917
1976
  }
1918
1977
  else {
@@ -1976,10 +2035,10 @@ export class InteractiveMode {
1976
2035
  component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
1977
2036
  showImages: this.settingsManager.getShowImages(),
1978
2037
  imageWidthCells: this.settingsManager.getImageWidthCells(),
1979
- displayLevel: this.toolOutputDisplay,
2038
+ view: this.toolOutputView,
1980
2039
  }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
1981
2040
  component.setExpanded(this.toolOutputExpanded);
1982
- this.chatContainer.addChild(component);
2041
+ this.attachToolBlock(component);
1983
2042
  this.pendingTools.set(event.toolCallId, component);
1984
2043
  }
1985
2044
  component.markExecutionStarted();
@@ -2256,9 +2315,18 @@ export class InteractiveMode {
2256
2315
  this.footer.invalidate();
2257
2316
  this.updateEditorBorderColor();
2258
2317
  }
2318
+ let lastAssistantStopReason;
2259
2319
  for (const message of sessionContext.messages) {
2260
2320
  // Assistant messages need special handling for tool calls
2261
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;
2262
2330
  this.addMessageToChat(message);
2263
2331
  // Render tool call components
2264
2332
  for (const content of message.content) {
@@ -2266,10 +2334,10 @@ export class InteractiveMode {
2266
2334
  const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
2267
2335
  showImages: this.settingsManager.getShowImages(),
2268
2336
  imageWidthCells: this.settingsManager.getImageWidthCells(),
2269
- displayLevel: this.toolOutputDisplay,
2337
+ view: this.toolOutputView,
2270
2338
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2271
2339
  component.setExpanded(this.toolOutputExpanded);
2272
- this.chatContainer.addChild(component);
2340
+ this.attachToolBlock(component);
2273
2341
  if (message.stopReason === "aborted" || message.stopReason === "error") {
2274
2342
  let errorMessage;
2275
2343
  if (message.stopReason === "aborted") {
@@ -2303,6 +2371,13 @@ export class InteractiveMode {
2303
2371
  this.addMessageToChat(message, options);
2304
2372
  }
2305
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
+ }
2306
2381
  for (const [toolCallId, component] of renderedPendingTools) {
2307
2382
  this.pendingTools.set(toolCallId, component);
2308
2383
  }
@@ -2529,9 +2604,124 @@ export class InteractiveMode {
2529
2604
  this.showStatus(`Thinking level: ${newLevel}`);
2530
2605
  }
2531
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
+ }
2532
2656
  toggleToolOutputExpansion() {
2533
2657
  this.setToolsExpanded(!this.toolOutputExpanded);
2534
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
+ }
2535
2725
  setToolsExpanded(expanded) {
2536
2726
  this.toolOutputExpanded = expanded;
2537
2727
  const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
@@ -2545,6 +2735,33 @@ export class InteractiveMode {
2545
2735
  }
2546
2736
  this.ui.requestRender();
2547
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
+ }
2548
2765
  toggleThinkingBlockVisibility() {
2549
2766
  this.hideThinkingBlock = !this.hideThinkingBlock;
2550
2767
  this.settingsManager.setHideThinkingBlock(this.hideThinkingBlock);
@@ -2748,7 +2965,7 @@ export class InteractiveMode {
2748
2965
  // shows as present without a restart.
2749
2966
  externalTools: describeExternalTools(),
2750
2967
  flags,
2751
- toolOutputDisplay: this.toolOutputDisplay,
2968
+ toolOutputView: this.toolOutputView,
2752
2969
  toolOutputMaxBytes: this.settingsManager.getToolOutputMaxBytes(),
2753
2970
  toolOutputMaxLines: this.settingsManager.getToolOutputMaxLines(),
2754
2971
  contextGc: this.settingsManager.getContextGcEnabled(),
@@ -2848,15 +3065,8 @@ export class InteractiveMode {
2848
3065
  break;
2849
3066
  }
2850
3067
  },
2851
- onToolOutputDisplayChange: (level) => {
2852
- this.toolOutputDisplay = level;
2853
- this.settingsManager.setToolOutputDisplay(level);
2854
- for (const child of this.chatContainer.children) {
2855
- if (child instanceof ToolExecutionComponent) {
2856
- child.setDisplayLevel(level);
2857
- }
2858
- }
2859
- this.ui.requestRender();
3068
+ onToolOutputViewChange: (view) => {
3069
+ this.applyToolOutputView(view);
2860
3070
  },
2861
3071
  onToolOutputMaxBytesChange: (bytes) => {
2862
3072
  this.settingsManager.setToolOutputMaxBytes(bytes);