@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7

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 (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. package/src/vibe/__tests__/token-rate.test.ts +0 -96
package/src/tools/bash.ts CHANGED
@@ -37,6 +37,7 @@ import { invalidateGithubCacheForBashCommand } from "./gh-cache-invalidation";
37
37
  import {
38
38
  formatStyledTruncationWarning,
39
39
  type OutputMeta,
40
+ resolveInlineByteCapBudget,
40
41
  stripOutputNotice,
41
42
  stripRawOutputArtifactNotice,
42
43
  } from "./output-meta";
@@ -655,6 +656,18 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
655
656
  details.exitCode = exitCode;
656
657
  }
657
658
 
659
+ // Final-defense inline cap config, shared by the timeout and normal
660
+ // completion paths. The sink already bounds inline bodies to the spill
661
+ // threshold, so with the notice slack this only fires on paths that
662
+ // bypass the sink (client-bridge terminals, minimizer misses). When the
663
+ // sink spilled, its artifact already holds the full raw stream — reuse
664
+ // that id instead of saving a second (already-truncated) copy, so the
665
+ // `[raw output: artifact://N]` footer and the truncation notice agree.
666
+ const inlineCap = {
667
+ maxBytes: resolveInlineByteCapBudget(this.session.settings),
668
+ saveArtifact: (full: string) => result.artifactId ?? saveBashOriginalArtifact(this.session, full),
669
+ };
670
+
658
671
  if (isTimeout) {
659
672
  details.timedOut = true;
660
673
  const message =
@@ -664,9 +677,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
664
677
  if (!normalizeResultOutput(result).startsWith(`[${message}]\n`)) {
665
678
  outputLines.push("", `[${message}]`);
666
679
  }
667
- const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"), {
668
- saveArtifact: full => saveBashOriginalArtifact(this.session, full),
669
- });
680
+ const timeoutOutputText = await enforceInlineByteCap(outputLines.join("\n"), inlineCap);
670
681
  return toolResult(details)
671
682
  .text(timeoutOutputText)
672
683
  .truncationFromSummary(result, { direction: "tail" })
@@ -677,12 +688,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
677
688
  // Non-timeout cancellations and missing exit status still propagate as thrown errors.
678
689
  this.#throwIfUnfinished(result, timeoutSec, outputText);
679
690
 
680
- // Final defense at the tool-result boundary: no bash path (client bridge,
681
- // head-retention spill, minimizer miss) may emit more than
682
- // ~DEFAULT_MAX_BYTES inline. No-op for already-bounded output.
683
- const cappedOutputText = await enforceInlineByteCap(outputText, {
684
- saveArtifact: full => saveBashOriginalArtifact(this.session, full),
685
- });
691
+ // No-op for already-bounded output; see `inlineCap` above.
692
+ const cappedOutputText = await enforceInlineByteCap(outputText, inlineCap);
686
693
 
687
694
  const resultBuilder = toolResult(details)
688
695
  .text(cappedOutputText)
@@ -32,6 +32,7 @@ import { TaskTool } from "../task";
32
32
  import type { AgentOutputManager } from "../task/output-manager";
33
33
  import { canSpawnAtDepth, type StructuredSubagentSchemaMode } from "../task/types";
34
34
  import type { EventBus } from "../utils/event-bus";
35
+ import { type InspectImageMode, isInspectImageToolActive } from "../utils/inspect-image-mode";
35
36
  import { WebSearchTool } from "../web/search";
36
37
  import type { WorkspaceTree } from "../workspace-tree";
37
38
  import { AskTool } from "./ask";
@@ -61,7 +62,7 @@ import { ReadTool } from "./read";
61
62
  import type { PlanProposalHandler } from "./resolve";
62
63
  import { type TodoPhase, TodoTool } from "./todo";
63
64
  import { WriteTool } from "./write";
64
- import { isMountableUnderXdev, XdevRegistry } from "./xdev";
65
+ import { isMountableUnderXdev, type XdevState } from "./xdev";
65
66
  import { YieldTool } from "./yield";
66
67
 
67
68
  export * from "../edit";
@@ -243,8 +244,10 @@ export interface ToolSession {
243
244
  isToolActive?: (name: string) => boolean;
244
245
  /** Update the active built-in tool predicate when a session changes tools mid-run. */
245
246
  setActiveToolNames?: (names: Iterable<string>) => void;
246
- /** Tools mounted under `xd://` (set by createTools when `tools.xdev` is active); read/write consult it at execute time. */
247
- xdevRegistry?: XdevRegistry;
247
+ /** Canonical map containing every registered tool exactly once. */
248
+ toolRegistry?: Map<string, Tool>;
249
+ /** `xd://` presentation state backed by {@link toolRegistry}. */
250
+ xdev?: XdevState;
248
251
  /** Agent registry for IRC routing across live sessions. */
249
252
  agentRegistry?: AgentRegistry;
250
253
  /** Idle→parked→revive lifecycle owner; lets the hub kill a non-job-backed agent registration. Default: AgentLifecycleManager.global(). */
@@ -263,6 +266,8 @@ export interface ToolSession {
263
266
  getActiveModelString?: () => string | undefined;
264
267
  /** Get the current session model object (provider/api capabilities), regardless of how it was chosen. */
265
268
  getActiveModel?: () => Model | undefined;
269
+ /** Session-scoped inspect_image mode override set by `/vision`; wins over the persisted setting. */
270
+ getInspectImageModeOverride?: () => InspectImageMode | undefined;
266
271
  /** Get the session's live per-family service tiers (undefined = none). Source of truth for subagent `tier.subagent: inherit`. */
267
272
  getServiceTierByFamily?: () => ServiceTierByFamily | undefined;
268
273
  /** Auth storage for passing to subagents (avoids re-discovery) */
@@ -555,7 +560,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
555
560
  if (name === "github") return session.settings.get("github.enabled");
556
561
  if (name === "ast_grep") return session.settings.get("astGrep.enabled");
557
562
  if (name === "ast_edit") return session.settings.get("astEdit.enabled");
558
- if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
563
+ if (name === "inspect_image") return isInspectImageToolActive(session);
559
564
  if (name === "web_search") return session.settings.get("web_search.enabled");
560
565
  if (name === "ask") return session.settings.get("ask.enabled");
561
566
  if (name === "browser") return session.settings.get("browser.enabled");
@@ -613,6 +618,10 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
613
618
  }),
614
619
  );
615
620
  let tools = baseResults.filter((r): r is Tool => r !== null);
621
+ const toolRegistry = session.toolRegistry ?? new Map<string, Tool>();
622
+ session.toolRegistry = toolRegistry;
623
+ const builtInNames = new Set(tools.map(tool => tool.name));
624
+ for (const tool of tools) toolRegistry.set(tool.name, tool);
616
625
 
617
626
  // Ordinary sessions use xd:// for discoverable built-ins, custom tools, and
618
627
  // MCP tools. Structured children must expose only their host-provided names,
@@ -621,26 +630,26 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
621
630
  const xdevEnabled = !restrictToolNames && session.settings.get("tools.xdev");
622
631
  const mountBuiltinTools = requestedTools === undefined;
623
632
  if (xdevEnabled) {
624
- const mounted: Tool[] = [];
633
+ const mountedNames = new Set<string>();
625
634
  const kept: Tool[] = [];
626
635
  for (const tool of tools) {
627
636
  const mountable = mountBuiltinTools && isMountableUnderXdev(tool) && tool.name in BUILTIN_TOOLS;
628
- (mountable ? mounted : kept).push(tool);
637
+ if (mountable) mountedNames.add(tool.name);
638
+ else kept.push(tool);
629
639
  }
630
- session.xdevRegistry = new XdevRegistry(mounted);
640
+ session.xdev = {
641
+ tools: toolRegistry,
642
+ mountedNames,
643
+ builtInNames,
644
+ isActive: name => session.isToolActive?.(name) === true,
645
+ };
631
646
  tools = kept;
632
- const finalActiveNames = new Set(tools.map(tool => tool.name));
633
- if (session.setActiveToolNames) {
634
- session.setActiveToolNames(finalActiveNames);
635
- } else {
636
- session.isToolActive = name => finalActiveNames.has(name);
637
- }
638
647
  }
639
648
  // The xd:// transport rides read/write: `read xd://` lists+documents devices,
640
649
  // `write xd://<tool>` executes them. Staged previews from deferrable tools
641
650
  // (e.g. ast_edit) also resolve through a `write` to xd://resolve/reject. Retain
642
651
  // both whenever any device is mounted or a deferrable tool can stage one.
643
- const xdevMounted = (session.xdevRegistry?.size ?? 0) > 0;
652
+ const xdevMounted = (session.xdev?.mountedNames.size ?? 0) > 0;
644
653
  if (
645
654
  !restrictToolNames &&
646
655
  (tools.some(tool => tool.deferrable === true) || xdevMounted) &&
@@ -648,15 +657,26 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
648
657
  ) {
649
658
  const writeTool = await logger.time("createTools:write", BUILTIN_TOOLS.write, session);
650
659
  if (writeTool) {
651
- tools.push(wrapToolWithMetaNotice(writeTool));
660
+ const wrapped = wrapToolWithMetaNotice(writeTool);
661
+ tools.push(wrapped);
662
+ toolRegistry.set(wrapped.name, wrapped);
663
+ builtInNames.add(wrapped.name);
652
664
  }
653
665
  }
654
666
  if (!restrictToolNames && xdevMounted && !tools.some(tool => tool.name === "read")) {
655
667
  const readTool = await logger.time("createTools:read", BUILTIN_TOOLS.read, session);
656
668
  if (readTool) {
657
- tools.push(wrapToolWithMetaNotice(readTool));
669
+ const wrapped = wrapToolWithMetaNotice(readTool);
670
+ tools.push(wrapped);
671
+ toolRegistry.set(wrapped.name, wrapped);
672
+ builtInNames.add(wrapped.name);
658
673
  }
659
674
  }
675
+ if (xdevEnabled) {
676
+ const finalActiveNames = new Set(tools.map(tool => tool.name));
677
+ if (session.setActiveToolNames) session.setActiveToolNames(finalActiveNames);
678
+ else session.isToolActive = name => finalActiveNames.has(name);
679
+ }
660
680
 
661
681
  return tools;
662
682
  }
@@ -631,6 +631,26 @@ export function resolveOutputSinkHeadBytes(s: Settings | undefined): number {
631
631
  return getSpillConfig(s).headBytes;
632
632
  }
633
633
 
634
+ /**
635
+ * Slack on top of the configured spill threshold before the final-defense
636
+ * inline byte cap fires. The OutputSink already bounds inline bodies to the
637
+ * threshold; only notice slop (wall time, exit code, elision marker,
638
+ * `[raw output: artifact://N]` footer) rides above it. The slack keeps the
639
+ * cap a genuine last resort for paths that bypass the sink (e.g. ACP
640
+ * client-bridge terminals) instead of re-truncating — and re-saving — every
641
+ * sink-elided result (the double-artifact `Artifact: N+1` vs `artifact://N`
642
+ * mismatch).
643
+ */
644
+ const INLINE_CAP_SLACK_BYTES = 2 * 1024;
645
+
646
+ /**
647
+ * Resolve the `enforceInlineByteCap` budget for streaming tools (bash/ssh)
648
+ * from session settings: the user's spill threshold plus notice slack.
649
+ */
650
+ export function resolveInlineByteCapBudget(s: Settings | undefined): number {
651
+ return getSpillConfig(s).threshold + INLINE_CAP_SLACK_BYTES;
652
+ }
653
+
634
654
  /**
635
655
  * Resolve the per-line column cap from session settings. Shared by streaming
636
656
  * executors (bash/python/ssh/eval via OutputSink) and the `read` tool's
package/src/tools/read.ts CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  import { fileHyperlink, renderCodeCell, renderMarkdownCell, renderStatusLine, tryResolveInternalUrlSync } from "../tui";
58
58
  import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
59
59
  import { buildLineEntriesWithBlockContext, type LineEntry, lineEntriesToPlainText } from "../utils/block-context";
60
+ import { isCpuProfilePath, renderCpuProfile } from "../utils/cpuprofile";
60
61
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
61
62
  import {
62
63
  ImageInputTooLargeError,
@@ -64,7 +65,9 @@ import {
64
65
  MAX_IMAGE_INPUT_BYTES,
65
66
  webpExclusionForModel,
66
67
  } from "../utils/image-loading";
68
+ import { isInspectImageToolActive } from "../utils/inspect-image-mode";
67
69
  import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit";
70
+ import { isSampleProfilePath, renderSampleProfile } from "../utils/sample-profile";
68
71
  import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "../utils/zip";
69
72
  import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
70
73
  import {
@@ -131,6 +134,7 @@ import {
131
134
  } from "./sqlite-reader";
132
135
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
133
136
  import { toolResult } from "./tool-result";
137
+ import { xdevDocs, xdevListing } from "./xdev";
134
138
 
135
139
  // Per-session memo for tree-sitter summaries. `summarizeCode` is a pure function
136
140
  // of (code, path, fold settings) but costs ~12-18ms for a ~1500-line file, and a
@@ -155,6 +159,8 @@ function getSummaryParseCache(session: object): LRUCache<string, SummaryResult |
155
159
  }
156
160
 
157
161
  const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
162
+ /** Largest profile (`*.sample.txt`, `*.cpuprofile`) converted to a bottleneck summary; bigger files read as plain text. */
163
+ const MAX_PROFILE_SUMMARY_BYTES = 32 * 1024 * 1024;
158
164
  const MAX_SUMMARY_LINES = 20_000;
159
165
  const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
160
166
  /**
@@ -855,31 +861,74 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
855
861
  pathTargetsSsh(String((args as { path?: unknown }).path ?? "")) ? "exec" : "read";
856
862
  readonly label = "Read";
857
863
  readonly loadMode = "essential";
858
- readonly description: string;
864
+ description: string;
859
865
  readonly parameters = readSchema;
860
866
  readonly strict = true;
861
867
 
862
868
  readonly #autoResizeImages: boolean;
863
869
  readonly #defaultLimit: number;
864
- readonly #inspectImageEnabled: boolean;
870
+ #inspectImageActive: boolean;
865
871
 
866
872
  constructor(private readonly session: ToolSession) {
867
- const displayMode = resolveFileDisplayMode(session);
868
873
  this.#autoResizeImages = session.settings.get("images.autoResize");
869
874
  this.#defaultLimit = Math.max(
870
875
  1,
871
876
  Math.min(session.settings.get("read.defaultLimit") ?? DEFAULT_MAX_LINES, DEFAULT_MAX_LINES),
872
877
  );
873
- this.#inspectImageEnabled = session.settings.get("inspect_image.enabled");
874
- this.description = prompt.render(readDescription, {
878
+ this.#inspectImageActive = this.#resolveInspectImageAvailability();
879
+ this.description = this.#renderDescription();
880
+ }
881
+
882
+ /**
883
+ * Re-render the tool description for the current display mode and the
884
+ * effective inspect_image state (mode setting, `/vision` override, and
885
+ * active-model image capability all feed it, so it can change at runtime).
886
+ */
887
+ #renderDescription(): string {
888
+ const displayMode = resolveFileDisplayMode(this.session);
889
+ return prompt.render(readDescription, {
875
890
  DEFAULT_LIMIT: String(this.#defaultLimit),
876
891
  DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
877
892
  IS_HL_MODE: displayMode.hashLines,
878
893
  IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
879
- INSPECT_IMAGE_ENABLED: this.#inspectImageEnabled,
894
+ INSPECT_IMAGE_ENABLED: this.#inspectImageActive,
880
895
  });
881
896
  }
882
897
 
898
+ /**
899
+ * Whether the agent can actually reach `inspect_image` right now: exposed
900
+ * top-level, or mounted as an `xd://` device while the effective mode wants
901
+ * it (mounted devices stay executable via `write xd://inspect_image`, so a
902
+ * metadata-only read remains actionable). Sessions with neither
903
+ * availability signal (tests, embedded use) fall back to the mode
904
+ * computation alone. Restricted slates (subagents without the tool and
905
+ * without xdev) resolve to unavailable, so those sessions get inline image
906
+ * blocks instead of guidance pointing at an absent tool.
907
+ */
908
+ #resolveInspectImageAvailability(): boolean {
909
+ const topLevel = this.session.isToolActive?.("inspect_image");
910
+ const xdev = this.session.xdev;
911
+ if (topLevel === undefined && xdev === undefined) return isInspectImageToolActive(this.session);
912
+ if (topLevel === true) return true;
913
+ return xdev?.mountedNames.has("inspect_image") === true && isInspectImageToolActive(this.session);
914
+ }
915
+
916
+ /**
917
+ * Re-evaluate the effective inspect_image state; it can flip when the model
918
+ * or the `/vision` override changes after this tool was constructed. Keeps
919
+ * the behavior branch and the advertised description in lockstep. Called
920
+ * per image read and by tool reconciliation before prompt rebuilds (which
921
+ * passes the post-change availability as `availableOverride`).
922
+ */
923
+ syncInspectImageState(availableOverride?: boolean): boolean {
924
+ const active = availableOverride ?? this.#resolveInspectImageAvailability();
925
+ if (active !== this.#inspectImageActive) {
926
+ this.#inspectImageActive = active;
927
+ this.description = this.#renderDescription();
928
+ }
929
+ return active;
930
+ }
931
+
883
932
  /**
884
933
  * Recover the active approved plan when a model rewrites its `local://` URL
885
934
  * as a same-basename path in the working-directory root.
@@ -1256,10 +1305,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1256
1305
 
1257
1306
  /**
1258
1307
  * Build content blocks for an on-disk image file: an `inspect_image`
1259
- * metadata note when inspection is enabled, otherwise the decoded image
1308
+ * metadata note when inspection is active, otherwise the decoded image
1260
1309
  * block. Shared by the plain-file read path and the `local://` image fast
1261
- * path so both honor `inspect_image.enabled`, the size cap, and auto-resize
1262
- * identically. Too-large / unsupported images surface as {@link ToolError}.
1310
+ * path so both honor the effective inspect_image state, the size cap, and
1311
+ * auto-resize identically. Too-large / unsupported images surface as {@link ToolError}.
1263
1312
  */
1264
1313
  async #loadImageContent(options: {
1265
1314
  readPath: string;
@@ -1269,7 +1318,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1269
1318
  fileSize: number;
1270
1319
  }): Promise<{ content: Array<TextContent | ImageContent>; details: ReadToolDetails; sourcePath: string }> {
1271
1320
  const { readPath, absolutePath, mimeType, imageMetadata, fileSize } = options;
1272
- if (this.#inspectImageEnabled) {
1321
+ if (this.syncInspectImageState()) {
1273
1322
  const outputMime = imageMetadata?.mimeType ?? mimeType;
1274
1323
  const metadataLines = [
1275
1324
  "Image metadata:",
@@ -2250,12 +2299,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2250
2299
  return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal);
2251
2300
  }
2252
2301
 
2253
- // Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://, omp://, issue://, pr://).
2302
+ // Handle native OMP URLs and custom-scheme resources advertised by MCP servers.
2254
2303
  // Use the internal-URL-aware splitter so malformed selectors are peeled
2255
2304
  // off the URL and surfaced via parseSel rather than confusing handlers.
2256
2305
  const internalRouter = InternalUrlRouter.instance();
2257
2306
  let promotedSelector: string | undefined;
2258
- if (internalRouter.canHandle(readPath)) {
2307
+ if (internalRouter.canResolve(readPath)) {
2259
2308
  const internalTarget = splitInternalUrlSel(readPath);
2260
2309
  const parsed = parseSel(internalTarget.sel);
2261
2310
  if (internalTarget.sel !== undefined && parsed.kind === "none") {
@@ -2432,6 +2481,31 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2432
2481
  const mimeType = imageMetadata?.mimeType;
2433
2482
  const ext = path.extname(absolutePath).toLowerCase();
2434
2483
  const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
2484
+
2485
+ // Profiler reports (macOS `sample` call trees, V8 `.cpuprofile` JSON):
2486
+ // replace the raw dump with a bottleneck summary (hot paths, top self
2487
+ // time/samples). `:raw` reads the original bytes; text that merely wears
2488
+ // the extension falls through to the plain-text path.
2489
+ if (!mimeType && !isRawSelector(parsed) && fileSize <= MAX_PROFILE_SUMMARY_BYTES) {
2490
+ let rendered: string | null = null;
2491
+ if (isSampleProfilePath(absolutePath)) rendered = renderSampleProfile(await Bun.file(absolutePath).text());
2492
+ else if (isCpuProfilePath(absolutePath)) rendered = renderCpuProfile(await Bun.file(absolutePath).text());
2493
+ if (rendered) {
2494
+ if (isMultiRange(parsed) && parsed.kind === "lines") {
2495
+ return this.#buildInMemoryMultiRangeResult(rendered, parsed.ranges, {
2496
+ details: { resolvedPath: absolutePath },
2497
+ sourcePath: absolutePath,
2498
+ entityLabel: "profile summary",
2499
+ });
2500
+ }
2501
+ const { offset, limit } = selToOffsetLimit(parsed);
2502
+ return this.#buildInMemoryTextResult(rendered, offset, limit, {
2503
+ details: { resolvedPath: absolutePath },
2504
+ sourcePath: absolutePath,
2505
+ entityLabel: "profile summary",
2506
+ });
2507
+ }
2508
+ }
2435
2509
  // Read the file based on type
2436
2510
  let content: Array<TextContent | ImageContent> | undefined;
2437
2511
  let details: ReadToolDetails = {};
@@ -3230,9 +3304,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
3230
3304
  read: async name => {
3231
3305
  if (name === REPORT_ISSUE_DEVICE_NAME) return reportIssueDeviceUsage();
3232
3306
  if (name && isResolutionDeviceName(name)) return resolutionDeviceUsage(name);
3233
- const registry = this.session.xdevRegistry;
3234
- if (!registry || registry.size === 0) throw new ToolError("xd:// is not mounted in this session.");
3235
- return name === null ? registry.listing() : registry.docs(name);
3307
+ const xdev = this.session.xdev;
3308
+ if (!xdev) throw new ToolError("xd:// is not mounted in this session.");
3309
+ return name === null ? xdevListing(xdev) : xdevDocs(xdev, name);
3236
3310
  },
3237
3311
  },
3238
3312
  });
@@ -88,7 +88,14 @@ import {
88
88
  } from "./sqlite-reader";
89
89
  import { ToolError } from "./tool-errors";
90
90
  import { toolResult } from "./tool-result";
91
- import { renderXdevCall, renderXdevResult, type XdevDispatch } from "./xdev";
91
+ import {
92
+ dispatchXdevTool,
93
+ renderXdevCall,
94
+ renderXdevResult,
95
+ resolveXdevTool,
96
+ type XdevDispatch,
97
+ xdevListing,
98
+ } from "./xdev";
92
99
 
93
100
  const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
94
101
  const EXECUTABLE_NOTICE = "[Notice: Made executable via chmod +x]";
@@ -471,7 +478,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
471
478
  if (xdevTarget) {
472
479
  if (xdevTarget.name === REPORT_ISSUE_DEVICE_NAME) return "write";
473
480
  if (xdevTarget.name && isResolutionDeviceName(xdevTarget.name)) return "read";
474
- const inst = xdevTarget.name ? this.session.xdevRegistry?.get(xdevTarget.name) : undefined;
481
+ const inst =
482
+ xdevTarget.name && this.session.xdev ? resolveXdevTool(this.session.xdev, xdevTarget.name) : undefined;
475
483
  if (!inst) return "exec";
476
484
  // Decode the device JSON payload and evaluate the mounted tool's own
477
485
  // approval (which may be argument-dependent, e.g. ast_edit is read-tier
@@ -1104,14 +1112,15 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
1104
1112
  };
1105
1113
  return;
1106
1114
  }
1107
- const registry = this.session.xdevRegistry;
1108
- if (!registry || registry.size === 0) {
1115
+ const xdev = this.session.xdev;
1116
+ if (!xdev) {
1109
1117
  throw new ToolError("xd:// is not mounted in this session.");
1110
1118
  }
1111
1119
  if (!name) {
1112
- throw new ToolError(`Cannot write to xd:// itself — pick a device:\n${registry.listing()}`);
1120
+ throw new ToolError(`Cannot write to xd:// itself — pick a device:\n${xdevListing(xdev)}`);
1113
1121
  }
1114
- const { result, xdev } = await registry.dispatch(
1122
+ const { result, xdev: dispatch } = await dispatchXdevTool(
1123
+ xdev,
1115
1124
  name,
1116
1125
  deviceContent,
1117
1126
  _toolCallId,
@@ -1124,7 +1133,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
1124
1133
  );
1125
1134
  xdResult = {
1126
1135
  content: result.content,
1127
- details: { xdev },
1136
+ details: { xdev: dispatch },
1128
1137
  isError: result.isError,
1129
1138
  useless: result.useless,
1130
1139
  };