@oh-my-pi/pi-coding-agent 17.2.9 → 17.2.11

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 (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -43,6 +43,8 @@ export interface AgentTranscriptViewerDeps {
43
43
  lifecycle?: () => AgentLifecycleManager;
44
44
  ui: TUI;
45
45
  getTool?: (name: string) => AgentTool | undefined;
46
+ /** Whether the active registry entry came from a built-in factory. */
47
+ isBuiltInTool?: (name: string) => boolean;
46
48
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
47
49
  cwd: string;
48
50
  hideThinkingBlock?: () => boolean;
@@ -161,6 +163,7 @@ export class AgentTranscriptViewer implements Component {
161
163
  this.#builder = new ChatTranscriptBuilder({
162
164
  ui: deps.ui,
163
165
  getTool: deps.getTool,
166
+ isBuiltInTool: deps.isBuiltInTool,
164
167
  getMessageRenderer: deps.getMessageRenderer,
165
168
  cwd: deps.cwd,
166
169
  hideThinkingBlock: deps.hideThinkingBlock,
@@ -11,7 +11,7 @@ import {
11
11
  Text,
12
12
  } from "@oh-my-pi/pi-tui";
13
13
  import { formatNumber } from "@oh-my-pi/pi-utils";
14
- import chalk from "chalk";
14
+ import chalk from "@oh-my-pi/pi-utils/chalk";
15
15
  import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
16
16
  import { getMarkdownTheme, theme } from "../../modes/theme/theme";
17
17
  import { expandKeyHint, getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
@@ -61,6 +61,8 @@ import { CollapsedSyntheticMessageComponent, UserMessageComponent } from "./user
61
61
  export interface ChatTranscriptBuilderDeps {
62
62
  ui: TUI;
63
63
  getTool?: (name: string) => AgentTool | undefined;
64
+ /** Whether the active registry entry came from a built-in factory. */
65
+ isBuiltInTool?: (name: string) => boolean;
64
66
  getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
65
67
  cwd: string;
66
68
  hideThinkingBlock?: () => boolean;
@@ -392,6 +394,7 @@ export class ChatTranscriptBuilder {
392
394
  content.name,
393
395
  content.arguments,
394
396
  {
397
+ useBuiltInRenderer: this.deps.isBuiltInTool?.(content.name) ?? true,
395
398
  // Stable ids and Kitty placeholder cells keep images anchored
396
399
  // while the transcript viewport scrolls and reflows.
397
400
  showImages: settings.get("terminal.showImages"),
@@ -30,7 +30,6 @@ type ConfigurableEditorAction = Extract<
30
30
  | "app.model.cycleBackward"
31
31
  | "app.model.select"
32
32
  | "app.model.selectTemporary"
33
- | "app.tools.expand"
34
33
  | "app.tools.toggleVisibility"
35
34
  | "app.thinking.toggle"
36
35
  | "app.editor.external"
@@ -53,7 +52,6 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
53
52
  "app.model.cycleBackward": ["shift+ctrl+p"],
54
53
  "app.model.select": ["alt+m"],
55
54
  "app.model.selectTemporary": ["alt+p"],
56
- "app.tools.expand": ["ctrl+o"],
57
55
  "app.tools.toggleVisibility": ["ctrl+shift+o"],
58
56
  "app.thinking.toggle": ["ctrl+t"],
59
57
  "app.editor.external": ["ctrl+g"],
@@ -551,7 +549,6 @@ export class CustomEditor extends Editor {
551
549
  onCycleModelForward?: () => void;
552
550
  onCycleModelBackward?: () => void;
553
551
  onSelectModel?: () => void;
554
- onExpandTools?: () => void;
555
552
  onToggleToolActivity?: () => void;
556
553
  onToggleThinking?: () => void;
557
554
  onExternalEditor?: () => void;
@@ -907,12 +904,6 @@ export class CustomEditor extends Editor {
907
904
  return;
908
905
  }
909
906
 
910
- // Intercept configured tool output expansion shortcut
911
- if (this.#matchesAction(canonical, "app.tools.expand") && this.onExpandTools) {
912
- this.onExpandTools();
913
- return;
914
- }
915
-
916
907
  // Intercept configured tool activity visibility toggle
917
908
  if (this.#matchesAction(canonical, "app.tools.toggleVisibility") && this.onToggleToolActivity) {
918
909
  this.onToggleToolActivity();
@@ -4,7 +4,7 @@
4
4
  * Shows name, description, origin, status, and kind-specific preview.
5
5
  */
6
6
  import * as os from "node:os";
7
- import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
7
+ import { arkToWireSchema, isArkSchema } from "@oh-my-pi/pi-ai/utils/schema";
8
8
  import { type Component, truncateToWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
9
9
  import { theme } from "../../../modes/theme/theme";
10
10
  import { shortenPath } from "../../../tools/render-utils";
@@ -168,16 +168,21 @@ export class InspectorPanel implements Component {
168
168
  lines.push(theme.fg("dim", theme.boxRound.horizontal.repeat(Math.min(width - 2, 40))));
169
169
 
170
170
  try {
171
- const tool = raw as any;
172
- const wire = (s: unknown): any => (isZodSchema(s) ? zodToWireSchema(s) : s);
173
- const paramSchema = wire(tool?.parameters);
174
- const inputSchema = wire(tool?.inputSchema);
171
+ const tool = raw as { parameters?: unknown; inputSchema?: unknown };
172
+ const wire = (schema: unknown): unknown => (isArkSchema(schema) ? arkToWireSchema(schema) : schema);
173
+ const paramSchema = wire(tool.parameters) as Record<string, unknown> | undefined;
174
+ const inputSchema = wire(tool.inputSchema) as Record<string, unknown> | undefined;
175
175
  const params = paramSchema?.properties || inputSchema?.properties || {};
176
176
 
177
177
  if (Object.keys(params).length === 0) {
178
178
  lines.push(theme.fg("dim", " (no arguments)"));
179
179
  } else {
180
- const required = new Set(paramSchema?.required || inputSchema?.required || []);
180
+ const requiredValue = paramSchema?.required ?? inputSchema?.required;
181
+ const required = new Set(
182
+ Array.isArray(requiredValue)
183
+ ? requiredValue.filter((value): value is string => typeof value === "string")
184
+ : [],
185
+ );
181
186
 
182
187
  for (const [name, spec] of Object.entries(params)) {
183
188
  const param = spec as any;
@@ -212,9 +212,7 @@ export class FooterComponent implements Component {
212
212
  rightSide = `${modelName} • ${resolved ? resolved : `${theme.thinking.autoPending} auto`}`;
213
213
  } else {
214
214
  const thinkingLevel = state.thinkingLevel ?? ThinkingLevel.Off;
215
- if (thinkingLevel !== ThinkingLevel.Off) {
216
- rightSide = `${modelName} • ${thinkingLevel}`;
217
- }
215
+ rightSide = `${modelName} ${thinkingLevel}`;
218
216
  }
219
217
  }
220
218
 
@@ -254,8 +254,16 @@ export function thinkingLevelGlyph(level: ConfiguredThinkingLevel): string {
254
254
  }
255
255
 
256
256
  /**
257
- * A slim role chip: `●default ◉` — solid dot for configured assignments,
257
+ * A slim role chip: `● default ◉` — solid dot for configured assignments,
258
258
  * hollow for auto-selected fallbacks, thinking glyph attached when set.
259
+ *
260
+ * The space after the status glyph is load-bearing. Under the `nerd` preset
261
+ * these are Nerd Font private-use icons (U+F111 / U+F10C) whose glyphs are
262
+ * drawn two cells wide, while `visibleWidth` counts them as one
263
+ * (`ambiguousIsNarrow: true` in tui/utils.ts — the PUA block is
264
+ * East_Asian_Width=Ambiguous). Without a separator the icon overhangs and
265
+ * eats the label's first character (`● default` renders as `●efault`).
266
+ * Mirrors the spacing already used for `status.success` in model-hub.
259
267
  */
260
268
  export function formatRoleChip(role: string, assignment: RoleAssignment, settings: Settings): string {
261
269
  const info = getRoleInfo(role, settings);
@@ -263,9 +271,9 @@ export function formatRoleChip(role: string, assignment: RoleAssignment, setting
263
271
  const glyph = thinkingLevelGlyph(assignment.thinkingLevel);
264
272
  const suffix = glyph ? ` ${theme.fg("dim", glyph)}` : "";
265
273
  if (assignment.autoSelected) {
266
- return theme.fg("dim", `${theme.status.shadowed}${label}`) + suffix;
274
+ return theme.fg("dim", `${theme.status.shadowed} ${label}`) + suffix;
267
275
  }
268
- return theme.fg(info.color ?? "muted", `${theme.status.enabled}${label}`) + suffix;
276
+ return theme.fg(info.color ?? "muted", `${theme.status.enabled} ${label}`) + suffix;
269
277
  }
270
278
 
271
279
  /** `$in/out` per-million cost pair; `free` when both legs are zero. */
@@ -837,7 +837,10 @@ export class ModelHubComponent implements Component {
837
837
  chips.push({
838
838
  label,
839
839
  styled: assignedHere
840
- ? theme.fg(info.color ?? "muted", `${theme.status.enabled}${label}`) +
840
+ ? // Separator required: under the `nerd` preset this glyph is a
841
+ // two-cell-wide PUA icon that `visibleWidth` counts as one, so
842
+ // without it the icon overhangs and eats `label`'s first char.
843
+ theme.fg(info.color ?? "muted", `${theme.status.enabled} ${label}`) +
841
844
  theme.fg("dim", ` ${theme.status.success}`)
842
845
  : theme.fg(info.color ?? "muted", label),
843
846
  role,
@@ -1556,6 +1556,7 @@ export class StatusLineComponent implements Component {
1556
1556
  return {
1557
1557
  session: this.session,
1558
1558
  focusedAgentId: this.#focusedAgentId,
1559
+ sessionAccent: this.#resolveSettings().sessionAccent !== false,
1559
1560
  activeRepo: activeRepoCache.activeRepo,
1560
1561
  width,
1561
1562
  options: segmentOptions ?? {},
@@ -117,9 +117,10 @@ const modelSegment: StatusLineSegment = {
117
117
  : `${theme.thinking.autoPending} auto`;
118
118
  } else {
119
119
  const level = state.thinkingLevel ?? ThinkingLevel.Off;
120
- if (level !== ThinkingLevel.Off) {
121
- thinkingDisplay = theme.thinking[level as keyof typeof theme.thinking] ?? "";
122
- }
120
+ thinkingDisplay =
121
+ level === ThinkingLevel.Off
122
+ ? `${theme.status.disabled} off`
123
+ : (theme.thinking[level as keyof typeof theme.thinking] ?? level);
123
124
  }
124
125
  }
125
126
 
@@ -591,10 +592,12 @@ const sessionNameSegment: StatusLineSegment = {
591
592
  const name = sessionManager?.getSessionName();
592
593
  if (!name) return { content: "", visible: false };
593
594
 
594
- const ansi =
595
- getSessionAccentAnsi(
596
- getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
597
- ) ?? theme.getFgAnsi("accent");
595
+ const accentEnabled = ctx.sessionAccent !== false;
596
+ const ansi = accentEnabled
597
+ ? (getSessionAccentAnsi(
598
+ getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
599
+ ) ?? theme.getFgAnsi("accent"))
600
+ : theme.getFgAnsi("accent");
598
601
  return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
599
602
  },
600
603
  };
@@ -52,6 +52,8 @@ export interface SegmentContext {
52
52
  session: AgentSession;
53
53
  /** Focused subagent id while the view is proxied at its session, undefined otherwise. */
54
54
  focusedAgentId?: string | undefined;
55
+ /** Effective `statusLine.sessionAccent`; `false` disables hash-derived accent colors, while `true` or omission enables them. */
56
+ sessionAccent?: boolean;
55
57
  activeRepo: ActiveRepoContext | null;
56
58
  width: number;
57
59
  options: StatusLineSegmentOptions;
@@ -23,7 +23,7 @@ import { formatDefaultToolExecution } from "../../tools/default-renderer";
23
23
  import { EVAL_DEFAULT_PREVIEW_LINES } from "../../tools/eval";
24
24
  import { isWaitingPollDetails } from "../../tools/hub";
25
25
  import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../../tools/render-utils";
26
- import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
26
+ import { type FirstResultViewportRepaint, type ToolRenderer, toolRenderers } from "../../tools/renderers";
27
27
  import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
28
28
  import type { XdevState } from "../../tools/xdev";
29
29
  import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
@@ -226,6 +226,8 @@ export interface ToolExecutionOptions {
226
226
  /** Session-persistent edit clipboard register, forked per preview frame. */
227
227
  clipboard?: Clipboard;
228
228
  showImages?: boolean; // default: true (only used if terminal supports images)
229
+ /** Allow the name-keyed renderer registry only when the active tool is the built-in implementation. */
230
+ useBuiltInRenderer?: boolean;
229
231
  editFuzzyThreshold?: number;
230
232
  editAllowFuzzy?: boolean;
231
233
  /** Live-region probe used to settle detached task progress once the block
@@ -312,6 +314,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
312
314
  // forcing the common image-free result to re-shape on every resize tick.
313
315
  #renderedImageCount = 0;
314
316
  #tool?: AgentTool;
317
+ #renderer?: ToolRenderer;
315
318
  #ui: ToolExecutionUi;
316
319
  #cwd: string;
317
320
  #result?: {
@@ -398,6 +401,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
398
401
  super();
399
402
  this.#toolName = toolName;
400
403
  this.#toolLabel = tool?.label ?? toolName;
404
+ this.#renderer = options.useBuiltInRenderer === false ? undefined : toolRenderers[toolName];
401
405
  this.#showImages = options.showImages ?? true;
402
406
  this.#editFuzzyThreshold = options.editFuzzyThreshold;
403
407
  this.#editAllowFuzzy = options.editAllowFuzzy;
@@ -418,10 +422,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
418
422
  this.#contentBox = new Box(0, 1);
419
423
  this.#contentText = new WidthAwareText(contentWidth => this.#renderDefaultCard(contentWidth), 1, 1);
420
424
 
421
- // Use Box for custom tools or built-in tools that have renderers
422
- const hasRenderer = toolName in toolRenderers;
425
+ // Use Box for custom tools or built-in tools with rich renderers.
423
426
  const hasCustomRenderer = !!(tool?.renderCall || tool?.renderResult);
424
- this.#usesContentBox = hasCustomRenderer || hasRenderer;
427
+ this.#usesContentBox = hasCustomRenderer || this.#renderer !== undefined;
425
428
  if (this.#usesContentBox) {
426
429
  this.addChild(this.#contentBox);
427
430
  } else {
@@ -668,12 +671,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
668
671
  const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
669
672
  const isBackgroundAsyncRunning =
670
673
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
671
- const renderer = toolRenderers[this.#toolName] as
672
- | {
673
- animatedPendingPreview?: boolean | ((args: unknown) => boolean);
674
- animatedPartialResult?: boolean | ((args: unknown) => boolean);
675
- }
676
- | undefined;
674
+ const renderer = this.#renderer;
677
675
  const pendingAnimation = renderer?.animatedPendingPreview;
678
676
  const partialAnimation = renderer?.animatedPartialResult;
679
677
  const pendingCallConsumesSpinner =
@@ -933,7 +931,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
933
931
 
934
932
  #rendererFlag(name: "forceResultViewportRepaintOnSettle"): boolean {
935
933
  const toolValue = (this.#tool as Record<string, unknown> | undefined)?.[name];
936
- const rendererValue = toolRenderers[this.#toolName]?.[name];
934
+ const rendererValue = this.#renderer?.[name];
937
935
  return toolValue === true || (toolValue === undefined && rendererValue === true);
938
936
  }
939
937
 
@@ -948,8 +946,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
948
946
  if (this.#result !== undefined) return false;
949
947
  const toolValue = (this.#tool as { forceFirstResultViewportRepaint?: FirstResultViewportRepaint } | undefined)
950
948
  ?.forceFirstResultViewportRepaint;
951
- const value =
952
- toolValue !== undefined ? toolValue : toolRenderers[this.#toolName]?.forceFirstResultViewportRepaint;
949
+ const value = toolValue !== undefined ? toolValue : this.#renderer?.forceFirstResultViewportRepaint;
953
950
  if (typeof value === "function") return value(this.#args, this.#renderState);
954
951
  return value === true;
955
952
  }
@@ -1101,9 +1098,9 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1101
1098
  const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
1102
1099
  this.#contentBox.setPaddingX(customFramed ? 0 : 1);
1103
1100
  this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
1104
- } else if (this.#toolName in toolRenderers) {
1105
- // Built-in tools with renderers
1106
- const renderer = toolRenderers[this.#toolName];
1101
+ } else if (this.#renderer) {
1102
+ // The active registry entry is a built-in tool with a rich renderer.
1103
+ const renderer = this.#renderer;
1107
1104
 
1108
1105
  // Clean up previous multi-file boxes
1109
1106
  for (const box of this.#multiFileBoxes) {
@@ -286,12 +286,20 @@ class TreeList implements Component {
286
286
 
287
287
  // Apply filter mode
288
288
  let passesFilter = true;
289
- // Entry types hidden in default view (settings/bookkeeping)
289
+ // Entry types hidden in default view (settings/bookkeeping). These carry
290
+ // no conversation content, so the tree only shows them in "all" mode.
290
291
  const isSettingsEntry =
291
292
  entry.type === "label" ||
292
293
  entry.type === "custom" ||
293
294
  entry.type === "model_change" ||
294
- entry.type === "thinking_level_change";
295
+ entry.type === "thinking_level_change" ||
296
+ entry.type === "service_tier_change" ||
297
+ entry.type === "title_change" ||
298
+ entry.type === "credential_pin" ||
299
+ entry.type === "session_init" ||
300
+ entry.type === "ttsr_injection" ||
301
+ entry.type === "mode_change" ||
302
+ entry.type === "reset_boundary";
295
303
 
296
304
  switch (this.#filterMode) {
297
305
  case "user-only":
@@ -390,6 +398,34 @@ class TreeList implements Component {
390
398
  case "label":
391
399
  parts.push("label", entry.label ?? "");
392
400
  break;
401
+ case "service_tier_change":
402
+ parts.push("service tier");
403
+ if (entry.serviceTier) {
404
+ const serviceTier = entry.serviceTier;
405
+ for (const family in serviceTier) {
406
+ const tier = serviceTier[family as keyof typeof serviceTier];
407
+ if (tier) parts.push(family, tier);
408
+ }
409
+ }
410
+ break;
411
+ case "title_change":
412
+ parts.push("title", entry.title);
413
+ break;
414
+ case "mode_change":
415
+ parts.push("mode", entry.mode);
416
+ break;
417
+ case "credential_pin":
418
+ parts.push("credential pin", entry.provider);
419
+ break;
420
+ case "ttsr_injection":
421
+ parts.push("ttsr injection", ...entry.injectedRules);
422
+ break;
423
+ case "reset_boundary":
424
+ parts.push("reset boundary");
425
+ break;
426
+ case "session_init":
427
+ parts.push("session init");
428
+ break;
393
429
  }
394
430
 
395
431
  return parts.join(" ");
@@ -658,8 +694,31 @@ class TreeList implements Component {
658
694
  case "label":
659
695
  result = theme.fg("dim", `[label: ${entry.label ?? "(cleared)"}]`);
660
696
  break;
697
+ case "service_tier_change": {
698
+ // Per-family map, or null when the session went back to the default.
699
+ const tiers = entry.serviceTier
700
+ ? Object.entries(entry.serviceTier)
701
+ .map(([family, tier]) => `${family}:${tier}`)
702
+ .join(" ")
703
+ : "(default)";
704
+ result = theme.fg("dim", `[service tier: ${tiers}]`);
705
+ break;
706
+ }
707
+ case "title_change":
708
+ result = theme.fg("dim", `[title: ${normalize(entry.title)}]`);
709
+ break;
710
+ case "mode_change":
711
+ result = theme.fg("dim", `[mode: ${entry.mode}]`);
712
+ break;
713
+ case "credential_pin":
714
+ result = theme.fg("dim", `[credential pin: ${entry.provider}]`);
715
+ break;
661
716
  default:
662
- result = "";
717
+ // Bookkeeping entries with nothing worth spelling out still get their
718
+ // type. A row that renders to the empty string is worse than a
719
+ // useless one: it draws as a bare bullet with no way to tell what it
720
+ // is or why the tree has a gap in it.
721
+ result = theme.fg("dim", `[${entry.type.replaceAll("_", " ")}]`);
663
722
  }
664
723
 
665
724
  return isSelected ? theme.bold(result) : result;
@@ -1436,7 +1436,10 @@ export class CommandController {
1436
1436
  }
1437
1437
  } catch (error) {
1438
1438
  const message = error instanceof Error ? error.message : String(error);
1439
- if (message === "Handoff cancelled" || (error instanceof Error && error.name === "AbortError")) {
1439
+ // `session.handoff()` normalizes genuine cancellations to this exact message; a
1440
+ // provider error (even one named AbortError) is re-thrown verbatim so it surfaces
1441
+ // as a real failure instead of a false "cancelled".
1442
+ if (message === "Handoff cancelled") {
1440
1443
  this.ctx.showError("Handoff cancelled");
1441
1444
  } else {
1442
1445
  this.ctx.showError(`Handoff failed: ${message}`);
@@ -1073,6 +1073,7 @@ export class EventController {
1073
1073
  content.name,
1074
1074
  renderArgs,
1075
1075
  {
1076
+ useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(content.name),
1076
1077
  snapshots: getFileSnapshotStore(this.ctx.viewSession),
1077
1078
  clipboard: getEditClipboard(this.ctx.viewSession),
1078
1079
  showImages: settings.get("terminal.showImages"),
@@ -1322,6 +1323,7 @@ export class EventController {
1322
1323
  event.toolName,
1323
1324
  event.args,
1324
1325
  {
1326
+ useBuiltInRenderer: this.ctx.viewSession.hasBuiltInTool(event.toolName),
1325
1327
  snapshots: getFileSnapshotStore(this.ctx.viewSession),
1326
1328
  clipboard: getEditClipboard(this.ctx.viewSession),
1327
1329
  showImages: settings.get("terminal.showImages"),
@@ -1605,11 +1607,29 @@ export class EventController {
1605
1607
  typeof details.title === "string" &&
1606
1608
  typeof details.planExists === "boolean"
1607
1609
  ) {
1608
- await this.ctx.handlePlanApproval({
1609
- planFilePath: details.planFilePath,
1610
- title: details.title,
1611
- planExists: details.planExists,
1612
- });
1610
+ // Dispatch the approval WITHOUT blocking the serialized event
1611
+ // dispatch chain. `handlePlanApproval` -> `#approvePlan` awaits
1612
+ // `session.prompt` for the ENTIRE approved-execution turn; awaiting
1613
+ // it here (this handler runs inside `#runSerialized`) would hold the
1614
+ // single dispatch link for the whole run, so the run's own
1615
+ // agent_start / message_start / tool / coalesced message_update
1616
+ // events queue behind it on `#dispatchTail` and the chat stays blank
1617
+ // until execution finishes (issue #7684, follow-up to #5688 which
1618
+ // only closed the overlay). Detaching frees the link the moment this
1619
+ // handler returns; the approval overlay and the turn's live events
1620
+ // then render. The approval flow surfaces its own failures via
1621
+ // `showError`, so only an unexpected rejection is logged here.
1622
+ void this.ctx
1623
+ .handlePlanApproval({
1624
+ planFilePath: details.planFilePath,
1625
+ title: details.title,
1626
+ planExists: details.planExists,
1627
+ })
1628
+ .catch(err => {
1629
+ logger.warn("Plan approval dispatch failed", {
1630
+ error: err instanceof Error ? err.message : String(err),
1631
+ });
1632
+ });
1613
1633
  }
1614
1634
  }
1615
1635
  }
@@ -1636,6 +1656,12 @@ export class EventController {
1636
1656
  // model/thinking level until the terminal settle.
1637
1657
  if (event.isTerminal === false) {
1638
1658
  await this.ctx.flushPendingModelSwitch();
1659
+ // Reaching here means the first guard passed, so `isStreaming` is already
1660
+ // false: a command issued from now on mounts immediately. Leaving earlier
1661
+ // panels queued would render them out of order, minutes later, after the
1662
+ // user was told they were only waiting for the turn. The transcript is
1663
+ // quiescent at a settle, which is the condition #4806 wanted.
1664
+ this.ctx.flushPendingCommandOutput();
1639
1665
  return;
1640
1666
  }
1641
1667
  setTerminalTitleState("idle");
@@ -1896,21 +1922,19 @@ export class EventController {
1896
1922
  this.ctx.retryLoader = undefined;
1897
1923
  this.ctx.statusContainer.disposeChildren();
1898
1924
  }
1899
- if (event.success) {
1900
- let appliedRecovered = false;
1901
- for (const recovered of event.recoveredErrors ?? []) {
1902
- const component = this.#takeRetrySupersededAssistantComponent(recovered.persistenceKey);
1903
- if (!component) continue;
1904
- component.applyRetryRecovery(recovered.retryRecovery);
1905
- if (this.#pinnedErrorComponent === component) this.#pinnedErrorComponent = undefined;
1906
- appliedRecovered = true;
1907
- }
1908
- if (appliedRecovered || (event.recoveredErrors?.length ?? 0) > 0) {
1909
- this.ctx.clearPinnedError();
1910
- }
1911
- this.#clearRetrySupersededAssistantComponents();
1912
- } else {
1913
- this.#clearRetrySupersededAssistantComponents();
1925
+ let appliedRetryUpdate = false;
1926
+ for (const retryError of event.retryErrors ?? []) {
1927
+ const component = this.#takeRetrySupersededAssistantComponent(retryError.persistenceKey);
1928
+ if (!component) continue;
1929
+ component.applyRetryRecovery(retryError.retryRecovery);
1930
+ if (this.#pinnedErrorComponent === component) this.#pinnedErrorComponent = undefined;
1931
+ appliedRetryUpdate = true;
1932
+ }
1933
+ if (appliedRetryUpdate || (event.retryErrors?.length ?? 0) > 0) {
1934
+ this.ctx.clearPinnedError();
1935
+ }
1936
+ this.#clearRetrySupersededAssistantComponents();
1937
+ if (!event.success) {
1914
1938
  this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
1915
1939
  }
1916
1940
  this.#ensureWorkingLoaderWhileStreaming();
@@ -166,7 +166,7 @@ export class ExtensionUiController {
166
166
  this.ctx.sessionManager.appendLabelChange(targetId, label);
167
167
  },
168
168
  getActiveTools: () => this.ctx.session.getEnabledToolNames(),
169
- getAllTools: () => this.ctx.session.getAllToolNames(),
169
+ getAllTools: () => this.ctx.session.getAllToolInfos(),
170
170
  setActiveTools: toolNames => this.ctx.session.setActiveToolsByName(toolNames),
171
171
  setModel: async model => {
172
172
  const key = await this.ctx.session.modelRegistry.getApiKey(model);
@@ -399,7 +399,7 @@ export class ExtensionUiController {
399
399
  this.ctx.sessionManager.appendLabelChange(targetId, label);
400
400
  },
401
401
  getActiveTools: () => this.ctx.session.getEnabledToolNames(),
402
- getAllTools: () => this.ctx.session.getAllToolNames(),
402
+ getAllTools: () => this.ctx.session.getAllToolInfos(),
403
403
  setActiveTools: toolNames => this.ctx.session.setActiveToolsByName(toolNames),
404
404
  setModel: async model => {
405
405
  const key = await this.ctx.session.modelRegistry.getApiKey(model);
@@ -13,6 +13,7 @@ import { renderSegmentTrack } from "../../modes/components/segment-track";
13
13
  import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder";
14
14
  import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
15
15
  import { ToolExecutionComponent } from "../../modes/components/tool-execution";
16
+ import { TreeSelectorComponent } from "../../modes/components/tree-selector";
16
17
  import { expandEmoticons } from "../../modes/emoji-autocomplete";
17
18
  import { materializeImageReferenceLinks, shiftImageMarkers } from "../../modes/image-references";
18
19
  import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
@@ -177,6 +178,7 @@ export class InputController {
177
178
  #focusedPasteListenerInstalled = false;
178
179
  #btwBranchListenerInstalled = false;
179
180
  #btwCopyListenerInstalled = false;
181
+ #expandToolsListenerInstalled = false;
180
182
  // Tap counter for the double-← gesture; reset whenever a quiet gap
181
183
  // (>= LEFT_DOUBLE_TAP_MAX_GAP_MS) starts a fresh sequence. See
182
184
  // #detectLeftDoubleTap.
@@ -278,6 +280,24 @@ export class InputController {
278
280
  return { consume: true };
279
281
  });
280
282
  }
283
+ if (!this.#expandToolsListenerInstalled) {
284
+ this.#expandToolsListenerInstalled = true;
285
+ // `app.tools.expand` (Ctrl+O) toggles the transcript's tool-output
286
+ // preview. It must fire regardless of focus so a truncated edit stays
287
+ // expandable while an approval prompt / select dialog holds keyboard
288
+ // focus (#7837). Defers when the main transcript is not the active
289
+ // surface (a fullscreen/anchored overlay — agent hub, transcript
290
+ // viewer, log viewer, model picker) or when the focused component
291
+ // rebinds Ctrl+O for its own use (the tree selector's filter cycle).
292
+ this.ctx.ui.addInputListener(data => {
293
+ if (!this.ctx.keybindings.matches(data, "app.tools.expand")) return undefined;
294
+ if (this.ctx.ui.hasOverlay()) return undefined;
295
+ if (this.ctx.ui.getFocused() instanceof TreeSelectorComponent && matchesKey(data, "ctrl+o"))
296
+ return undefined;
297
+ this.toggleToolOutputExpansion();
298
+ return { consume: true };
299
+ });
300
+ }
281
301
  this.ctx.editor.onEscape = () => {
282
302
  // Side-channel panels are the topmost view. Esc dismisses them before
283
303
  // touching loop mode, maintenance, or the underlying main turn.
@@ -456,8 +476,6 @@ export class InputController {
456
476
  this.ctx.keybindings.getKeys("app.clipboard.copyPrompt"),
457
477
  );
458
478
  this.ctx.editor.onCopyPrompt = () => this.handleCopyPrompt();
459
- this.ctx.editor.setActionKeys("app.tools.expand", this.ctx.keybindings.getKeys("app.tools.expand"));
460
- this.ctx.editor.onExpandTools = () => this.toggleToolOutputExpansion();
461
479
  this.ctx.editor.setActionKeys(
462
480
  "app.tools.toggleVisibility",
463
481
  this.ctx.keybindings.getKeys("app.tools.toggleVisibility"),