@kolisachint/hoocode-agent 0.5.56 → 0.5.58

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 (103) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/dist/core/agent-session.d.ts +2 -2
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +4 -3
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/runner.d.ts.map +1 -1
  7. package/dist/core/extensions/runner.js +4 -1
  8. package/dist/core/extensions/runner.js.map +1 -1
  9. package/dist/core/keybindings.d.ts +162 -108
  10. package/dist/core/keybindings.d.ts.map +1 -1
  11. package/dist/core/keybindings.js +225 -120
  12. package/dist/core/keybindings.js.map +1 -1
  13. package/dist/core/settings-defaults.d.ts +1 -1
  14. package/dist/core/settings-defaults.d.ts.map +1 -1
  15. package/dist/core/settings-defaults.js +1 -1
  16. package/dist/core/settings-defaults.js.map +1 -1
  17. package/dist/core/settings-manager.d.ts +1 -1
  18. package/dist/core/settings-manager.d.ts.map +1 -1
  19. package/dist/core/settings-manager.js +1 -1
  20. package/dist/core/settings-manager.js.map +1 -1
  21. package/dist/core/settings-types.d.ts +1 -1
  22. package/dist/core/settings-types.d.ts.map +1 -1
  23. package/dist/core/settings-types.js.map +1 -1
  24. package/dist/core/tool-output-view.d.ts +28 -10
  25. package/dist/core/tool-output-view.d.ts.map +1 -1
  26. package/dist/core/tool-output-view.js +24 -9
  27. package/dist/core/tool-output-view.js.map +1 -1
  28. package/dist/core/tools/bash.d.ts.map +1 -1
  29. package/dist/core/tools/bash.js +2 -2
  30. package/dist/core/tools/bash.js.map +1 -1
  31. package/dist/core/tools/plugins.d.ts.map +1 -1
  32. package/dist/core/tools/plugins.js +2 -1
  33. package/dist/core/tools/plugins.js.map +1 -1
  34. package/dist/core/tools/read.d.ts.map +1 -1
  35. package/dist/core/tools/read.js +2 -1
  36. package/dist/core/tools/read.js.map +1 -1
  37. package/dist/core/tools/search.d.ts.map +1 -1
  38. package/dist/core/tools/search.js +2 -1
  39. package/dist/core/tools/search.js.map +1 -1
  40. package/dist/core/tools/webfetch.d.ts.map +1 -1
  41. package/dist/core/tools/webfetch.js +2 -1
  42. package/dist/core/tools/webfetch.js.map +1 -1
  43. package/dist/core/tools/websearch.d.ts.map +1 -1
  44. package/dist/core/tools/websearch.js +2 -1
  45. package/dist/core/tools/websearch.js.map +1 -1
  46. package/dist/core/tools/write.d.ts.map +1 -1
  47. package/dist/core/tools/write.js +2 -1
  48. package/dist/core/tools/write.js.map +1 -1
  49. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  50. package/dist/modes/interactive/command-executor.js +95 -65
  51. package/dist/modes/interactive/command-executor.js.map +1 -1
  52. package/dist/modes/interactive/components/ask-options.d.ts +20 -6
  53. package/dist/modes/interactive/components/ask-options.d.ts.map +1 -1
  54. package/dist/modes/interactive/components/ask-options.js +35 -13
  55. package/dist/modes/interactive/components/ask-options.js.map +1 -1
  56. package/dist/modes/interactive/components/footer.d.ts +1 -1
  57. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  58. package/dist/modes/interactive/components/footer.js +3 -2
  59. package/dist/modes/interactive/components/footer.js.map +1 -1
  60. package/dist/modes/interactive/components/keybinding-hints.d.ts +9 -0
  61. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  62. package/dist/modes/interactive/components/keybinding-hints.js +12 -0
  63. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  64. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/settings-selector.js +1 -1
  66. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  67. package/dist/modes/interactive/components/task-panel.d.ts +2 -2
  68. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/task-panel.js +24 -11
  70. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  71. package/dist/modes/interactive/components/tool-chain-summary.d.ts +2 -1
  72. package/dist/modes/interactive/components/tool-chain-summary.d.ts.map +1 -1
  73. package/dist/modes/interactive/components/tool-chain-summary.js +2 -1
  74. package/dist/modes/interactive/components/tool-chain-summary.js.map +1 -1
  75. package/dist/modes/interactive/components/tool-chain.d.ts +2 -9
  76. package/dist/modes/interactive/components/tool-chain.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/tool-chain.js +3 -20
  78. package/dist/modes/interactive/components/tool-chain.js.map +1 -1
  79. package/dist/modes/interactive/components/tool-execution.d.ts +16 -20
  80. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  81. package/dist/modes/interactive/components/tool-execution.js +38 -37
  82. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  83. package/dist/modes/interactive/components/tool-signal.d.ts.map +1 -1
  84. package/dist/modes/interactive/components/tool-signal.js +1 -1
  85. package/dist/modes/interactive/components/tool-signal.js.map +1 -1
  86. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  87. package/dist/modes/interactive/components/tree-selector.js +29 -12
  88. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  89. package/dist/modes/interactive/interactive-mode.d.ts +45 -20
  90. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  91. package/dist/modes/interactive/interactive-mode.js +140 -97
  92. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  93. package/dist/modes/interactive/model-controller.d.ts +3 -0
  94. package/dist/modes/interactive/model-controller.d.ts.map +1 -1
  95. package/dist/modes/interactive/model-controller.js +1 -1
  96. package/dist/modes/interactive/model-controller.js.map +1 -1
  97. package/docs/keybindings.md +247 -78
  98. package/docs/terminal-setup.md +8 -3
  99. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  100. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  101. package/examples/extensions/sandbox/package.json +1 -1
  102. package/examples/extensions/with-deps/package.json +1 -1
  103. package/package.json +4 -4
@@ -31,7 +31,7 @@ import { SessionManager } from "../../core/session-manager.js";
31
31
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
32
32
  import { startupProgress } from "../../core/startup-progress.js";
33
33
  import { taskStore } from "../../core/task-store.js";
34
- import { cycleToolOutputView } from "../../core/tool-output-view.js";
34
+ import { cycleToolOutputView, DEFAULT_TOOL_OUTPUT_VIEW, MAX_TOOL_OUTPUT_VIEW, } from "../../core/tool-output-view.js";
35
35
  import { settleDanglingMainTasks } from "../../core/tools/todo.js";
36
36
  import { buildCompactWordmark } from "../../core/wordmark.js";
37
37
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
@@ -51,7 +51,7 @@ import { CustomEditor } from "./components/custom-editor.js";
51
51
  import { CustomMessageComponent } from "./components/custom-message.js";
52
52
  import { DynamicBorder } from "./components/dynamic-border.js";
53
53
  import { FooterComponent } from "./components/footer.js";
54
- import { keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
54
+ import { appKeyLabel, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
55
55
  import { renderSessionChip } from "./components/session-chip.js";
56
56
  import { SessionColorSelectorComponent } from "./components/session-color-selector.js";
57
57
  import { SessionSelectorComponent } from "./components/session-selector.js";
@@ -185,6 +185,8 @@ export class InteractiveMode {
185
185
  startupNoticesShown = false;
186
186
  // Status line tracking (for mutating immediately-sequential status updates)
187
187
  lastStatusSpacer = undefined;
188
+ /** Dial reverses already named this session; see showDialStep. */
189
+ dialReverseTaught = new Set();
188
190
  lastStatusText = undefined;
189
191
  // Streaming message tracking
190
192
  streamingComponent = undefined;
@@ -202,10 +204,21 @@ export class InteractiveMode {
202
204
  });
203
205
  // Tool execution tracking: toolCallId -> component
204
206
  pendingTools = new Map();
205
- // Tool output expansion state
206
- toolOutputExpanded = false;
207
- // Persisted view dial (radar / glance / full). See core/tool-output-view.ts.
208
- toolOutputView = "glance";
207
+ // Persisted view dial (radar / peek / full). See core/tool-output-view.ts.
208
+ toolOutputView = DEFAULT_TOOL_OUTPUT_VIEW;
209
+ // Where `app.tools.expand` came from, so the same key goes back there. Kept
210
+ // in memory only: the jump is the momentary look, the dial is the decision.
211
+ viewBeforeJump;
212
+ /**
213
+ * Whether everything foldable is currently open.
214
+ *
215
+ * Not a state of its own any more — it is the top of the view dial. The
216
+ * header, compaction and branch summaries and skill blocks follow the dial
217
+ * for the same reason tool bodies do: `full` means nothing is held back.
218
+ */
219
+ get toolOutputExpanded() {
220
+ return this.toolOutputView === MAX_TOOL_OUTPUT_VIEW;
221
+ }
209
222
  // The chain currently collecting tool calls, if the agent is mid-run.
210
223
  openChain;
211
224
  /** The newest tool block in the transcript; radar marks it. */
@@ -450,6 +463,7 @@ export class InteractiveMode {
450
463
  },
451
464
  showSelector: (create) => this.showSelector(create),
452
465
  showStatus: (message) => this.showStatus(message),
466
+ showDialStep: (backward, message) => this.showDialStep(backward, message),
453
467
  showError: (message) => this.showError(message),
454
468
  showWarning: (message) => this.showWarning(message),
455
469
  showNotice: (title, body) => this.showNotice(title, body),
@@ -698,34 +712,53 @@ export class InteractiveMode {
698
712
  : theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
699
713
  // Build startup instructions using keybinding hint helpers
700
714
  const hint = (keybinding, description) => keyHint(keybinding, description);
715
+ // A dial as one line in one shape: the key steps it forward, the same
716
+ // key with shift steps it back. The banner is where that rule is first
717
+ // read, and it is the rule covering the most-pressed keys in the app,
718
+ // so the dials are listed together rather than scattered by topic.
719
+ // appKeyLabel, not keyText: the thinking dial answers to shift+tab as well,
720
+ // and splicing that into the middle of `alt+t/shift+alt+t` would hide the
721
+ // one shape these six lines exist to show.
722
+ const dial = (forward, backward, subject) => rawKeyHint(`${appKeyLabel(forward)}/${appKeyLabel(backward)}`, `to step ${subject}`);
723
+ // Grouped the way the map itself is (see core/keybindings.ts): by what you
724
+ // are trying to do, not by what the widget is. A flat list of thirty
725
+ // hints is a wall nobody reads; five short groups with a heading each is
726
+ // something a person can skim once and place a key in later.
727
+ const group = (title) => theme.fg("dim", `\n${title}`);
701
728
  const expandedInstructions = [
729
+ group("Compose — the message in your hands"),
730
+ hint("app.editor.external", "for external editor"),
731
+ hint("app.input.voiceTranscribe", "to speak instead of type"),
732
+ hint("app.clipboard.pasteImage", "to paste image"),
733
+ hint("app.message.followUp", "to queue follow-up"),
734
+ hint("app.message.dequeue", "to edit all queued messages"),
735
+ rawKeyHint("drop files", "to attach"),
736
+ rawKeyHint("/", "for commands"),
737
+ rawKeyHint("!", "to run bash"),
738
+ rawKeyHint("!!", "to run bash (no context)"),
739
+ group("Steer — what the agent is before it runs"),
740
+ dial("app.mode.cycleForward", "app.mode.cycleBackward", "agent mode (ask/plan/build/debug)"),
741
+ dial("app.model.cycleForward", "app.model.cycleBackward", "model — /model to pick one"),
742
+ dial("app.thinking.cycleForward", "app.thinking.cycleBackward", "thinking level"),
743
+ group("Read — what you see of what it did"),
744
+ dial("app.view.cycleForward", "app.view.cycleBackward", "tool output (radar/peek/full)"),
745
+ dial("app.tasks.cycleForward", "app.tasks.cycleBackward", "task panel view"),
746
+ hint("app.tools.expand", "to jump to full output and back"),
747
+ hint("app.thinking.toggle", "to show or hide thinking"),
748
+ ...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
749
+ group("Go — sessions and places"),
750
+ hint("app.session.resume", "to resume a session"),
751
+ hint("app.session.changeDirectory", "to change working directory"),
752
+ dial("app.session.color.cycleForward", "app.session.color.cycleBackward", "session colour"),
753
+ hint("app.settings.open", "for settings"),
754
+ hint("app.hotkeys.open", "for all shortcuts"),
755
+ group("Flow — getting out, getting back"),
702
756
  hint("app.interrupt", "to interrupt"),
703
757
  hint("app.clear", "to clear"),
704
758
  rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
705
759
  hint("app.exit", "to exit (empty)"),
706
760
  hint("app.suspend", "to suspend"),
707
761
  keyHint("tui.editor.deleteToLineEnd", "to delete to end"),
708
- hint("app.thinking.cycle", "to cycle thinking level"),
709
- rawKeyHint(`${keyText("app.model.cycleForward")}/${keyText("app.model.cycleBackward")}`, "to cycle models"),
710
- hint("app.model.select", "to select model"),
711
- hint("app.tools.expand", "to expand all tool output"),
712
- hint("app.tools.unfoldOne", "to open one chain or block (repeat to peel back)"),
713
- hint("app.view.cycleForward", "to cycle tool output (radar/glance/full)"),
714
- hint("app.thinking.toggle", "to expand thinking"),
715
- hint("app.tasks.cycleView", "to cycle task panel view"),
716
- ...(this.teamFocus.connected ? [hint("app.team.focus", "to focus team roster")] : []),
717
- hint("app.mode.cycle", "to cycle agent mode"),
718
- hint("app.session.changeDirectory", "to change working directory"),
719
- hint("app.settings.open", "for settings"),
720
- hint("app.hotkeys.open", "for all shortcuts"),
721
- hint("app.editor.external", "for external editor"),
722
- rawKeyHint("/", "for commands"),
723
- rawKeyHint("!", "to run bash"),
724
- rawKeyHint("!!", "to run bash (no context)"),
725
- hint("app.message.followUp", "to queue follow-up"),
726
- hint("app.message.dequeue", "to edit all queued messages"),
727
- hint("app.clipboard.pasteImage", "to paste image"),
728
- rawKeyHint("drop files", "to attach"),
729
762
  ].join("\n");
730
763
  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}.`);
731
764
  this.builtInHeader = new ExpandableText(() => logo(), () => `${logo()}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
@@ -1500,20 +1533,22 @@ export class InteractiveMode {
1500
1533
  this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
1501
1534
  this.defaultEditor.onCtrlD = () => this.handleCtrlD();
1502
1535
  this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ());
1503
- this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel());
1536
+ this.defaultEditor.onAction("app.thinking.cycleForward", () => this.cycleThinkingLevel("forward"));
1537
+ this.defaultEditor.onAction("app.thinking.cycleBackward", () => this.cycleThinkingLevel("backward"));
1504
1538
  this.defaultEditor.onAction("app.model.cycleForward", () => this.modelController.cycleModel("forward"));
1505
1539
  this.defaultEditor.onAction("app.model.cycleBackward", () => this.modelController.cycleModel("backward"));
1506
1540
  // Global debug handler on TUI (works regardless of focus)
1507
1541
  this.ui.onDebug = () => this.commandExecutor.handleDebug();
1508
1542
  this.defaultEditor.onAction("app.model.select", () => this.modelController.showModelSelector());
1509
- this.defaultEditor.onAction("app.tools.expand", () => this.toggleToolOutputExpansion());
1543
+ this.defaultEditor.onAction("app.tools.expand", () => this.jumpToFullView());
1510
1544
  this.defaultEditor.onAction("app.view.cycleForward", () => this.cycleToolOutputView("forward"));
1511
1545
  this.defaultEditor.onAction("app.view.cycleBackward", () => this.cycleToolOutputView("backward"));
1512
- this.defaultEditor.onAction("app.tools.unfoldOne", () => this.stepToolOutput("unfold"));
1513
- this.defaultEditor.onAction("app.tools.foldOne", () => this.stepToolOutput("fold"));
1514
1546
  this.defaultEditor.onAction("app.thinking.toggle", () => this.toggleThinkingBlockVisibility());
1515
- this.defaultEditor.onAction("app.tasks.cycleView", () => {
1516
- this.taskPanel.cycleView();
1547
+ this.defaultEditor.onAction("app.tasks.cycleForward", () => {
1548
+ this.taskPanel.cycleView("forward");
1549
+ });
1550
+ this.defaultEditor.onAction("app.tasks.cycleBackward", () => {
1551
+ this.taskPanel.cycleView("backward");
1517
1552
  });
1518
1553
  this.defaultEditor.onAction("app.team.focus", () => this.teamFocus.enterFocus());
1519
1554
  this.defaultEditor.onAction("app.editor.external", () => this.openExternalEditor());
@@ -1534,7 +1569,8 @@ export class InteractiveMode {
1534
1569
  this.defaultEditor.onAction("app.session.color.cycleBackward", () => this.cycleSessionColor("backward"));
1535
1570
  this.defaultEditor.onAction("app.settings.open", () => this.showSettingsSelector());
1536
1571
  this.defaultEditor.onAction("app.hotkeys.open", () => this.commandExecutor.handleHotkeys());
1537
- this.defaultEditor.onAction("app.mode.cycle", () => void this.cycleAgentMode());
1572
+ this.defaultEditor.onAction("app.mode.cycleForward", () => void this.cycleAgentMode("forward"));
1573
+ this.defaultEditor.onAction("app.mode.cycleBackward", () => void this.cycleAgentMode("backward"));
1538
1574
  this.defaultEditor.onChange = (text) => {
1539
1575
  const wasBashMode = this.isBashMode;
1540
1576
  this.isBashMode = text.trimStart().startsWith("!");
@@ -2022,7 +2058,6 @@ export class InteractiveMode {
2022
2058
  imageWidthCells: this.settingsManager.getImageWidthCells(),
2023
2059
  view: this.toolOutputView,
2024
2060
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2025
- component.setExpanded(this.toolOutputExpanded);
2026
2061
  this.attachToolBlock(component);
2027
2062
  this.pendingTools.set(content.id, component);
2028
2063
  }
@@ -2089,7 +2124,6 @@ export class InteractiveMode {
2089
2124
  imageWidthCells: this.settingsManager.getImageWidthCells(),
2090
2125
  view: this.toolOutputView,
2091
2126
  }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
2092
- component.setExpanded(this.toolOutputExpanded);
2093
2127
  this.attachToolBlock(component);
2094
2128
  this.pendingTools.set(event.toolCallId, component);
2095
2129
  }
@@ -2261,6 +2295,28 @@ export class InteractiveMode {
2261
2295
  * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
2262
2296
  * we update the previous status line instead of appending new ones to avoid log spam.
2263
2297
  */
2298
+ /**
2299
+ * Report a dial step, and name the key that steps it back — once.
2300
+ *
2301
+ * The instant after someone moves a dial is the one moment they are primed to
2302
+ * learn its other half: they have just used one direction and can feel the
2303
+ * missing one. So the reverse rides along with the first step of each dial in
2304
+ * a session and never again. A hint that repeats forever stops being read,
2305
+ * and its cost falls entirely on the people who already know it.
2306
+ *
2307
+ * Only the three dials that announce their new value in the transcript come
2308
+ * through here. The other three announce themselves where they live — the
2309
+ * task panel prints its own key in its header, and the mode chip and view
2310
+ * glyph change in place in the footer.
2311
+ */
2312
+ showDialStep(backward, message) {
2313
+ if (this.dialReverseTaught.has(backward)) {
2314
+ this.showStatus(message);
2315
+ return;
2316
+ }
2317
+ this.dialReverseTaught.add(backward);
2318
+ this.showStatus(theme.fg("dim", message) + theme.fg("halftone", ` ${keyText(backward)} steps back`));
2319
+ }
2264
2320
  showStatus(message) {
2265
2321
  const children = this.chatContainer.children;
2266
2322
  const last = children.length > 0 ? children[children.length - 1] : undefined;
@@ -2388,7 +2444,6 @@ export class InteractiveMode {
2388
2444
  imageWidthCells: this.settingsManager.getImageWidthCells(),
2389
2445
  view: this.toolOutputView,
2390
2446
  }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2391
- component.setExpanded(this.toolOutputExpanded);
2392
2447
  this.attachToolBlock(component);
2393
2448
  if (message.stopReason === "aborted" || message.stopReason === "error") {
2394
2449
  let errorMessage;
@@ -2645,15 +2700,15 @@ export class InteractiveMode {
2645
2700
  }
2646
2701
  this.ui.requestRender();
2647
2702
  }
2648
- cycleThinkingLevel() {
2649
- const newLevel = this.session.cycleThinkingLevel();
2703
+ cycleThinkingLevel(direction) {
2704
+ const newLevel = this.session.cycleThinkingLevel(direction);
2650
2705
  if (newLevel === undefined) {
2651
2706
  this.showStatus("Current model does not support thinking");
2652
2707
  }
2653
2708
  else {
2654
2709
  this.footer.invalidate();
2655
2710
  this.updateEditorBorderColor();
2656
- this.showStatus(`Thinking level: ${newLevel}`);
2711
+ this.showDialStep("app.thinking.cycleBackward", `Thinking level: ${newLevel}`);
2657
2712
  }
2658
2713
  }
2659
2714
  /**
@@ -2714,66 +2769,35 @@ export class InteractiveMode {
2714
2769
  }
2715
2770
  return blocks;
2716
2771
  }
2717
- /** Every chain in the transcript, in order. */
2718
- transcriptChains() {
2719
- return this.chatContainer.children.filter((child) => child instanceof ToolChainComponent);
2720
- }
2721
- toggleToolOutputExpansion() {
2722
- this.setToolsExpanded(!this.toolOutputExpanded);
2723
- }
2724
2772
  /**
2725
- * Open or close one tool block instead of all of them.
2773
+ * Jump to the full view, or back to where the jump started.
2726
2774
  *
2727
- * `ctrl+o` is all-or-nothing, which left the caret on every glance and
2728
- * radar row advertising something no key could do. This is the missing half:
2729
- * unfold walks back from the newest block to the first one still folded,
2730
- * fold walks back to the most recently opened one.
2775
+ * The dial's top stop is the whole result with nothing trimmed, which is what
2776
+ * "expand" always meant so expand is not a second state layered over the
2777
+ * dial any more, it is a jump along it. From any stop the first press lands
2778
+ * on `full`; the next press returns to the stop you came from (`peek` if the
2779
+ * jump started there, so the key is never a no-op).
2731
2780
  *
2732
- * Working from the tail is not a shortcut, it is the only thing this view can
2733
- * honestly offer. The transcript is bottom-anchored and has no app-level
2734
- * scrolling — anything far enough up is in the terminal's own scrollback,
2735
- * where this process cannot put a cursor or scroll to a selection. So the
2736
- * key peels backwards through what is actually on screen, and each block it
2737
- * opens is its own marker for where you have got to.
2781
+ * Only `app.view.cycleForward` writes the setting. This key deliberately does
2782
+ * not: the dial is the decision you keep, the jump is the look you take.
2738
2783
  */
2739
- stepToolOutput(direction) {
2740
- const wantOpen = direction === "unfold";
2741
- // Radar's unit is the chain: one press turns the newest summary line back
2742
- // into the calls it stands for. Its per-call bodies are a glance/full
2743
- // question, so the step stops there rather than cascading.
2744
- if (this.toolOutputView === "radar") {
2745
- const chains = this.transcriptChains();
2746
- for (let i = chains.length - 1; i >= 0; i--) {
2747
- if (chains[i].isEmpty || chains[i].isOpened === wantOpen)
2748
- continue;
2749
- // A chain of one is never summarised, so there is nothing for unfold to
2750
- // reveal; skipping it keeps the press moving to a chain that will change.
2751
- if (wantOpen && !chains[i].isSummarised)
2752
- continue;
2753
- chains[i].setOpened(wantOpen);
2754
- this.ui.requestRender();
2755
- return;
2756
- }
2757
- this.showStatus(wantOpen ? "No collapsed chains below this point" : "No open chains below this point");
2784
+ jumpToFullView() {
2785
+ if (this.toolOutputView === MAX_TOOL_OUTPUT_VIEW) {
2786
+ const back = this.viewBeforeJump ?? DEFAULT_TOOL_OUTPUT_VIEW;
2787
+ this.viewBeforeJump = undefined;
2788
+ this.applyToolOutputView(back, { persist: false });
2758
2789
  return;
2759
2790
  }
2760
- const blocks = this.transcriptToolBlocks();
2761
- for (let i = blocks.length - 1; i >= 0; i--) {
2762
- const block = blocks[i];
2763
- if (block.isRevealed() === wantOpen)
2764
- continue;
2765
- block.setExpanded(wantOpen);
2766
- this.ui.requestRender();
2767
- return;
2768
- }
2769
- this.showStatus(wantOpen ? "No folded tool output below this point" : "No unfolded tool output below this point");
2791
+ this.viewBeforeJump = this.toolOutputView;
2792
+ this.applyToolOutputView(MAX_TOOL_OUTPUT_VIEW, { persist: false });
2770
2793
  }
2771
2794
  /**
2772
- * Move the view dial one stop and report where it landed.
2795
+ * Move the view dial one stop; the footer shows where it landed.
2773
2796
  *
2774
- * The dial is the persistent decision ("how much do I ever want to see"),
2775
- * which is why it saves; `app.tools.expand` stays the momentary one ("open
2776
- * what is in front of me"), which is why it does not.
2797
+ * This is the persistent decision ("how much do I ever want to see"), which
2798
+ * is why it saves. `app.tools.expand` jumps along the same dial without
2799
+ * saving, which is what keeps "the look I am taking now" from overwriting
2800
+ * "the view I live in".
2777
2801
  */
2778
2802
  cycleToolOutputView(direction) {
2779
2803
  const next = cycleToolOutputView(this.toolOutputView, direction);
@@ -2802,10 +2826,17 @@ export class InteractiveMode {
2802
2826
  return "omit";
2803
2827
  return this.hideThinkingBlock ? "label" : "full";
2804
2828
  }
2805
- applyToolOutputView(view) {
2829
+ applyToolOutputView(view, options = {}) {
2806
2830
  const previousThinking = this.thinkingDisplayForView();
2831
+ const wasExpanded = this.toolOutputExpanded;
2807
2832
  this.toolOutputView = view;
2808
- this.settingsManager.setToolOutputView(view);
2833
+ if (options.persist !== false) {
2834
+ this.settingsManager.setToolOutputView(view);
2835
+ // Anything that moves the dial deliberately — the cycle key, the
2836
+ // settings pane — is a new home stop, so the jump has nowhere stale to
2837
+ // return to.
2838
+ this.viewBeforeJump = undefined;
2839
+ }
2809
2840
  this.footer.setToolOutputView(view);
2810
2841
  const thinking = this.thinkingDisplayForView();
2811
2842
  for (const child of this.chatContainer.children) {
@@ -2820,10 +2851,18 @@ export class InteractiveMode {
2820
2851
  if (this.streamingComponent && thinking !== previousThinking) {
2821
2852
  this.streamingComponent.setThinkingDisplay(thinking);
2822
2853
  }
2854
+ // The header and the summary blocks are not tool output, but "full" means
2855
+ // nothing is held back, so they open and close with the dial's top stop.
2856
+ if (this.toolOutputExpanded !== wasExpanded)
2857
+ this.setToolsExpanded(this.toolOutputExpanded);
2823
2858
  this.ui.requestRender();
2824
2859
  }
2860
+ /**
2861
+ * Open or close everything that folds but is not a tool call — the header,
2862
+ * compaction and branch summaries, skill blocks. Tool blocks and chains are
2863
+ * not in this sweep: they take the view dial directly.
2864
+ */
2825
2865
  setToolsExpanded(expanded) {
2826
- this.toolOutputExpanded = expanded;
2827
2866
  const activeHeader = this.chrome.customHeader ?? this.builtInHeader;
2828
2867
  if (isExpandable(activeHeader)) {
2829
2868
  activeHeader.setExpanded(expanded);
@@ -2845,7 +2884,7 @@ export class InteractiveMode {
2845
2884
  * is missing (a `--light` run strips the extension), the key says so instead
2846
2885
  * of guessing.
2847
2886
  */
2848
- async cycleAgentMode() {
2887
+ async cycleAgentMode(direction) {
2849
2888
  const command = this.session.extensionRunner.getCommand("mode");
2850
2889
  if (!command) {
2851
2890
  this.showWarning("Modes are not available in this session");
@@ -2858,8 +2897,12 @@ export class InteractiveMode {
2858
2897
  return;
2859
2898
  }
2860
2899
  const current = this.footerDataProvider.getActiveMode();
2900
+ // indexOf is -1 when the active mode is not in the list; stepping from
2901
+ // there lands on the first mode going forward and the last going back,
2902
+ // which is the useful answer either way.
2861
2903
  const index = modes.indexOf(current);
2862
- const next = modes[(index + 1) % modes.length];
2904
+ const step = direction === "forward" ? 1 : -1;
2905
+ const next = modes[(index + step + modes.length) % modes.length];
2863
2906
  await this.session.prompt(`/mode ${next}`);
2864
2907
  }
2865
2908
  /**
@@ -2876,7 +2919,7 @@ export class InteractiveMode {
2876
2919
  const slot = cycleSessionColorSlot(this.sessionManager.getSessionColorSlot(), direction);
2877
2920
  this.session.setSessionColor(slot);
2878
2921
  const name = sessionColorName(slot);
2879
- this.showStatus(`Session color: ${name ?? slot}`);
2922
+ this.showDialStep("app.session.color.cycleBackward", `Session color: ${name ?? slot}`);
2880
2923
  }
2881
2924
  toggleThinkingBlockVisibility() {
2882
2925
  this.hideThinkingBlock = !this.hideThinkingBlock;