@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.6

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 (348) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
  3. package/dist/cli.js +4438 -3962
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/cli/args.d.ts +4 -1
  6. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  7. package/dist/types/cli/command-help.d.ts +99 -0
  8. package/dist/types/cli/gc-cli.d.ts +1 -0
  9. package/dist/types/cli/help-extra.d.ts +2 -0
  10. package/dist/types/cli/thinking-levels.d.ts +5 -0
  11. package/dist/types/cli/update-cli.d.ts +13 -0
  12. package/dist/types/commands/browser-relay.d.ts +38 -0
  13. package/dist/types/commands/commit.d.ts +3 -0
  14. package/dist/types/commands/complete.d.ts +1 -1
  15. package/dist/types/commands/completions.d.ts +6 -0
  16. package/dist/types/commands/grep.d.ts +3 -0
  17. package/dist/types/commands/join.d.ts +4 -0
  18. package/dist/types/commands/launch-help.d.ts +171 -0
  19. package/dist/types/commands/launch.d.ts +5 -1
  20. package/dist/types/commands/models.d.ts +3 -0
  21. package/dist/types/commands/say.d.ts +10 -0
  22. package/dist/types/commands/worktree.d.ts +3 -0
  23. package/dist/types/config/keybindings.d.ts +5 -0
  24. package/dist/types/config/model-discovery.d.ts +2 -2
  25. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  26. package/dist/types/config/models-config-schema.d.ts +2 -0
  27. package/dist/types/config/models-config.d.ts +1 -0
  28. package/dist/types/config/service-tier.d.ts +2 -0
  29. package/dist/types/config/settings-schema.d.ts +69 -21
  30. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  31. package/dist/types/cursor.d.ts +7 -4
  32. package/dist/types/edit/index.d.ts +3 -3
  33. package/dist/types/edit/modes/replace.d.ts +22 -15
  34. package/dist/types/edit/renderer.d.ts +1 -1
  35. package/dist/types/eval/py/kernel.d.ts +3 -1
  36. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  37. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  38. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  40. package/dist/types/launch/broker.d.ts +1 -1
  41. package/dist/types/launch/client.d.ts +15 -4
  42. package/dist/types/launch/ensure.d.ts +8 -0
  43. package/dist/types/launch/paths.d.ts +1 -1
  44. package/dist/types/launch/protocol.d.ts +19 -2
  45. package/dist/types/lsp/client.d.ts +2 -0
  46. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  47. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  48. package/dist/types/lsp/mux/server.d.ts +18 -0
  49. package/dist/types/lsp/types.d.ts +22 -2
  50. package/dist/types/main.d.ts +2 -0
  51. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  52. package/dist/types/mnemopi/state.d.ts +0 -23
  53. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  54. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  56. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  57. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  58. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  59. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  60. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  61. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  62. package/dist/types/modes/interactive-mode.d.ts +5 -1
  63. package/dist/types/modes/types.d.ts +4 -1
  64. package/dist/types/modes/workflow.d.ts +2 -1
  65. package/dist/types/sdk.d.ts +3 -1
  66. package/dist/types/secrets/index.d.ts +1 -1
  67. package/dist/types/security/contracts/schemas.d.ts +405 -403
  68. package/dist/types/session/agent-session-types.d.ts +8 -1
  69. package/dist/types/session/agent-session.d.ts +26 -2
  70. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  71. package/dist/types/session/launch-completion.d.ts +10 -0
  72. package/dist/types/session/session-advisors.d.ts +1 -1
  73. package/dist/types/session/session-context.d.ts +2 -1
  74. package/dist/types/session/session-entries.d.ts +15 -1
  75. package/dist/types/session/session-maintenance.d.ts +1 -0
  76. package/dist/types/session/session-manager.d.ts +23 -11
  77. package/dist/types/session/session-stats.d.ts +10 -0
  78. package/dist/types/session/session-storage.d.ts +15 -3
  79. package/dist/types/session/yield-queue.d.ts +6 -1
  80. package/dist/types/system-prompt.d.ts +4 -1
  81. package/dist/types/task/index.d.ts +2 -1
  82. package/dist/types/task/spawn-policy.d.ts +5 -0
  83. package/dist/types/thinking.d.ts +1 -7
  84. package/dist/types/tools/ast-grep.d.ts +1 -1
  85. package/dist/types/tools/browser/registry.d.ts +2 -1
  86. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  87. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  88. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  89. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  90. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  91. package/dist/types/tools/browser/render.d.ts +1 -0
  92. package/dist/types/tools/browser.d.ts +3 -0
  93. package/dist/types/tools/computer/exposure.d.ts +9 -7
  94. package/dist/types/tools/computer/protocol.d.ts +70 -17
  95. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  96. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  97. package/dist/types/tools/computer/worker.d.ts +42 -7
  98. package/dist/types/tools/computer-renderer.d.ts +7 -5
  99. package/dist/types/tools/computer.d.ts +25 -44
  100. package/dist/types/tools/context.d.ts +3 -0
  101. package/dist/types/tools/glob.d.ts +1 -1
  102. package/dist/types/tools/grep.d.ts +1 -1
  103. package/dist/types/tools/index.d.ts +9 -0
  104. package/dist/types/tools/render-utils.d.ts +2 -0
  105. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  106. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  107. package/dist/types/tools/xdev.d.ts +8 -0
  108. package/dist/types/utils/clipboard.d.ts +2 -2
  109. package/dist/types/web/kagi.d.ts +1 -0
  110. package/dist/types/web/search/index.d.ts +1 -1
  111. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  112. package/dist/types/web/search/providers/base.d.ts +2 -0
  113. package/dist/types/web/search/providers/brave.d.ts +1 -0
  114. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  115. package/dist/types/web/search/providers/codex.d.ts +1 -0
  116. package/dist/types/web/search/providers/exa.d.ts +1 -0
  117. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  118. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  119. package/dist/types/web/search/providers/jina.d.ts +1 -0
  120. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  121. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  122. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  123. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  124. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  125. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  126. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  127. package/dist/types/web/search/providers/utils.d.ts +6 -9
  128. package/dist/types/web/search/providers/zai.d.ts +1 -0
  129. package/dist/types/web/search/types.d.ts +4 -0
  130. package/package.json +12 -12
  131. package/src/cli/args.ts +7 -87
  132. package/src/cli/browser-relay-cli.ts +119 -0
  133. package/src/cli/command-help.ts +103 -0
  134. package/src/cli/completion-gen.ts +5 -5
  135. package/src/cli/flag-tables.ts +9 -0
  136. package/src/cli/gc-cli.ts +641 -21
  137. package/src/cli/help-extra.ts +89 -0
  138. package/src/cli/setup-cli.ts +13 -21
  139. package/src/cli/thinking-levels.ts +7 -0
  140. package/src/cli/update-cli.ts +26 -7
  141. package/src/cli-commands.ts +170 -33
  142. package/src/cli.ts +17 -5
  143. package/src/commands/acp.ts +3 -1
  144. package/src/commands/agents.ts +3 -2
  145. package/src/commands/auth-broker.ts +3 -2
  146. package/src/commands/auth-gateway.ts +3 -2
  147. package/src/commands/bench.ts +2 -3
  148. package/src/commands/browser-relay.ts +53 -0
  149. package/src/commands/cleanse.ts +2 -2
  150. package/src/commands/commit.ts +3 -2
  151. package/src/commands/complete.ts +2 -1
  152. package/src/commands/completions.ts +3 -2
  153. package/src/commands/config.ts +3 -2
  154. package/src/commands/dry-balance.ts +2 -2
  155. package/src/commands/gallery.ts +3 -2
  156. package/src/commands/gc.ts +3 -2
  157. package/src/commands/grep.ts +3 -2
  158. package/src/commands/grievances.ts +3 -2
  159. package/src/commands/install.ts +2 -2
  160. package/src/commands/join.ts +3 -2
  161. package/src/commands/launch-help.ts +116 -0
  162. package/src/commands/launch.ts +7 -191
  163. package/src/commands/models.ts +3 -2
  164. package/src/commands/plugin.ts +3 -2
  165. package/src/commands/read.ts +3 -2
  166. package/src/commands/say.ts +3 -2
  167. package/src/commands/setup.ts +3 -2
  168. package/src/commands/shell.ts +3 -2
  169. package/src/commands/ssh.ts +3 -2
  170. package/src/commands/stats.ts +3 -2
  171. package/src/commands/tiny-models.ts +2 -2
  172. package/src/commands/token.ts +2 -2
  173. package/src/commands/ttsr.ts +2 -2
  174. package/src/commands/update.ts +3 -2
  175. package/src/commands/usage.ts +3 -2
  176. package/src/commands/web-search.ts +3 -2
  177. package/src/commands/worktree.ts +3 -2
  178. package/src/config/keybindings.ts +5 -0
  179. package/src/config/model-discovery.ts +44 -27
  180. package/src/config/model-registry.ts +19 -13
  181. package/src/config/models-config-schema-bundle.ts +9 -0
  182. package/src/config/resolve-config-value.ts +2 -1
  183. package/src/config/service-tier.ts +5 -0
  184. package/src/config/settings-schema.ts +72 -20
  185. package/src/config/settings.ts +1 -1
  186. package/src/cursor-bridge-tools.ts +1 -1
  187. package/src/cursor.ts +10 -14
  188. package/src/edit/diff.ts +17 -8
  189. package/src/edit/index.ts +20 -7
  190. package/src/edit/modes/replace.ts +93 -40
  191. package/src/edit/renderer.ts +1 -1
  192. package/src/edit/streaming.ts +14 -17
  193. package/src/eval/py/kernel.ts +2 -1
  194. package/src/eval/py/spawn-options.ts +24 -29
  195. package/src/export/html/index.ts +10 -3
  196. package/src/export/html/template.js +8 -26
  197. package/src/export/share.ts +4 -0
  198. package/src/extensibility/extensions/wrapper.ts +1 -0
  199. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  200. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  201. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  202. package/src/extensibility/skills.ts +11 -0
  203. package/src/extensibility/utils.ts +10 -2
  204. package/src/launch/broker.ts +223 -7
  205. package/src/launch/client.ts +192 -21
  206. package/src/launch/ensure.ts +73 -0
  207. package/src/launch/paths.ts +1 -1
  208. package/src/launch/protocol.ts +54 -2
  209. package/src/lsp/client.ts +23 -2
  210. package/src/lsp/index.ts +8 -0
  211. package/src/lsp/mux/daemon.ts +348 -0
  212. package/src/lsp/mux/protocol.ts +96 -0
  213. package/src/lsp/mux/server.ts +797 -0
  214. package/src/lsp/types.ts +28 -2
  215. package/src/main.ts +20 -8
  216. package/src/mcp/config-writer.ts +1 -1
  217. package/src/mcp/oauth-flow.ts +11 -1
  218. package/src/mnemopi/embed-client.ts +49 -2
  219. package/src/mnemopi/state.ts +57 -23
  220. package/src/modes/components/assistant-message.ts +11 -0
  221. package/src/modes/components/btw-panel.ts +41 -4
  222. package/src/modes/components/chat-transcript-builder.ts +4 -0
  223. package/src/modes/components/custom-editor.ts +9 -0
  224. package/src/modes/components/read-tool-group.ts +10 -0
  225. package/src/modes/components/status-line/component.ts +1 -1
  226. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  227. package/src/modes/components/tool-execution.ts +8 -0
  228. package/src/modes/components/tree-selector.ts +46 -48
  229. package/src/modes/controllers/btw-controller.ts +55 -7
  230. package/src/modes/controllers/command-controller.ts +31 -0
  231. package/src/modes/controllers/event-controller.ts +17 -0
  232. package/src/modes/controllers/input-controller.ts +63 -8
  233. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  234. package/src/modes/controllers/selector-controller.ts +20 -0
  235. package/src/modes/interactive-mode.ts +18 -2
  236. package/src/modes/types.ts +9 -1
  237. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  238. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  239. package/src/modes/utils/ui-helpers.ts +6 -10
  240. package/src/modes/workflow.ts +8 -2
  241. package/src/prompts/agents/init.md +1 -1
  242. package/src/prompts/session/launch-completion.md +1 -0
  243. package/src/prompts/system/plan-mode-active.md +2 -2
  244. package/src/prompts/system/system-prompt.md +26 -52
  245. package/src/prompts/system/workflow-notice.md +3 -3
  246. package/src/prompts/tools/ast-grep.md +1 -1
  247. package/src/prompts/tools/bash.md +1 -1
  248. package/src/prompts/tools/browser.md +1 -0
  249. package/src/prompts/tools/computer.md +21 -21
  250. package/src/prompts/tools/glob.md +1 -1
  251. package/src/prompts/tools/grep.md +1 -1
  252. package/src/prompts/tools/replace.md +5 -4
  253. package/src/prompts/tools/task.md +4 -4
  254. package/src/sdk.ts +49 -6
  255. package/src/secrets/index.ts +1 -1
  256. package/src/security/contracts/schemas.ts +205 -183
  257. package/src/security/contracts/validation.ts +5 -1
  258. package/src/security/store.ts +2 -2
  259. package/src/session/agent-session-types.ts +9 -1
  260. package/src/session/agent-session.ts +204 -15
  261. package/src/session/indexed-session-storage.ts +9 -0
  262. package/src/session/launch-completion.ts +37 -0
  263. package/src/session/prewalk.ts +24 -2
  264. package/src/session/session-advisors.ts +2 -2
  265. package/src/session/session-context.ts +42 -12
  266. package/src/session/session-dump-format.ts +1 -1
  267. package/src/session/session-entries.ts +17 -1
  268. package/src/session/session-maintenance.ts +38 -4
  269. package/src/session/session-manager.ts +123 -67
  270. package/src/session/session-paths.ts +118 -55
  271. package/src/session/session-stats.ts +48 -3
  272. package/src/session/session-storage.ts +30 -22
  273. package/src/session/stream-guards.ts +1 -1
  274. package/src/session/yield-queue.ts +121 -16
  275. package/src/slash-commands/builtin-registry.ts +37 -24
  276. package/src/system-prompt.ts +8 -3
  277. package/src/task/index.ts +23 -8
  278. package/src/task/spawn-policy.ts +14 -0
  279. package/src/thinking.ts +2 -8
  280. package/src/tools/ast-grep.ts +10 -4
  281. package/src/tools/browser/cmux/cmux-tab.ts +97 -14
  282. package/src/tools/browser/cmux/rpc.ts +2 -8
  283. package/src/tools/browser/registry.ts +53 -2
  284. package/src/tools/browser/relay/bridge.ts +945 -0
  285. package/src/tools/browser/relay/daemon.ts +117 -0
  286. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  287. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  288. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  289. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  290. package/src/tools/browser/relay/kind.ts +41 -0
  291. package/src/tools/browser/relay/protocol.ts +54 -0
  292. package/src/tools/browser/relay/server.ts +141 -0
  293. package/src/tools/browser/render.ts +4 -1
  294. package/src/tools/browser/shared-daemon.ts +7 -58
  295. package/src/tools/browser/tab-supervisor.ts +5 -4
  296. package/src/tools/browser/tab-worker.ts +33 -12
  297. package/src/tools/browser.ts +26 -1
  298. package/src/tools/computer/exposure.ts +11 -35
  299. package/src/tools/computer/protocol.ts +51 -10
  300. package/src/tools/computer/supervisor.ts +227 -124
  301. package/src/tools/computer/worker-entry.ts +11 -6
  302. package/src/tools/computer/worker.ts +695 -81
  303. package/src/tools/computer-renderer.ts +104 -65
  304. package/src/tools/computer.ts +134 -428
  305. package/src/tools/context.ts +3 -0
  306. package/src/tools/glob.ts +9 -2
  307. package/src/tools/grep.ts +12 -6
  308. package/src/tools/hub/launch.ts +122 -6
  309. package/src/tools/index.ts +9 -0
  310. package/src/tools/inspect-image.ts +32 -6
  311. package/src/tools/render-utils.ts +2 -0
  312. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  313. package/src/tools/tool-timeouts.ts +1 -0
  314. package/src/tools/xdev.ts +29 -2
  315. package/src/utils/clipboard.ts +40 -14
  316. package/src/utils/command-args.ts +26 -28
  317. package/src/utils/tool-choice.ts +0 -13
  318. package/src/web/kagi.ts +2 -1
  319. package/src/web/search/index.ts +18 -2
  320. package/src/web/search/providers/anthropic.ts +4 -1
  321. package/src/web/search/providers/base.ts +2 -0
  322. package/src/web/search/providers/brave.ts +3 -1
  323. package/src/web/search/providers/browser-page.ts +6 -4
  324. package/src/web/search/providers/codex.ts +4 -1
  325. package/src/web/search/providers/duckduckgo.ts +2 -1
  326. package/src/web/search/providers/ecosia.ts +2 -1
  327. package/src/web/search/providers/exa.ts +4 -2
  328. package/src/web/search/providers/firecrawl.ts +3 -1
  329. package/src/web/search/providers/gemini.ts +8 -2
  330. package/src/web/search/providers/google.ts +2 -1
  331. package/src/web/search/providers/jina.ts +12 -2
  332. package/src/web/search/providers/kagi.ts +3 -0
  333. package/src/web/search/providers/kimi.ts +5 -1
  334. package/src/web/search/providers/mojeek.ts +2 -1
  335. package/src/web/search/providers/parallel.ts +5 -1
  336. package/src/web/search/providers/perplexity.ts +6 -3
  337. package/src/web/search/providers/public.ts +1 -1
  338. package/src/web/search/providers/searxng.ts +11 -5
  339. package/src/web/search/providers/startpage.ts +10 -4
  340. package/src/web/search/providers/synthetic.ts +11 -5
  341. package/src/web/search/providers/tavily.ts +3 -1
  342. package/src/web/search/providers/tinyfish.ts +3 -1
  343. package/src/web/search/providers/utils.ts +12 -10
  344. package/src/web/search/providers/xai.ts +2 -2
  345. package/src/web/search/providers/zai.ts +18 -3
  346. package/src/web/search/types.ts +6 -0
  347. package/dist/types/config/file-lock.d.ts +0 -29
  348. package/src/config/file-lock.ts +0 -164
package/src/lsp/types.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { ptree } from "@oh-my-pi/pi-utils";
2
1
  import { type } from "arktype";
3
2
  import { TOOL_TIMEOUTS } from "../tools/tool-timeouts";
4
3
 
@@ -370,6 +369,33 @@ export interface ServerConfig {
370
369
  createClient?: LinterClientFactory;
371
370
  }
372
371
 
372
+ // =============================================================================
373
+ // Transport
374
+ // =============================================================================
375
+
376
+ /** Minimal write sink for the server-bound byte stream (satisfied by `Bun.FileSink` and the mux socket adapter). */
377
+ export interface LspWriteSink {
378
+ write(data: string | Uint8Array): number | Promise<number>;
379
+ flush(): number | void | Promise<number | void>;
380
+ }
381
+
382
+ /**
383
+ * Byte transport carrying one LSP JSON-RPC link. Structurally satisfied by
384
+ * `ptree.ChildProcess<"pipe">` (local server spawn) and by the socket adapter
385
+ * in `mux/daemon.ts` (broker-shared server). `exited` may reject (ptree kill).
386
+ */
387
+ export interface LspTransport {
388
+ readonly stdin: LspWriteSink;
389
+ readonly stdout: ReadableStream<Uint8Array>;
390
+ readonly exited: Promise<number>;
391
+ readonly exitCode: number | null;
392
+ readonly pid?: number;
393
+ /** Present and true on broker-shared mux links; `lsp reload` uses it to request a shared-server restart. */
394
+ readonly sharedMux?: boolean;
395
+ kill(): void;
396
+ peekStderr(): string;
397
+ }
398
+
373
399
  // =============================================================================
374
400
  // Client State
375
401
  // =============================================================================
@@ -401,7 +427,7 @@ export interface LspClient {
401
427
  name: string;
402
428
  cwd: string;
403
429
  config: ServerConfig;
404
- proc: ptree.ChildProcess<"pipe">;
430
+ proc: LspTransport;
405
431
  requestId: number;
406
432
  diagnostics: Map<string, PublishedDiagnostics>;
407
433
  diagnosticsVersion: number;
package/src/main.ts CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  type ScopedModel,
42
42
  } from "./config/model-resolver";
43
43
  import { ModelsConfigFile } from "./config/models-config";
44
+ import { serviceTierSettingToTier } from "./config/service-tier";
44
45
  import { getDefault, type SettingPath, Settings, type SettingValue, settings } from "./config/settings";
45
46
  import { initializeWithSettings } from "./discovery";
46
47
  import {
@@ -188,8 +189,9 @@ function applyAcpDefaultSettingOverrides(targetSettings: Settings = settings): v
188
189
  applyDefaultSettingOverrides(HOST_DEFAULTED_SETTING_PATHS, targetSettings);
189
190
  }
190
191
 
191
- async function readPipedInput(): Promise<string | undefined> {
192
- if (process.stdin.isTTY !== false) return undefined;
192
+ /** Reads a non-TTY stdin stream as prompt text. */
193
+ export async function readPipedInput(): Promise<string | undefined> {
194
+ if (process.stdin.isTTY === true) return undefined;
193
195
  // stdin is a pipe: a producer that never writes nor closes would block
194
196
  // startup forever with zero output. Say what we're blocked on after 1s.
195
197
  const notice = setTimeout(() => {
@@ -862,6 +864,9 @@ export async function buildSessionOptions(
862
864
  autoApprove: parsed.autoApprove ?? false,
863
865
  };
864
866
  const restoringSession = Boolean(parsed.continue || parsed.resume || isForeignSessionImport(parsed));
867
+ if (parsed.serviceTier !== undefined) {
868
+ options.openAIServiceTier = serviceTierSettingToTier(parsed.serviceTier) ?? null;
869
+ }
865
870
  const cliDirs = parsed.addDir ?? [];
866
871
  const settingsDirs = activeSettings.get("workspace.additionalDirectories");
867
872
  if (cliDirs.length > 0 || settingsDirs.length > 0) {
@@ -1592,6 +1597,18 @@ export async function runRootCommand(
1592
1597
  stdoutIsTTY: process.stdout.isTTY,
1593
1598
  });
1594
1599
 
1600
+ // Startup changelog is only consumed by interactive mode below; kick the
1601
+ // CHANGELOG.md parse off now so it overlaps session creation instead of
1602
+ // serializing after it.
1603
+ const startupChangelogPromise = isInteractive
1604
+ ? logger.time(
1605
+ "main:getChangelogForDisplay",
1606
+ getChangelogForDisplay,
1607
+ parsedArgs,
1608
+ settingsInstance.get("startup.changelogMode"),
1609
+ )
1610
+ : undefined;
1611
+
1595
1612
  const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager } = await createSession({
1596
1613
  ...sessionOptions,
1597
1614
  eventBus,
@@ -1651,12 +1668,7 @@ export async function runRootCommand(
1651
1668
  await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined, eventBus, rpcInput);
1652
1669
  } else if (isInteractive) {
1653
1670
  const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
1654
- const startupChangelog = await logger.time(
1655
- "main:getChangelogForDisplay",
1656
- getChangelogForDisplay,
1657
- parsedArgs,
1658
- settingsInstance.get("startup.changelogMode"),
1659
- );
1671
+ const startupChangelog = await startupChangelogPromise;
1660
1672
 
1661
1673
  const modelScopeNotification = buildModelScopeNotification(
1662
1674
  scopedModels,
@@ -7,8 +7,8 @@ import { randomUUID } from "node:crypto";
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { isEnoent } from "@oh-my-pi/pi-utils";
10
+ import { withFileLock } from "@oh-my-pi/pi-utils/file-lock";
10
11
  import { invalidate as invalidateFsCache } from "../capability/fs";
11
- import { withFileLock } from "../config/file-lock";
12
12
 
13
13
  import { validateServerConfig } from "./config";
14
14
  import { MCP_CONFIG_SCHEMA_URL, type MCPConfigFile, type MCPServerConfig } from "./types";
@@ -489,12 +489,22 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
489
489
  }
490
490
 
491
491
  const data = (await response.json()) as {
492
- access_token: string;
492
+ access_token?: string;
493
493
  refresh_token?: string;
494
494
  expires_in?: number;
495
495
  token_type?: string;
496
+ error?: string;
497
+ error_description?: string;
496
498
  };
497
499
 
500
+ // Some providers (e.g. the Slack Web API) signal failure with HTTP 200 and
501
+ // an `{ ok: false, error }` body. Accepting such a response would store an
502
+ // empty access token and only surface `invalid_token` on a later request.
503
+ if (typeof data.access_token !== "string" || data.access_token.length === 0) {
504
+ const providerError = data.error_description ?? data.error;
505
+ throw new Error(`Token exchange returned no access token${providerError ? `: ${providerError}` : ""}`);
506
+ }
507
+
498
508
  // Calculate expiry timestamp
499
509
  const expiresIn = data.expires_in ?? 3600; // Default to 1 hour
500
510
  const expires = Date.now() + expiresIn * 1000;
@@ -84,6 +84,22 @@ export interface MnemopiSubprocessEmbeddingModel {
84
84
  embed(texts: string[], batchSize?: number): AsyncIterable<number[][]>;
85
85
  }
86
86
 
87
+ /**
88
+ * Upper bound on a steady-state embed IPC round-trip. Initialization is
89
+ * intentionally exempt: bundled installs may spend several minutes installing
90
+ * fastembed and bootstrapping the model, and killing that worker can strand the
91
+ * runtime install lock. Once initialization succeeds, a longer embed stall
92
+ * means a hung native runtime (issue #4792) that would otherwise pin whatever
93
+ * awaits the embed — a turn's memory recall or the headless shutdown
94
+ * consolidation — indefinitely, leaving the process alive with an unreaped
95
+ * `__omp_worker_mnemopi_embed` child (issue #7352). On expiry the embed fails
96
+ * and the worker is SIGKILL-reaped so the next request respawns a fresh one.
97
+ */
98
+ const EMBED_REQUEST_TIMEOUT_MS = 120_000;
99
+
100
+ /** Race marker for {@link MnemopiEmbedClient.#awaitRequest}. */
101
+ const REQUEST_TIMED_OUT = Symbol("mnemopi.embed.timedOut");
102
+
87
103
  export class MnemopiEmbedClient {
88
104
  #worker: MnemopiEmbedWorkerHandle | null = null;
89
105
  #unsubscribeMessage: (() => void) | null = null;
@@ -91,9 +107,14 @@ export class MnemopiEmbedClient {
91
107
  #pending = new Map<string, PendingRequest>();
92
108
  #nextRequestId = 0;
93
109
  #spawnWorker: () => MnemopiEmbedWorkerHandle;
110
+ #requestTimeoutMs: number;
94
111
 
95
- constructor(spawnWorker: () => MnemopiEmbedWorkerHandle = spawnMnemopiEmbedWorker) {
112
+ constructor(
113
+ spawnWorker: () => MnemopiEmbedWorkerHandle = spawnMnemopiEmbedWorker,
114
+ requestTimeoutMs: number = EMBED_REQUEST_TIMEOUT_MS,
115
+ ) {
96
116
  this.#spawnWorker = spawnWorker;
117
+ this.#requestTimeoutMs = requestTimeoutMs;
97
118
  }
98
119
 
99
120
  /**
@@ -166,7 +187,7 @@ export class MnemopiEmbedClient {
166
187
  // worker's "embed before init" guard. Worker `ensureLoaded` is
167
188
  // idempotent so steady-state embeds pay no extra cost.
168
189
  worker.send({ type: "embed", id, model, cacheDir, texts, batchSize });
169
- const result = await promise;
190
+ const result = await this.#awaitRequest(promise);
170
191
  if (result instanceof Error) throw result;
171
192
  return result;
172
193
  } finally {
@@ -174,6 +195,32 @@ export class MnemopiEmbedClient {
174
195
  }
175
196
  }
176
197
 
198
+ /**
199
+ * Await one steady-state embed reply, bounded by
200
+ * {@link EMBED_REQUEST_TIMEOUT_MS}. The timeout timer is `unref`'d so a
201
+ * pending request never keeps the parent event loop alive on its own (the
202
+ * awaiting caller does). On expiry the wedged worker is SIGKILL-reaped via
203
+ * {@link terminate} — faulting any other in-flight request and letting the
204
+ * next call respawn a fresh child — before the request rejects, so a hung
205
+ * native runtime cannot pin a turn's recall or shutdown consolidation
206
+ * forever (issue #7352).
207
+ */
208
+ async #awaitRequest<T>(promise: Promise<T>): Promise<T> {
209
+ const { promise: timedOut, resolve: fire } = Promise.withResolvers<typeof REQUEST_TIMED_OUT>();
210
+ const timer = setTimeout(() => fire(REQUEST_TIMED_OUT), this.#requestTimeoutMs);
211
+ timer.unref();
212
+ try {
213
+ const winner = await Promise.race([promise, timedOut]);
214
+ if (winner === REQUEST_TIMED_OUT) {
215
+ void this.terminate();
216
+ throw new Error("mnemopi embed worker request timed out");
217
+ }
218
+ return winner;
219
+ } finally {
220
+ clearTimeout(timer);
221
+ }
222
+ }
223
+
177
224
  async *#streamEmbed(
178
225
  model: MnemopiEmbedModelId,
179
226
  cacheDir: string | undefined,
@@ -4,7 +4,7 @@ import type * as MnemopiNs from "@oh-my-pi/pi-mnemopi";
4
4
  import type { Mnemopi, RecallResult } from "@oh-my-pi/pi-mnemopi";
5
5
  import type * as MnemopiCoreNs from "@oh-my-pi/pi-mnemopi/core";
6
6
  import type { LocalModelInitializer } from "@oh-my-pi/pi-mnemopi/core";
7
- import { logger } from "@oh-my-pi/pi-utils";
7
+ import { logger, toError } from "@oh-my-pi/pi-utils";
8
8
  import {
9
9
  composeRecallQuery,
10
10
  formatCurrentTime,
@@ -386,6 +386,8 @@ export class MnemopiSessionState {
386
386
  const merged: RecallResult[] = [];
387
387
  const byId = new Map<string, number>();
388
388
  const byContent = new Map<string, number>();
389
+ const failures: Array<{ bank: string; error: Error }> = [];
390
+ let successfulTargets = 0;
389
391
  const sharedFallbackQuery = deriveSharedRecallFallbackQuery(
390
392
  query,
391
393
  this.scoped.retain.bank,
@@ -394,24 +396,35 @@ export class MnemopiSessionState {
394
396
  for (const target of this.scoped.recall) {
395
397
  const queries =
396
398
  target.bank === this.scoped.global?.bank && sharedFallbackQuery ? [query, sharedFallbackQuery] : [query];
399
+ let targetSucceeded = false;
397
400
  try {
398
401
  for (const recallQuery of queries) {
399
402
  const results = await target.memory.recallEnhanced(recallQuery, this.config.recallLimit, {
400
403
  includeFacts: true,
401
404
  channelId: target.bank,
402
405
  });
406
+ targetSucceeded = true;
403
407
  for (const result of results) {
404
408
  mergeRecallResult(merged, byId, byContent, result);
405
409
  }
406
410
  }
407
411
  } catch (error) {
408
- if (this.config.debug) {
409
- logger.debug("Mnemopi: scoped recall target failed", {
410
- bank: target.bank,
411
- error: String(error),
412
- });
413
- }
412
+ const failure = toError(error);
413
+ failures.push({ bank: target.bank, error: failure });
414
+ logger.warn("Mnemopi: scoped recall target failed", {
415
+ bank: target.bank,
416
+ error: failure.message,
417
+ });
414
418
  }
419
+ if (targetSucceeded) successfulTargets++;
420
+ }
421
+ if (successfulTargets === 0 && failures.length > 0) {
422
+ if (failures.length === 1) throw failures[0].error;
423
+ const details = failures.map(({ bank, error }) => `${bank}: ${error.message}`).join("; ");
424
+ throw new AggregateError(
425
+ failures.map(({ error }) => error),
426
+ `Mnemopi recall failed for all scoped targets (${details})`,
427
+ );
415
428
  }
416
429
  merged.sort(compareRecallResults);
417
430
  if (merged.length > this.config.recallLimit) merged.length = this.config.recallLimit;
@@ -575,7 +588,16 @@ export class MnemopiSessionState {
575
588
  if (!lastUser) return;
576
589
  const query = composeRecallQuery(lastUser.content, messages, this.config.recallContextTurns);
577
590
  const truncated = truncateRecallQuery(query, lastUser.content, this.config.recallMaxQueryChars);
578
- const context = await this.recallForContext(truncated);
591
+ let context: string | undefined;
592
+ try {
593
+ context = await this.recallForContext(truncated);
594
+ } catch (error) {
595
+ logger.warn("Mnemopi: auto-recall failed", {
596
+ bank: this.config.bank,
597
+ error: toError(error).message,
598
+ });
599
+ return;
600
+ }
579
601
  this.hasRecalledForFirstTurn = true;
580
602
  if (!context) return;
581
603
  this.lastRecallSnippet = context;
@@ -639,17 +661,26 @@ export class MnemopiSessionState {
639
661
  * tokens on memories that will be wiped on the next line is wasted work
640
662
  * (PR #2327 review).
641
663
  *
642
- * `timeoutMs` caps how long the consolidate await blocks the caller
643
- * (the user-visible `/quit` / `/exit` shutdown path passes this so
644
- * dispose returns within a UX budget issue #3641). When the cap is
645
- * hit, dispose returns immediately and detaches the still-in-flight
646
- * consolidate; the SQLite handles are closed in the background once
647
- * the consolidate settles so writes never race a closed handle, and
648
- * any pending embeddings are SIGKILL'd along with the embed worker
664
+ * `timeoutMs` caps both synchronous SQLite lock waits during final retention
665
+ * and the asynchronous consolidation drain (the user-visible `/quit`,
666
+ * `/exit`, and print paths pass this so disposal stays within their shutdown
667
+ * budget). When the cap is hit, dispose returns immediately and detaches the
668
+ * still-in-flight consolidate; the SQLite handles are closed in the
669
+ * background once the consolidate settles so writes never race a closed handle,
670
+ * and any pending embeddings are SIGKILL'd along with the embed worker
649
671
  * (a tolerable loss — working memory rows are durable; only the
650
672
  * episodic promotion / embedding for the LAST few turns is skipped,
651
673
  * and `maybeRetainOnAgentEnd` has already retained earlier turns).
652
674
  */
675
+ #boundOwnedBusyTimeout(timeoutMs: number): void {
676
+ // SQLite lock waits block the JS thread, so a Promise race cannot interrupt
677
+ // them. consolidate() flushes every owned bank, so bound each one — not just
678
+ // the retain bank — or a locked shared bank (per-project-tagged) still stalls
679
+ // teardown for Mnemopi's default 5s busy timeout (#7351 review).
680
+ const busyTimeoutMs = Math.max(1, Math.floor(timeoutMs));
681
+ for (const memory of this.scoped.owned) memory.beam.db.exec(`PRAGMA busy_timeout=${busyTimeoutMs}`);
682
+ }
683
+
653
684
  async dispose(options: { consolidate?: boolean; timeoutMs?: number } = {}): Promise<void> {
654
685
  this.unsubscribe?.();
655
686
  this.unsubscribe = undefined;
@@ -661,19 +692,22 @@ export class MnemopiSessionState {
661
692
  closeOwned();
662
693
  return;
663
694
  }
695
+ const { timeoutMs } = options;
696
+ const boundedTimeoutMs = timeoutMs !== undefined && timeoutMs > 0 ? timeoutMs : undefined;
697
+ const deadline = boundedTimeoutMs !== undefined ? performance.now() + boundedTimeoutMs : undefined;
698
+ if (boundedTimeoutMs !== undefined) this.#boundOwnedBusyTimeout(boundedTimeoutMs);
664
699
  const consolidatePromise = this.consolidate({ full: false, extract: false, sleep: false }).catch(
665
700
  (error: unknown) => {
666
701
  logger.warn("Mnemopi: consolidation on dispose failed.", { error: String(error) });
667
702
  },
668
703
  );
669
- const { timeoutMs } = options;
670
- if (timeoutMs !== undefined && timeoutMs > 0) {
671
- const TIMED_OUT = Symbol("mnemopi.dispose.timedOut");
672
- const winner = await Promise.race([
673
- consolidatePromise.then(() => undefined as unknown),
674
- Bun.sleep(timeoutMs).then(() => TIMED_OUT as unknown),
675
- ]);
676
- if (winner === TIMED_OUT) {
704
+ if (deadline !== undefined) {
705
+ const remainingMs = deadline - performance.now();
706
+ const completed =
707
+ remainingMs > 0
708
+ ? await Promise.race([consolidatePromise.then(() => true), Bun.sleep(remainingMs).then(() => false)])
709
+ : false;
710
+ if (!completed) {
677
711
  logger.warn("Mnemopi: consolidate-on-dispose exceeded shutdown budget; detaching to background.", {
678
712
  timeoutMs,
679
713
  });
@@ -185,6 +185,7 @@ export class AssistantMessageComponent extends Container {
185
185
  #toolImagesByCallId = new Map<string, ImageContent[]>();
186
186
  #convertedKittyImages = new Map<string, ImageContent>();
187
187
  #showImages = true;
188
+ #showToolResultImages = true;
188
189
  #kittyConversionsInFlight = new Set<string>();
189
190
  #transcriptBlockFinalized: boolean;
190
191
  /**
@@ -578,6 +579,15 @@ export class AssistantMessageComponent extends Container {
578
579
  }
579
580
  }
580
581
 
582
+ /** Toggle only images produced by tool results; assistant-native images remain governed by setImagesVisible. */
583
+ setToolResultImagesVisible(visible: boolean): void {
584
+ if (this.#showToolResultImages === visible) return;
585
+ this.#showToolResultImages = visible;
586
+ if (this.#lastMessage) {
587
+ this.updateContent(this.#lastMessage, { transient: this.#lastUpdateTransient });
588
+ }
589
+ }
590
+
581
591
  setToolResultImages(toolCallId: string, images: ImageContent[]): void {
582
592
  if (!toolCallId) return;
583
593
  const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
@@ -649,6 +659,7 @@ export class AssistantMessageComponent extends Container {
649
659
  }
650
660
 
651
661
  #renderToolImages(): void {
662
+ if (!this.#showToolResultImages) return;
652
663
  const entries = Array.from(this.#toolImagesByCallId.entries()).flatMap(([toolCallId, images]) =>
653
664
  images.map((image, index) => ({ image, key: `${toolCallId}:${index}` })),
654
665
  );
@@ -3,16 +3,37 @@ import { replaceTabs } from "../../tools/render-utils";
3
3
  import { getMarkdownTheme, theme } from "../theme/theme";
4
4
  import { DynamicBorder } from "./dynamic-border";
5
5
 
6
- type BtwPanelState = "running" | "complete" | "aborted" | "error";
6
+ type BtwPanelState = "running" | "complete" | "branching" | "aborted" | "error";
7
7
 
8
8
  interface BtwPanelComponentOptions {
9
9
  question: string;
10
10
  tui: TUI;
11
+ canBranch?: () => boolean;
12
+ }
13
+
14
+ class BtwFooter implements Component {
15
+ #getLine: () => string;
16
+ #line: string | undefined;
17
+ #text: Text | undefined;
18
+
19
+ constructor(getLine: () => string) {
20
+ this.#getLine = getLine;
21
+ }
22
+
23
+ render(width: number): readonly string[] {
24
+ const line = this.#getLine();
25
+ if (line !== this.#line || !this.#text) {
26
+ this.#line = line;
27
+ this.#text = new Text(line, 1, 0);
28
+ }
29
+ return this.#text.render(width);
30
+ }
11
31
  }
12
32
 
13
33
  export class BtwPanelComponent extends Container {
14
34
  #question: string;
15
35
  #tui: TUI;
36
+ #canBranch: (() => boolean) | undefined;
16
37
  #state: BtwPanelState = "running";
17
38
  #answer = "";
18
39
  #errorMessage: string | undefined;
@@ -23,6 +44,7 @@ export class BtwPanelComponent extends Container {
23
44
  super();
24
45
  this.#question = options.question;
25
46
  this.#tui = options.tui;
47
+ this.#canBranch = options.canBranch;
26
48
  this.#rebuild();
27
49
  }
28
50
 
@@ -47,6 +69,14 @@ export class BtwPanelComponent extends Container {
47
69
  this.#rebuild();
48
70
  }
49
71
 
72
+ /** Shows that the completed answer is being promoted into the chat session. */
73
+ markBranching(): void {
74
+ if (this.#closed) return;
75
+ this.#state = "branching";
76
+ this.#errorMessage = undefined;
77
+ this.#rebuild();
78
+ }
79
+
50
80
  markAborted(): void {
51
81
  if (this.#closed) return;
52
82
  this.#state = "aborted";
@@ -86,7 +116,7 @@ export class BtwPanelComponent extends Container {
86
116
  this.addChild(new Spacer(1));
87
117
  this.addChild(this.#contentComponent());
88
118
  this.addChild(new Spacer(1));
89
- this.addChild(new Text(this.#footerLine(), 1, 0));
119
+ this.addChild(new BtwFooter(() => this.#footerLine()));
90
120
  this.addChild(new Spacer(1));
91
121
  this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
92
122
  // Component-scoped: a rebuild replaces only this panel's own children
@@ -100,8 +130,15 @@ export class BtwPanelComponent extends Container {
100
130
  switch (this.#state) {
101
131
  case "running":
102
132
  return theme.fg("muted", "Esc cancel /btw");
103
- case "complete":
104
- return theme.fg("muted", this.isCopyable() ? "c copy · b branch to chat · Esc dismiss" : "Esc dismiss");
133
+ case "complete": {
134
+ if (!this.isCopyable()) return theme.fg("muted", "Esc dismiss");
135
+ const actions = ["c copy"];
136
+ if (this.#canBranch?.() ?? this.isBranchable()) actions.push("b branch to chat");
137
+ actions.push("Esc dismiss");
138
+ return theme.fg("muted", actions.join(" · "));
139
+ }
140
+ case "branching":
141
+ return theme.fg("muted", `${theme.status.pending} Branching to chat…`);
105
142
  case "aborted":
106
143
  return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
107
144
  case "error":
@@ -190,6 +190,7 @@ export class ChatTranscriptBuilder {
190
190
  this.#readGroup = new ReadToolGroupComponent({
191
191
  showContentPreview: settings.get("read.toolResultPreview"),
192
192
  });
193
+ this.#readGroup.setToolActivityVisible(!settings.get("display.hideToolActivity"));
193
194
  this.#trackExpandable(this.#readGroup);
194
195
  this.container.addChild(this.#readGroup);
195
196
  }
@@ -321,6 +322,7 @@ export class ChatTranscriptBuilder {
321
322
  proseOnlyThinking,
322
323
  );
323
324
  assistantComponent.setImagesVisible(settings.get("terminal.showImages"));
325
+ assistantComponent.setToolResultImagesVisible(!settings.get("display.hideToolActivity"));
324
326
  this.#trackExpandable(assistantComponent);
325
327
  this.container.addChild(assistantComponent);
326
328
 
@@ -353,6 +355,7 @@ export class ChatTranscriptBuilder {
353
355
  proseOnlyThinking,
354
356
  );
355
357
  component.setImagesVisible(settings.get("terminal.showImages"));
358
+ component.setToolResultImagesVisible(!settings.get("display.hideToolActivity"));
356
359
  this.#trackExpandable(component);
357
360
  this.container.addChild(component);
358
361
  };
@@ -401,6 +404,7 @@ export class ChatTranscriptBuilder {
401
404
  this.deps.cwd,
402
405
  content.id,
403
406
  );
407
+ component.setToolActivityVisible(!settings.get("display.hideToolActivity"));
404
408
  this.#trackExpandable(component);
405
409
  this.container.addChild(component);
406
410
 
@@ -31,6 +31,7 @@ type ConfigurableEditorAction = Extract<
31
31
  | "app.model.select"
32
32
  | "app.model.selectTemporary"
33
33
  | "app.tools.expand"
34
+ | "app.tools.toggleVisibility"
34
35
  | "app.thinking.toggle"
35
36
  | "app.editor.external"
36
37
  | "app.history.search"
@@ -53,6 +54,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
53
54
  "app.model.select": ["alt+m"],
54
55
  "app.model.selectTemporary": ["alt+p"],
55
56
  "app.tools.expand": ["ctrl+o"],
57
+ "app.tools.toggleVisibility": ["ctrl+shift+o"],
56
58
  "app.thinking.toggle": ["ctrl+t"],
57
59
  "app.editor.external": ["ctrl+g"],
58
60
  "app.history.search": ["ctrl+r"],
@@ -550,6 +552,7 @@ export class CustomEditor extends Editor {
550
552
  onCycleModelBackward?: () => void;
551
553
  onSelectModel?: () => void;
552
554
  onExpandTools?: () => void;
555
+ onToggleToolActivity?: () => void;
553
556
  onToggleThinking?: () => void;
554
557
  onExternalEditor?: () => void;
555
558
  onHistorySearch?: () => void;
@@ -910,6 +913,12 @@ export class CustomEditor extends Editor {
910
913
  return;
911
914
  }
912
915
 
916
+ // Intercept configured tool activity visibility toggle
917
+ if (this.#matchesAction(canonical, "app.tools.toggleVisibility") && this.onToggleToolActivity) {
918
+ this.onToggleToolActivity();
919
+ return;
920
+ }
921
+
913
922
  // Intercept configured backward model cycling (check before forward cycling)
914
923
  if (this.#matchesAction(canonical, "app.model.cycleBackward") && this.onCycleModelBackward) {
915
924
  this.onCycleModelBackward();
@@ -332,6 +332,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
332
332
  #usageBatchByToolCallId = new Map<string, string>();
333
333
  #text: Text;
334
334
  #expanded = false;
335
+ #toolActivityVisible = true;
335
336
  #showContentPreview: boolean;
336
337
  // A read group accretes entries across multiple assistant completions for as
337
338
  // long as the run of reads is uninterrupted. While it is the active group it
@@ -353,6 +354,10 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
353
354
  this.#updateDisplay();
354
355
  }
355
356
 
357
+ override render(width: number): readonly string[] {
358
+ if (!this.#toolActivityVisible) return [];
359
+ return super.render(width);
360
+ }
356
361
  isTranscriptBlockFinalized(): boolean {
357
362
  if (this.#sealed) return true;
358
363
  if (!this.#finalized) return false;
@@ -496,6 +501,11 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
496
501
  this.#updateDisplay();
497
502
  }
498
503
 
504
+ setToolActivityVisible(visible: boolean): void {
505
+ this.#toolActivityVisible = visible;
506
+ super.invalidate();
507
+ }
508
+
499
509
  getComponent(): Component {
500
510
  return this;
501
511
  }
@@ -1785,7 +1785,7 @@ export class StatusLineComponent implements Component {
1785
1785
  : separatorDef.endCaps.left
1786
1786
  : "";
1787
1787
  const capPrefix = separatorDef.endCaps?.useBgAsFg ? bgAnsi.replace("\x1b[48;", "\x1b[38;") : bgAnsi + sepAnsi;
1788
- const capText = cap ? `${capPrefix}${cap}\x1b[0m` : "";
1788
+ const capText = cap ? `${capPrefix}${this.#focusedAgentId ? "\x1b[22m" : ""}${cap}\x1b[0m` : "";
1789
1789
 
1790
1790
  let content = bgAnsi + fgAnsi;
1791
1791
  content += ` ${parts.join(` ${sepAnsi}${sep}${fgAnsi} `)} `;
@@ -0,0 +1,35 @@
1
+ import { Text } from "@oh-my-pi/pi-tui";
2
+ import { theme } from "../../modes/theme/theme";
3
+
4
+ /**
5
+ * Dim transcript marker for tool calls stripped from the resolved branch
6
+ * (failed/retried turns, results on sibling branches). It is tool activity,
7
+ * so it hides and reappears with the `display.hideToolActivity` toggle.
8
+ */
9
+ export class StrippedToolCallsPlaceholder extends Text {
10
+ #toolActivityVisible: boolean;
11
+
12
+ constructor(strippedToolCalls: number, toolActivityVisible: boolean) {
13
+ super(
14
+ theme.fg(
15
+ "dim",
16
+ theme.italic(
17
+ `${strippedToolCalls} tool call${strippedToolCalls === 1 ? "" : "s"} elided — no result on this branch`,
18
+ ),
19
+ ),
20
+ 1,
21
+ 0,
22
+ );
23
+ this.#toolActivityVisible = toolActivityVisible;
24
+ }
25
+
26
+ setToolActivityVisible(visible: boolean): void {
27
+ this.#toolActivityVisible = visible;
28
+ this.invalidate();
29
+ }
30
+
31
+ override render(width: number): readonly string[] {
32
+ if (!this.#toolActivityVisible) return [];
33
+ return super.render(width);
34
+ }
35
+ }
@@ -246,6 +246,7 @@ export interface ToolExecutionHandle extends Component {
246
246
  ): void;
247
247
  setArgsComplete(toolCallId?: string): void;
248
248
  setExpanded(expanded: boolean): void;
249
+ setToolActivityVisible(visible: boolean): void;
249
250
  /** Freeze the block as final history: stop spinners and let it commit to scrollback. */
250
251
  seal(): void;
251
252
  }
@@ -287,6 +288,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
287
288
  #toolLabel: string;
288
289
  #args: any;
289
290
  #expanded = false;
291
+ #toolActivityVisible = true;
290
292
  #showImages: boolean;
291
293
  #editFuzzyThreshold: number | undefined;
292
294
  #editAllowFuzzy: boolean | undefined;
@@ -901,6 +903,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
901
903
  this.#updateDisplay();
902
904
  }
903
905
 
906
+ setToolActivityVisible(visible: boolean): void {
907
+ this.#toolActivityVisible = visible;
908
+ super.invalidate();
909
+ }
910
+
904
911
  setShowImages(show: boolean): void {
905
912
  this.#showImages = show;
906
913
  this.#updateDisplay();
@@ -960,6 +967,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
960
967
  }
961
968
 
962
969
  override render(width: number): readonly string[] {
970
+ if (!this.#toolActivityVisible) return [];
963
971
  const lines = super.render(width);
964
972
  // Update the paint-tracking flags after `super.render(width)` — the
965
973
  // override runs on every compose the parent Container performs, so a