@oh-my-pi/pi-coding-agent 14.9.8 → 15.0.0

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 (138) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/package.json +7 -7
  3. package/scripts/build-binary.ts +11 -0
  4. package/scripts/format-prompts.ts +1 -1
  5. package/src/cli/args.ts +2 -2
  6. package/src/cli/stats-cli.ts +2 -0
  7. package/src/cli.ts +24 -1
  8. package/src/commands/acp.ts +24 -0
  9. package/src/commands/launch.ts +6 -4
  10. package/src/commit/agentic/prompts/system.md +1 -1
  11. package/src/config/model-resolver.ts +30 -0
  12. package/src/config/settings-schema.ts +61 -9
  13. package/src/config/settings.ts +18 -1
  14. package/src/edit/index.ts +22 -1
  15. package/src/edit/modes/patch.ts +10 -0
  16. package/src/edit/modes/replace.ts +3 -0
  17. package/src/edit/renderer.ts +10 -0
  18. package/src/edit/streaming.ts +1 -1
  19. package/src/eval/js/context-manager.ts +10 -9
  20. package/src/eval/js/shared/rewrite-imports.ts +120 -48
  21. package/src/eval/js/shared/runtime.ts +31 -4
  22. package/src/eval/js/tool-bridge.ts +43 -21
  23. package/src/extensibility/extensions/runner.ts +54 -1
  24. package/src/extensibility/extensions/types.ts +11 -0
  25. package/src/extensibility/skills.ts +33 -1
  26. package/src/hashline/grammar.lark +1 -1
  27. package/src/hashline/input.ts +11 -5
  28. package/src/internal-urls/docs-index.generated.ts +7 -7
  29. package/src/internal-urls/index.ts +1 -0
  30. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  31. package/src/internal-urls/router.ts +6 -3
  32. package/src/internal-urls/types.ts +22 -1
  33. package/src/main.ts +13 -9
  34. package/src/modes/acp/acp-agent.ts +361 -54
  35. package/src/modes/acp/acp-client-bridge.ts +152 -0
  36. package/src/modes/acp/acp-event-mapper.ts +180 -15
  37. package/src/modes/acp/terminal-auth.ts +37 -0
  38. package/src/modes/components/read-tool-group.ts +29 -1
  39. package/src/modes/controllers/command-controller.ts +14 -6
  40. package/src/modes/controllers/event-controller.ts +24 -11
  41. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  42. package/src/modes/controllers/input-controller.ts +72 -39
  43. package/src/modes/interactive-mode.ts +71 -7
  44. package/src/modes/rpc/rpc-mode.ts +17 -2
  45. package/src/modes/types.ts +6 -2
  46. package/src/modes/utils/ui-helpers.ts +15 -3
  47. package/src/prompts/agents/designer.md +5 -5
  48. package/src/prompts/agents/explore.md +7 -7
  49. package/src/prompts/agents/init.md +9 -9
  50. package/src/prompts/agents/librarian.md +14 -14
  51. package/src/prompts/agents/plan.md +4 -4
  52. package/src/prompts/agents/reviewer.md +5 -5
  53. package/src/prompts/agents/task.md +10 -10
  54. package/src/prompts/commands/orchestrate.md +2 -2
  55. package/src/prompts/compaction/branch-summary.md +3 -3
  56. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  57. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  58. package/src/prompts/compaction/compaction-summary.md +5 -5
  59. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  60. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  61. package/src/prompts/memories/consolidation.md +2 -2
  62. package/src/prompts/memories/read-path.md +1 -1
  63. package/src/prompts/memories/stage_one_input.md +1 -1
  64. package/src/prompts/memories/stage_one_system.md +5 -5
  65. package/src/prompts/review-request.md +4 -4
  66. package/src/prompts/system/agent-creation-architect.md +17 -17
  67. package/src/prompts/system/agent-creation-user.md +2 -2
  68. package/src/prompts/system/commit-message-system.md +2 -2
  69. package/src/prompts/system/custom-system-prompt.md +2 -2
  70. package/src/prompts/system/eager-todo.md +6 -6
  71. package/src/prompts/system/handoff-document.md +1 -1
  72. package/src/prompts/system/plan-mode-active.md +22 -21
  73. package/src/prompts/system/plan-mode-approved.md +4 -4
  74. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  75. package/src/prompts/system/plan-mode-reference.md +2 -2
  76. package/src/prompts/system/plan-mode-subagent.md +8 -8
  77. package/src/prompts/system/plan-mode-tool-decision-reminder.md +2 -2
  78. package/src/prompts/system/project-prompt.md +4 -4
  79. package/src/prompts/system/subagent-system-prompt.md +7 -7
  80. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  81. package/src/prompts/system/system-prompt.md +72 -71
  82. package/src/prompts/system/ttsr-interrupt.md +1 -1
  83. package/src/prompts/tools/apply-patch.md +1 -1
  84. package/src/prompts/tools/ast-edit.md +3 -3
  85. package/src/prompts/tools/ast-grep.md +3 -3
  86. package/src/prompts/tools/browser.md +3 -3
  87. package/src/prompts/tools/checkpoint.md +3 -3
  88. package/src/prompts/tools/exit-plan-mode.md +2 -2
  89. package/src/prompts/tools/find.md +3 -3
  90. package/src/prompts/tools/github.md +2 -5
  91. package/src/prompts/tools/hashline.md +20 -20
  92. package/src/prompts/tools/image-gen.md +3 -3
  93. package/src/prompts/tools/irc.md +1 -1
  94. package/src/prompts/tools/lsp.md +2 -2
  95. package/src/prompts/tools/patch.md +6 -6
  96. package/src/prompts/tools/read.md +7 -7
  97. package/src/prompts/tools/replace.md +5 -5
  98. package/src/prompts/tools/retain.md +1 -1
  99. package/src/prompts/tools/rewind.md +2 -2
  100. package/src/prompts/tools/search.md +2 -2
  101. package/src/prompts/tools/ssh.md +2 -2
  102. package/src/prompts/tools/task.md +12 -6
  103. package/src/prompts/tools/web-search.md +2 -2
  104. package/src/prompts/tools/write.md +3 -3
  105. package/src/sdk.ts +69 -12
  106. package/src/session/agent-session.ts +231 -22
  107. package/src/session/client-bridge.ts +81 -0
  108. package/src/session/compaction/errors.ts +31 -0
  109. package/src/session/compaction/index.ts +1 -0
  110. package/src/slash-commands/acp-builtins.ts +46 -0
  111. package/src/slash-commands/builtin-registry.ts +699 -116
  112. package/src/slash-commands/helpers/context-report.ts +39 -0
  113. package/src/slash-commands/helpers/format.ts +23 -0
  114. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  115. package/src/slash-commands/helpers/mcp.ts +532 -0
  116. package/src/slash-commands/helpers/parse.ts +85 -0
  117. package/src/slash-commands/helpers/ssh.ts +193 -0
  118. package/src/slash-commands/helpers/todo.ts +279 -0
  119. package/src/slash-commands/helpers/usage-report.ts +91 -0
  120. package/src/slash-commands/types.ts +126 -0
  121. package/src/task/executor.ts +10 -3
  122. package/src/task/index.ts +29 -51
  123. package/src/task/render.ts +6 -3
  124. package/src/task/worktree.ts +170 -239
  125. package/src/tools/bash.ts +176 -2
  126. package/src/tools/browser/tab-supervisor.ts +13 -13
  127. package/src/tools/conflict-detect.ts +6 -6
  128. package/src/tools/fetch.ts +15 -4
  129. package/src/tools/find.ts +19 -1
  130. package/src/tools/gh-renderer.ts +0 -12
  131. package/src/tools/gh.ts +682 -176
  132. package/src/tools/github-cache.ts +548 -0
  133. package/src/tools/index.ts +3 -0
  134. package/src/tools/read.ts +110 -27
  135. package/src/tools/write.ts +23 -1
  136. package/src/tui/code-cell.ts +70 -2
  137. package/src/utils/git.ts +5 -0
  138. package/src/task/isolation-backend.ts +0 -94
package/CHANGELOG.md CHANGED
@@ -2,6 +2,107 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.0.0] - 2026-05-13
6
+ ### Breaking Changes
7
+
8
+ - Removed `op: issue_view` and `op: pr_view` from the `github` tool. Read single issues/PRs via the `read` tool against `issue://<N>` / `pr://<N>` (or the long form `issue://<owner>/<repo>/<N>` / `pr://<owner>/<repo>/<N>`); append `?comments=0` to drop the comments section. The `issue` and `comments` parameters were removed from the tool schema since no remaining op consumes them. Mutating ops (`pr_create`, `pr_checkout`, `pr_push`), `repo_view`, `search_*`, and `run_watch` are unchanged.
9
+ - Removed `op: pr_diff` (along with the `nameOnly` and `exclude` schema fields) from the `github` tool. Read PR diffs through the new `pr://` URL family: `pr://<N>/diff` for the changed-file listing, `pr://<N>/diff/<i>` for a single file slice (1-indexed), and `pr://<N>/diff/all` for the verbatim unified diff. Long-form `pr://<owner>/<repo>/<N>/diff[/…]` works the same way. All three variants share one `gh pr diff` invocation through a new `pr-diff` cache row, so the listing and per-file slices reconstruct from cached bytes without re-shelling. Diff content is served as `text/plain` so the `read` tool's line selectors (e.g. `pr://<N>/diff/all:200-400`) page the cached output without falsely advertising hashline anchors.
10
+ - Renamed ACP custom extension methods from `omp/*` to `_omp/*` to comply with the ACP spec's `_`-prefix requirement for non-spec methods; existing callers must update method names
11
+
12
+ ### Added
13
+
14
+ - Added markdown rendering for `read` results when content type is `text/markdown`, so GitHub internal-URL outputs are shown as formatted markdown instead of plain code blocks
15
+ - Added `pr://<N>/diff`, `pr://<N>/diff/<i>`, and `pr://<N>/diff/all` internal-URL shapes covering changed-file listings, per-file slices, and the full unified diff. They share one `pr-diff` SQLite cache row with the same TTL knobs as `pr://<N>` views (`github.cache.softTtlSec` / `github.cache.hardTtlSec` / `github.cache.enabled`). Single PR views now advertise the diff entry point via a `Diff: pr://<owner>/<repo>/<N>/diff` note. Cache schema bumped to `user_version = 3`; older rows are dropped on first open to add credential-scoped keys and relax the `kind` CHECK constraint.
16
+ - Added `issue://` / `pr://` internal-URL schemes that share a SQLite-backed cache with the rest of the `github` tool. Single-item reads (`issue://<N>`, `issue://<owner>/<repo>/<N>`) return rendered markdown and within `github.cache.softTtlSec` (default 5 minutes) skip the `gh` round-trip entirely; within `github.cache.hardTtlSec` (default 7 days) the cached row is returned and a background refresh is scheduled. Root and repo-scoped reads (`issue://`, `pr://owner/repo`) issue a live `gh issue list` / `gh pr list` for browsing, supporting `?state=open|closed|all` for issues, `?state=open|closed|merged|all` for PRs, and `?limit=`, `?author=`, `?label=` query params. Rendered output lands in `~/.omp/cache/github-cache.db` (override via `OMP_GITHUB_CACHE_DB`); disable the cache entirely with `github.cache.enabled = false`. Cwd→default-repo lookups (`gh repo view`) are memoized per-process.
17
+ - Added new `Approve and compact context` choice to the ExitPlanMode approval selector. Sits between `Approve and execute` (purge session) and `Approve and keep context` (full transcript) — runs `/compact` on the plan-mode transcript with a planning-specific summarization hint, then dispatches the plan-approved execution turn so it lands on a fresh cache anchor with the summarized rationale carried over. Cancelling the compaction (Esc or any other abort source) defers the execution dispatch and surfaces a warning so the operator can resubmit manually; non-abort failures proceed best-effort.
18
+ - Added `CompactionCancelledError` typed sentinel and `CompactionOutcome` (`"ok" | "cancelled" | "failed"`) return type to `@oh-my-pi/pi-coding-agent/session/compaction`. `CommandController.executeCompaction` and `handleCompactCommand` now return the outcome instead of `void` so callers can discriminate user-driven aborts from generic failures without inspecting error messages.
19
+ - Added a `credential_disabled` extension event so extensions can subscribe via `pi.on("credential_disabled", handler)` and react when `AuthStorage` automatically soft-disables a credential (e.g. OAuth `invalid_grant`). Replaces the current `agent_end` errorMessage regex pattern downstream extensions have to match against. Handler payload is `{ type, provider, disabledCause }`. `createAgentSession()` subscribes the per-session extension runner to the shared `AuthStorage` via `authStorage.onCredentialDisabled(...)` at the very top of session creation — before any startup model probes run — so events fire on every disable regardless of whether the embedder also has a constructor `onCredentialDisabled` handler attached. The SDK forwards through `ExtensionRunner.emitCredentialDisabled(event)`, which buffers events until `runner.initialize(...)` runs in the mode controller and then flushes them through `emit()` so extension handlers see populated UI/runtime context (rather than the constructor's no-op default with `hasUI=false`, an unset model, and no-op runtime actions). On `session.dispose()` the subscription is unsubscribed; the embedder's constructor-attached listener keeps firing through its own permanent subscription. The outer `createAgentSession()` catch also releases the subscription if startup throws before the dispose-wrap is wired, so repeated retries don't accumulate dead listeners.
20
+ - Added `omp acp` subcommand for launching as an ACP (Agent Client Protocol) server over stdio
21
+ - Added explicit `type` discriminators to ACP `initialize` auth methods, including a `terminal` setup method gated on `clientCapabilities.auth.terminal`
22
+ - Added ACP equivalents for the remaining TUI slash commands (`/jobs`, `/changelog`, `/dump`, `/copy`, `/hotkeys`, `/extensions`, `/agents`, `/model`, `/plan`, `/loop`, `/btw`, `/login`, `/logout`, `/resume`, `/tree`, `/branch`, `/new`, `/drop`, `/handoff`, `/fork`, `/session delete`, `/export`, `/share`, `/todo`, `/memory`, `/move`, `/mcp`, `/ssh`, `/marketplace`, `/plugins`) so ACP clients reach feature parity with the TUI for non-interactive flows
23
+ - Added ACP `plan` mode: when `plan.enabled` setting is on, ACP `session/new`/`load`/`resume`/`fork` advertise a `plan` mode alongside `default`; `session/set_mode` toggles plan-mode state so the next agent turn injects the plan-mode system prompt
24
+ - Added ACP `ClientBridge` abstraction (`packages/coding-agent/src/session/client-bridge.ts`) that routes tool I/O through the connected client when capabilities are advertised at `initialize`; populated from `AgentSideConnection` in ACP mode
25
+ - Added ACP `terminal/*` routing for `bash`: when the client advertises `terminal: true`, the tool creates a client-side terminal, embeds its `terminalId` on the live tool card, polls output, and releases the handle on exit or abort
26
+ - Added ACP `fs/read_text_file` and `fs/write_text_file` routing for the `read` and `write` tools: when the client advertises `fs.readTextFile` / `fs.writeTextFile`, plain-text reads/writes go through the editor (surfacing unsaved buffer content and letting the editor track agent writes); falls back to disk only for reads, throws on bridge write failures
27
+ - Added ACP `session/request_permission` gate around `bash`, `edit`, `write`, and `ast_edit` when an ACP client is connected; remembers `allow_always` / `reject_always` decisions per tool for the session lifetime
28
+ - Added `diff` `ToolCallContent` emission for edit tool results: per-file `oldText`/`newText` is threaded through `EditToolPerFileResult` / `EditToolDetails` so ACP clients can render inline diffs
29
+ - Added richer ACP `StopReason` mapping (`max_tokens`, `refusal`, `cancelled`) derived from the last assistant message's internal stop reason; previously only `end_turn`/`cancelled` were emitted
30
+ - Added `_meta.messageCount` and `_meta.size` on `session/list` `SessionInfo` entries
31
+ - Added ACP `tool_call_update` `locations` refresh from in-flight tool args and final result details so clients can "follow along" multi-file edits in real time
32
+
33
+ ### Changed
34
+
35
+ - Changed issue and pull-request list entries to link to repository-qualified URLs (for example `issue://owner/repo/<N>`) so list items open correctly outside the default repo
36
+ - Aligned prompt instruction language by defining `NEVER` and `AVOID` as strict aliases for `MUST NOT` and `SHOULD NOT` in the system prompt, and standardized agent, tool, and system prompt templates to use those terms consistently
37
+ - Changed `--mode acp` to apply the same stdout-quiet overrides as `--mode rpc` so no banner or status text leaks into the JSON-RPC channel
38
+ - Changed ACP startup to no longer require a configured model so registry validators and clients can complete `initialize` and `authenticate` before any model is selected
39
+ ### Fixed
40
+
41
+ - Deferred flushing of buffered `credential_disabled` events during extension runner initialization to a microtask so handler failures are now routed through `onError()` registrations made immediately after `initialize()`, preserving extension error reporting
42
+ - Fixed `eval` tool dynamic `await import("./relative.ts")` calls failing with `Cannot find module ... from .../eval/js/shared/runtime.ts`. The static-import rewriter only handled `ImportDeclaration` nodes, so dynamic-import call expressions resolved their specifier against the worker module's URL instead of the session cwd. The rewriter now walks the full AST and additionally swaps `import` callees in `CallExpression` nodes for `__omp_import__`, which forwards the optional options bag verbatim to native `import()` so `{ with: { type: "json" } }` round-trips. Renamed `rewriteStaticImports` → `rewriteImports`.
43
+ - Fixed `pr://<owner>/<repo>/diff` URLs for repositories named `diff` to continue resolving to PR list lookups instead of being parsed as short-form diff links
44
+ - Fixed `issue://<N>/diff` short form previously misparsing as a repo named `<N>/diff` and surfacing a confusing GraphQL "Could not resolve to a Repository" error. The numeric-host disambiguation that already routed `pr://<N>/diff` through the diff path now applies to both schemes, so `issue://<N>/diff[/…]` falls through to the existing "Issue views do not have a diff; use pr://<owner>/<repo>/<n>/diff for pull requests." rejection — matching the long-form `issue://<owner>/<repo>/<N>/diff` behavior. `<scheme>://<owner>/diff` listings for a repo literally named `diff` are unchanged.
45
+ - Fixed PR unified diff parsing so changed-file headers with quoted paths (such as paths containing spaces) are now detected correctly and hunk content lines beginning with `---`/`+++` are counted in additions/deletions
46
+ - Fixed GitHub view caching to account for active credential identity and avoid serving cached issue/PR data across different account/token contexts
47
+ - Fixed `read` call tracking so calls without an explicit path or URL target no longer appear as regular file reads in the execution tracker
48
+ - Fixed `createAgentSession()` subscribing the `credential_disabled` bridge to a freshly discovered `AuthStorage` orphan when an embedder supplied only `options.modelRegistry` (no `options.authStorage`). Refresh failures emitted by `modelRegistry.getApiKey()` flow through `modelRegistry.authStorage`, so a divergent local instance silently swallowed every disable event and also leaked into the `mcpManager` and session result. The SDK now reconciles `authStorage` to `modelRegistry.authStorage` up front and rejects mismatched `options.authStorage`/`options.modelRegistry.authStorage` pairs at session construction.
49
+ - Fixed `runSubagent` (subagent task executor) carrying the same latent `AuthStorage`/`ModelRegistry` divergence as `createAgentSession()`: when only `options.modelRegistry` was supplied, the executor previously fell through to a fresh `discoverAuthStorage()` and handed that orphan into `createAgentSession()` alongside a registry whose `.authStorage` was a different instance. The executor now reconciles to `modelRegistry.authStorage` before any further work and rejects mismatched `options.authStorage`/`options.modelRegistry.authStorage` pairs the same way the SDK does, so subagents can no longer silently observe a different storage view than their parent.
50
+ - Fixed `github` tool's `search_issues`/`search_prs`/`search_code`/`search_commits`/`search_repos` ops always returning 0 results when the query contained more than one qualifier (e.g. `is:merged is:pr`, `is:open author:foo`). `gh search …` since the `advanced_search=true` rollout in gh 2.92 silently wraps multi-token positional queries in parentheses and quotes everything after the first qualifier as that qualifier's value (`is:"merged is:pr"`), which GitHub then matches as a literal state filter that no PR can satisfy. The tool now calls `gh api -X GET /search/<endpoint> -f q=… -F per_page=…` directly so the qualifiers reach GitHub's search API verbatim. `is:issue`/`is:pr` and `repo:<owner>/<repo>` are appended internally to preserve the previous CLI-flag behavior; the user-facing query string in the formatted output is unchanged. `state` for merged PRs is derived from `pull_request.merged_at` so the rendered `State:` line stays `merged`/`closed`/`open` as before.
51
+ - Fixed `read` tool renderer rendering failed reads with a success check (`✓`) and styling the error message as file content while the surrounding box was red. The renderer now branches on `isError` for both file and URL paths: header shows `✘ Read <path>` with a proper error icon and the underlying message is rendered as an error line. `renderReadUrlResult` got the same treatment so failed URL reads also get the cross icon instead of falling through to the `"No response data"` Text fallback. Mirrors the `bash`/`find` renderer error pattern.
52
+ - Fixed ACP mode to advertise and handle non-TUI builtin slash commands and `/skill:<name>` commands
53
+ - Fixed ACP `session/resume` and `session/close` to dispatch correctly under SDK 0.21 by renaming `unstable_resumeSession` / `unstable_closeSession` to the stable `resumeSession` / `closeSession` method names the SDK now routes to
54
+ - Fixed ACP `tool_call` / `tool_call_update` `locations` to always emit absolute paths (resolved against the session cwd) so editor clients can reliably open or focus the referenced file
55
+ - Fixed ACP edit `diff` metadata for moves to point at the destination path rather than the now-deleted source so post-edit "open file" actions land on the new file
56
+ - Fixed ACP `session/request_permission` `locations` to be absolute and to honor the `requestPermission` capability bit instead of only checking for the method, matching the read/write/bash capability gating
57
+ - Fixed ACP `authenticate` to reject `methodId` values that were not advertised by `initialize` so malformed clients fail fast instead of being treated as authenticated
58
+ - Fixed ACP mode changes made via `session/set_session_config_option` (`MODE_CONFIG_ID`) to also emit a `current_mode_update` notification, matching `session/set_mode` so clients tracking `modes.currentModeId` stay in sync
59
+ - Fixed `/model` ACP builtin to emit a `config_option_update` after switching models so clients show the new model in config selectors immediately
60
+ - Fixed `/mcp list` (ACP) to redact query strings and userinfo from server URLs before emitting them, so API keys embedded in URLs (e.g. `?exaApiKey=…`) are not leaked to clients
61
+ - Fixed `/mcp test|resources|prompts` (ACP) to wire the auth storage before `prepareConfig` so OAuth-backed MCP servers can refresh tokens and inject `Authorization` headers
62
+ - Fixed `/mcp list`, `/mcp test`, `/mcp resources`, `/mcp prompts`, `/mcp enable`, and `/mcp disable` (ACP) to preserve project-over-user precedence when the same server name is defined in both scopes, matching the runtime capability merge so toggling the duplicated name flips the effective entry
63
+ - Fixed `/ssh add --port` parsing to reject non-integer values (e.g. `22oops`) instead of silently coercing them via `Number.parseInt`
64
+ - Fixed `/ssh list` to deduplicate hosts shared between project and user scopes, listing project entries first to match capability-loader precedence
65
+ - Fixed `/export` (ACP) to reject clipboard aliases (`--copy`, `clipboard`, `copy`) instead of using them as the output filename
66
+ - Fixed ACP builtin commands (`/compact`, `/force`, `/move`, `/browser`) to surface underlying failures via `output()` instead of swallowing them
67
+ - Fixed `/session save|delete` (ACP) to route through the active `SessionManager` so the persist writer is consulted and stale storage references are removed
68
+ - Fixed `/reload-plugins`, `/marketplace install|uninstall|upgrade`, and `/plugins enable|disable` (ACP) to refresh slash command registries and emit `available_commands_update` after plugin state changes
69
+ - Fixed ACP `usage()` text emission to be awaited so help and error output is not dropped or reordered when commands return immediately
70
+ - Fixed ACP `bash` tool to release the client terminal handle on `terminal/output` or `waitForExit` failures, and to race output polling against abort so a stuck RPC cannot delay cancellation
71
+ - Fixed ACP `resource` content blocks with `image/*` MIME types to be routed into the LLM `images` array instead of being dropped as opaque blobs
72
+ - Fixed `pr://` and `issue://` URLs accepting empty, `.`, or `..` path segments. `pr://owner//77`, `pr://owner/repo/77/diff//2`, and `pr://owner/../77/diff` previously slipped past the `.filter(Boolean)` split and were forwarded to `gh`; now they throw `Invalid <scheme>:// URL: empty or unsafe path segment` before any subprocess work.
73
+ - Fixed `read` of `issue://` / `pr://` URLs ignoring the read tool's `AbortSignal`. Aborting a long `pr://<N>/diff/all` or stale issue fetch now propagates into the resolver and short-circuits at the handler entry; previously the `gh` round-trip and cache write ran to completion.
74
+ - Fixed `read <path>:raw` (and the `raw: true` arg) still rendering markdown internal-URL content through the formatted markdown renderer. The TUI now respects the raw selector and falls back to the code-cell renderer so verbatim bytes are shown when requested.
75
+ - Fixed `eval` tool JS cells crashing with a `structuredClone` error when an awaited final expression returned a non-cloneable value (module namespace, function, symbol, etc.). `displayValue` now falls back to a text representation and logs at debug instead of throwing.
76
+ - Fixed `eval` tool JS rewriter missing the final expression when followed by trailing empty statements (e.g. `await Promise.resolve(1);;`). `returnFinalExpression` now scans backward past `EmptyStatement` nodes before deciding there is no final expression.
77
+ - Fixed `github` view cache evicting valid rows on open whenever a longer-than-default `github.cache.hardTtlSec` was configured. `openDb()` no longer sweeps with the 7-day default before settings load; the per-lookup `sweepIfDue()` enforces the configured retention exclusively.
78
+ - Fixed extension `ctx.shutdown()` being a no-op in the primary interactive path. The handler in `initHooksAndCustomTools` now sets `shutdownRequested` (mirroring the backgrounded-reinit path), and the main REPL loop drains the flag at the post-stream idle boundary so queued steering messages still flush before teardown.
79
+ - Fixed plan-mode "Approve and compact context" dispatching queued user input against the stale plan-mode reference path. `setPlanReferencePath(finalPlanFilePath)` now runs before `handleCompactCommand` flushes the compaction queue, so any message typed during compaction is delivered with the approved plan context attached.
80
+ - Fixed `.omp/commands/fix-issues.md` and `.omp/commands/review-prs.md` still instructing agents to call the removed `github issue_view` / `pr_view` / `pr_diff` ops; they now reference `read issue://<N>` and `read pr://<N>[/diff[/all|<i>]]`.
81
+ - Fixed `ExtensionRunner.initialize()` flushing buffered `credential_disabled` events before mode controllers had a chance to register their `onError` listener. Mode controllers call `runner.initialize(...)` immediately followed by `runner.onError(...)` synchronously; the flush now runs in a microtask after splicing the buffer, so a synchronously throwing `credential_disabled` handler is routed through the registered error listener instead of being silently dropped.
82
+
83
+ ### Security
84
+
85
+ - Secured the GitHub cache store with strict file permissions (`0600` files) and private permissions for newly created cache directories (`0700`) to reduce local cache exposure
86
+
87
+ ## [14.9.9] - 2026-05-12
88
+
89
+ ### Added
90
+
91
+ - Added new `task.isolation.mode` values `auto`, `apfs`, `btrfs`, `zfs`, `reflink`, `overlayfs`, `projfs`, `block-clone`, and `rcopy` for native PAL-backed task isolation backends
92
+ - Added automatic PAL-backed isolation backend selection so `task.isolation.mode` uses the host's best-available backend
93
+ - Added input-token and output-token totals to `omp stats --summary`.
94
+
95
+ ### Changed
96
+
97
+ - Changed `task.isolation.enabled=true` migration to map to `task.isolation.mode = "auto"` instead of legacy `worktree` isolation
98
+ - Updated isolation configuration UI labels and descriptions to expose new back-end names (`overlayfs`, `projfs`, etc.) and removed references to deprecated values in guidance text
99
+
100
+ ### Fixed
101
+
102
+ - Fixed worktree delta capture to include previously untracked file state by baselining untracked patches for both snapshots
103
+ - Fixed task isolation startup to try alternate PAL backends when the preferred one is unavailable, allowing successful fallback instead of immediate failure
104
+ - Mapped legacy `task.isolation.mode` values `worktree`, `fuse-overlay`, and `fuse-projfs` to their new equivalents during settings migration to preserve behavior with older configs
105
+
5
106
  ## [14.9.8] - 2026-05-12
6
107
 
7
108
  ### Breaking Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.9.8",
4
+ "version": "15.0.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -47,12 +47,12 @@
47
47
  "@agentclientprotocol/sdk": "0.21.0",
48
48
  "@babel/parser": "^7.29.3",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/omp-stats": "14.9.8",
51
- "@oh-my-pi/pi-agent-core": "14.9.8",
52
- "@oh-my-pi/pi-ai": "14.9.8",
53
- "@oh-my-pi/pi-natives": "14.9.8",
54
- "@oh-my-pi/pi-tui": "14.9.8",
55
- "@oh-my-pi/pi-utils": "14.9.8",
50
+ "@oh-my-pi/omp-stats": "15.0.0",
51
+ "@oh-my-pi/pi-agent-core": "15.0.0",
52
+ "@oh-my-pi/pi-ai": "15.0.0",
53
+ "@oh-my-pi/pi-natives": "15.0.0",
54
+ "@oh-my-pi/pi-tui": "15.0.0",
55
+ "@oh-my-pi/pi-utils": "15.0.0",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@sinclair/typebox": "^0.34.49",
58
58
  "@types/turndown": "5.0.6",
@@ -40,6 +40,17 @@ async function main(): Promise<void> {
40
40
  "--root",
41
41
  "../..",
42
42
  "./src/cli.ts",
43
+ // Worker entrypoints. Bun's `--compile` discovers the literal in
44
+ // `new Worker("…", …)` at each spawn site, but only actually
45
+ // emits the worker into the bunfs root when it is listed here as
46
+ // an explicit additional entry. Paths are relative to this
47
+ // script's cwd (packages/coding-agent) and the `--root` above
48
+ // (../..) makes them appear inside the binary at
49
+ // `/$bunfs/root/packages/<pkg>/src/<worker>.js`, which is
50
+ // exactly what the literals at the spawn sites resolve to.
51
+ "../stats/src/sync-worker.ts",
52
+ "./src/tools/browser/tab-worker-entry.ts",
53
+ "./src/eval/js/worker-entry.ts",
43
54
  "--outfile",
44
55
  "dist/omp",
45
56
  ],
@@ -25,7 +25,7 @@ const PROMPT_DIRS = [PROMPTS_DIR, COMMIT_PROMPTS_DIR, AGENTIC_PROMPTS_DIR];
25
25
  const PROMPT_FORMAT_OPTIONS = {
26
26
  renderPhase: "pre-render",
27
27
  replaceAsciiSymbols: true,
28
- boldRfc2119Keywords: true,
28
+ normalizeRfc2119: true,
29
29
  } as const;
30
30
 
31
31
  async function main() {
package/src/cli/args.ts CHANGED
@@ -7,7 +7,7 @@ import chalk from "chalk";
7
7
  import { parseEffort } from "../thinking";
8
8
  import { BUILTIN_TOOLS } from "../tools";
9
9
 
10
- export type Mode = "text" | "json" | "rpc" | "acp";
10
+ export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui";
11
11
 
12
12
  export interface Args {
13
13
  cwd?: string;
@@ -79,7 +79,7 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
79
79
  result.allowHome = true;
80
80
  } else if (arg === "--mode" && i + 1 < args.length) {
81
81
  const mode = args[++i];
82
- if (mode === "text" || mode === "json" || mode === "rpc" || mode === "acp") {
82
+ if (mode === "text" || mode === "json" || mode === "rpc" || mode === "acp" || mode === "rpc-ui") {
83
83
  result.mode = mode;
84
84
  }
85
85
  } else if (arg === "--continue" || arg === "-c") {
@@ -176,6 +176,8 @@ async function printStatsSummary(): Promise<void> {
176
176
  console.log(` Requests: ${formatNumber(overall.totalRequests)} (${formatNumber(overall.failedRequests)} errors)`);
177
177
  console.log(` Error Rate: ${formatPercent(overall.errorRate)}`);
178
178
  console.log(` Total Tokens: ${formatNumber(overall.totalInputTokens + overall.totalOutputTokens)}`);
179
+ console.log(` Input Tokens: ${formatNumber(overall.totalInputTokens)}`);
180
+ console.log(` Output Tokens: ${formatNumber(overall.totalOutputTokens)}`);
179
181
  console.log(` Cache Rate: ${formatPercent(overall.cacheRate)}`);
180
182
  console.log(` Total Cost: ${formatCost(overall.totalCost)}`);
181
183
  console.log(` Premium Requests: ${formatNumber(normalizePremiumRequests(overall.totalPremiumRequests ?? 0))}`);
package/src/cli.ts CHANGED
@@ -50,6 +50,7 @@ process.title = APP_NAME;
50
50
 
51
51
  const commands: CommandEntry[] = [
52
52
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
53
+ { name: "acp", load: () => import("./commands/acp").then(m => m.default) },
53
54
  { name: "agents", load: () => import("./commands/agents").then(m => m.default) },
54
55
  { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
55
56
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
@@ -84,8 +85,30 @@ function isSubcommand(first: string | undefined): boolean {
84
85
  return commands.some(e => e.name === first || e.aliases?.includes(first));
85
86
  }
86
87
 
88
+ /**
89
+ * Smoke-test entry. Spawns the stats sync worker, pings it, exits.
90
+ *
91
+ * Purpose: catch the silent worker-load regressions that hit compiled
92
+ * binaries (issues #1011 and #1027). Neither `--version` nor
93
+ * `stats --summary` actually spawns a Worker on a fresh install — the
94
+ * sync path early-returns when no session files exist. This probe is the
95
+ * minimal end-to-end test that proves `new Worker(...)` resolves and the
96
+ * bundled worker module evaluates successfully. Wired into
97
+ * `scripts/install-tests/run-ci.sh` so binary / source-link / tarball
98
+ * installs all exercise it on every CI run.
99
+ */
100
+ async function runSmokeTest(): Promise<void> {
101
+ const { smokeTestSyncWorker } = await import("@oh-my-pi/omp-stats");
102
+ await smokeTestSyncWorker();
103
+ process.stdout.write("smoke-test: ok\n");
104
+ }
105
+
87
106
  /** Run the CLI with the given argv (no `process.argv` prefix). */
88
- export function runCli(argv: string[]): Promise<void> {
107
+ export async function runCli(argv: string[]): Promise<void> {
108
+ if (argv[0] === "--smoke-test") {
109
+ await runSmokeTest();
110
+ return;
111
+ }
89
112
  // --help and --version are handled by run() directly, don't rewrite those.
90
113
  // Everything else that isn't a known subcommand routes to "launch".
91
114
  const first = argv[0];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Run Oh My Pi as an ACP (Agent Client Protocol) server over stdio.
3
+ *
4
+ * Thin wrapper around the launch flow that forces `mode: "acp"` unless the
5
+ * ACP terminal-auth flag asks the same command to open the interactive TUI.
6
+ */
7
+ import { Command } from "@oh-my-pi/pi-utils/cli";
8
+ import { parseArgs } from "../cli/args";
9
+ import { runRootCommand } from "../main";
10
+ import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
11
+
12
+ export default class Acp extends Command {
13
+ static description = "Run Oh My Pi as an ACP (Agent Client Protocol) server over stdio";
14
+ static strict = false;
15
+
16
+ async run(): Promise<void> {
17
+ const { args, terminalAuth } = prepareAcpTerminalAuthArgs(this.argv);
18
+ const parsed = parseArgs(args);
19
+ if (!terminalAuth) {
20
+ parsed.mode = "acp";
21
+ }
22
+ await runRootCommand(parsed, args);
23
+ }
24
+ }
@@ -7,6 +7,7 @@ import { APP_NAME } from "@oh-my-pi/pi-utils";
7
7
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
8
8
  import { parseArgs } from "../cli/args";
9
9
  import { runRootCommand } from "../main";
10
+ import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
10
11
 
11
12
  export default class Index extends Command {
12
13
  static description = "AI coding assistant";
@@ -49,8 +50,8 @@ export default class Index extends Command {
49
50
  description: "Allow starting in ~ without auto-switching to a temp dir",
50
51
  }),
51
52
  mode: Flags.string({
52
- description: "Output mode: text (default), json, or rpc",
53
- options: ["text", "json", "rpc"],
53
+ description: "Output mode: text (default), json, rpc, or rpc-ui",
54
+ options: ["text", "json", "rpc", "acp", "rpc-ui"],
54
55
  }),
55
56
  print: Flags.boolean({
56
57
  char: "p",
@@ -135,7 +136,8 @@ export default class Index extends Command {
135
136
  static strict = false;
136
137
 
137
138
  async run(): Promise<void> {
138
- const parsed = parseArgs(this.argv);
139
- await runRootCommand(parsed, this.argv);
139
+ const { args } = prepareAcpTerminalAuthArgs(this.argv);
140
+ const parsed = parseArgs(args);
141
+ await runRootCommand(parsed, args);
140
142
  }
141
143
  }
@@ -34,5 +34,5 @@ Tool guidance:
34
34
 
35
35
  ## Changelog Requirements
36
36
 
37
- If changelog targets provided, you **MUST** call `propose_changelog` before finishing.
37
+ If changelog targets provided, you MUST call `propose_changelog` before finishing.
38
38
  If you propose split commit plan, include changelog target files in relevant commit changes.
@@ -956,6 +956,36 @@ export async function resolveModelScope(
956
956
  return scopedModels;
957
957
  }
958
958
 
959
+ /**
960
+ * Resolve the set of models a session is allowed to use, given the active
961
+ * settings. Starts from `modelRegistry.getAvailable()` (so disabled providers
962
+ * and providers without credentials are already filtered out) and, when
963
+ * `enabledModels` is configured for the current path scope, further restricts
964
+ * the result to models matching those patterns.
965
+ *
966
+ * Returns the unfiltered available list when `enabledModels` is empty.
967
+ * Returns an empty list when `enabledModels` is configured but no available
968
+ * model matches any pattern — callers MUST treat this as "no usable model"
969
+ * rather than falling back to the global default (see issue #1022).
970
+ */
971
+ export async function resolveAllowedModels(
972
+ modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
973
+ settings: Settings | undefined,
974
+ preferences?: ModelMatchPreferences,
975
+ ): Promise<Model<Api>[]> {
976
+ const available = modelRegistry.getAvailable();
977
+ const patterns = settings?.get("enabledModels");
978
+ if (!patterns || patterns.length === 0) {
979
+ return available;
980
+ }
981
+ const scoped = await resolveModelScope(patterns, modelRegistry, preferences);
982
+ if (scoped.length === 0) {
983
+ return [];
984
+ }
985
+ const allowed = new Set(scoped.map(entry => `${entry.model.provider}/${entry.model.id}`));
986
+ return available.filter(model => allowed.has(`${model.provider}/${model.id}`));
987
+ }
988
+
959
989
  export interface ResolveCliModelResult {
960
990
  model: Model<Api> | undefined;
961
991
  selector?: string;
@@ -1864,6 +1864,37 @@ export const SETTINGS_SCHEMA = {
1864
1864
  },
1865
1865
  },
1866
1866
 
1867
+ "github.cache.enabled": {
1868
+ type: "boolean",
1869
+ default: true,
1870
+ ui: {
1871
+ tab: "tools",
1872
+ label: "GitHub view cache",
1873
+ description: "Cache rendered issue/PR view output in ~/.omp/cache/github-cache.db so repeated reads are free",
1874
+ },
1875
+ },
1876
+
1877
+ "github.cache.softTtlSec": {
1878
+ type: "number",
1879
+ default: 300,
1880
+ ui: {
1881
+ tab: "tools",
1882
+ label: "GitHub cache soft TTL (seconds)",
1883
+ description: "Within this window, cached issue/PR view rows are returned directly. Default 5 minutes.",
1884
+ },
1885
+ },
1886
+
1887
+ "github.cache.hardTtlSec": {
1888
+ type: "number",
1889
+ default: 604800,
1890
+ ui: {
1891
+ tab: "tools",
1892
+ label: "GitHub cache hard TTL (seconds)",
1893
+ description:
1894
+ "Past soft TTL but within hard TTL, the tool returns the cached row and refreshes it in the background. Past hard TTL, the row is dropped. Default 7 days.",
1895
+ },
1896
+ },
1897
+
1867
1898
  "web_search.enabled": {
1868
1899
  type: "boolean",
1869
1900
  default: true,
@@ -2067,25 +2098,46 @@ export const SETTINGS_SCHEMA = {
2067
2098
  // Delegation
2068
2099
  "task.isolation.mode": {
2069
2100
  type: "enum",
2070
- values: ["none", "worktree", "fuse-overlay", "fuse-projfs"] as const,
2101
+ values: [
2102
+ "none",
2103
+ "auto",
2104
+ "apfs",
2105
+ "btrfs",
2106
+ "zfs",
2107
+ "reflink",
2108
+ "overlayfs",
2109
+ "projfs",
2110
+ "block-clone",
2111
+ "rcopy",
2112
+ ] as const,
2071
2113
  default: "none",
2072
2114
  ui: {
2073
2115
  tab: "tasks",
2074
2116
  label: "Isolation Mode",
2075
2117
  description:
2076
- "Isolation mode for subagents (none, git worktree, fuse-overlayfs on Unix, or ProjFS on Windows via fuse-projfs; unsupported modes fall back to worktree)",
2118
+ 'Isolation backend for subagents. "auto" lets the native PAL pick the best available backend (CoW-aware filesystems, then overlayfs/ProjFS, then a git worktree / recursive-copy fallback).',
2077
2119
  options: [
2078
2120
  { value: "none", label: "None", description: "No isolation" },
2079
- { value: "worktree", label: "Worktree", description: "Git worktree isolation" },
2121
+ { value: "auto", label: "Auto", description: "Let the PAL pick the best available backend" },
2122
+ { value: "apfs", label: "APFS", description: "macOS clonefile reflink (APFS)" },
2123
+ { value: "btrfs", label: "btrfs", description: "btrfs subvolume snapshot" },
2124
+ { value: "zfs", label: "ZFS", description: "ZFS snapshot + clone" },
2125
+ { value: "reflink", label: "Reflink", description: "Linux FICLONE per-file reflink" },
2126
+ {
2127
+ value: "overlayfs",
2128
+ label: "Overlayfs",
2129
+ description: "Linux kernel overlay (or fuse-overlayfs fallback)",
2130
+ },
2131
+ { value: "projfs", label: "ProjFS", description: "Windows Projected File System" },
2080
2132
  {
2081
- value: "fuse-overlay",
2082
- label: "Fuse Overlay",
2083
- description: "COW overlay via fuse-overlayfs (Unix only)",
2133
+ value: "block-clone",
2134
+ label: "Block clone",
2135
+ description: "Windows FSCTL_DUPLICATE_EXTENTS_TO_FILE (NTFS/ReFS)",
2084
2136
  },
2085
2137
  {
2086
- value: "fuse-projfs",
2087
- label: "Fuse ProjFS",
2088
- description: "COW overlay via ProjFS (Windows only; falls back to worktree if unavailable)",
2138
+ value: "rcopy",
2139
+ label: "Recursive copy",
2140
+ description: "git worktree if available, otherwise recursive copy",
2089
2141
  },
2090
2142
  ],
2091
2143
  },
@@ -598,11 +598,28 @@ export class Settings {
598
598
  const isolationObj = taskObj?.isolation as Record<string, unknown> | undefined;
599
599
  if (isolationObj && "enabled" in isolationObj) {
600
600
  if (typeof isolationObj.enabled === "boolean") {
601
- isolationObj.mode = isolationObj.enabled ? "worktree" : "none";
601
+ isolationObj.mode = isolationObj.enabled ? "auto" : "none";
602
602
  }
603
603
  delete isolationObj.enabled;
604
604
  }
605
605
 
606
+ // task.isolation.mode: legacy values from before the pi-iso PAL refactor.
607
+ // `worktree` was git worktree → now lives under `rcopy`. `fuse-overlay`
608
+ // and `fuse-projfs` are now the platform-named `overlayfs` / `projfs`
609
+ // kinds; the PAL falls back internally when the chosen one isn't
610
+ // available, so we don't need the old TS-side platform guards.
611
+ if (isolationObj && typeof isolationObj.mode === "string") {
612
+ const legacy: Record<string, string> = {
613
+ worktree: "rcopy",
614
+ "fuse-overlay": "overlayfs",
615
+ "fuse-projfs": "projfs",
616
+ };
617
+ const mapped = legacy[isolationObj.mode as string];
618
+ if (mapped !== undefined) {
619
+ isolationObj.mode = mapped;
620
+ }
621
+ }
622
+
606
623
  // edit.mode: removed "atom" variant is now "hashline"
607
624
  const editObj = raw.edit as Record<string, unknown> | undefined;
608
625
  if (editObj) {
package/src/edit/index.ts CHANGED
@@ -145,13 +145,15 @@ async function executeApplyPatchPerFile(
145
145
  const result = await run(batchRequest);
146
146
  const details = result.details;
147
147
  perFileResults.push({
148
- path,
148
+ path: details?.path ?? path,
149
149
  diff: details?.diff ?? "",
150
150
  firstChangedLine: details?.firstChangedLine,
151
151
  diagnostics: details?.diagnostics,
152
152
  op: details?.op,
153
153
  move: details?.move,
154
154
  meta: details?.meta,
155
+ oldText: details?.oldText,
156
+ newText: details?.newText,
155
157
  });
156
158
  const text = result.content?.find(c => c.type === "text")?.text ?? "";
157
159
  if (text) contentTexts.push(text);
@@ -205,6 +207,11 @@ async function executeSinglePathEntries(
205
207
  const diffTexts: string[] = [];
206
208
  let firstChangedLine: number | undefined;
207
209
  let errorCount = 0;
210
+ let metadataPath: string | undefined;
211
+ let hasFirstOldText = false;
212
+ let firstOldText: string | undefined;
213
+ let hasLastNewText = false;
214
+ let lastNewText: string | undefined;
208
215
 
209
216
  for (let i = 0; i < runs.length; i++) {
210
217
  const isLast = i === runs.length - 1;
@@ -217,6 +224,17 @@ async function executeSinglePathEntries(
217
224
  const details = result.details;
218
225
  if (details?.diff) diffTexts.push(details.diff);
219
226
  firstChangedLine ??= details?.firstChangedLine;
227
+ if (details?.path) {
228
+ metadataPath ??= details.path;
229
+ }
230
+ if (details && "oldText" in details && !hasFirstOldText) {
231
+ firstOldText = details.oldText;
232
+ hasFirstOldText = true;
233
+ }
234
+ if (details && "newText" in details) {
235
+ lastNewText = details.newText;
236
+ hasLastNewText = true;
237
+ }
220
238
  const text = result.content?.find(c => c.type === "text")?.text ?? "";
221
239
  if (text) contentTexts.push(text);
222
240
  } catch (err) {
@@ -242,6 +260,9 @@ async function executeSinglePathEntries(
242
260
  details: {
243
261
  diff: diffTexts.join("\n"),
244
262
  firstChangedLine,
263
+ path: metadataPath ?? path,
264
+ ...(hasFirstOldText ? { oldText: firstOldText } : {}),
265
+ ...(hasLastNewText ? { newText: lastNewText } : {}),
245
266
  },
246
267
  // Any per-entry failure marks the aggregate result as an error so the
247
268
  // renderer takes the error branch instead of falling through to the
@@ -1772,15 +1772,25 @@ export async function executePatchSingle(
1772
1772
  .diagnostics(mergedDiagnostics?.summary ?? "", mergedDiagnostics?.messages ?? [])
1773
1773
  .get();
1774
1774
 
1775
+ const oldText = result.change.type !== "create" ? result.change.oldContent : undefined;
1776
+ const newText = result.change.type !== "delete" ? result.change.newContent : undefined;
1777
+
1775
1778
  return {
1776
1779
  content: [{ type: "text", text: resultText }],
1777
1780
  details: {
1778
1781
  diff: diffResult.diff,
1782
+ // When the patch moves the file, anchor the diff to the destination
1783
+ // path. ACP `ToolCallContent.diff.path` comes from this field, and
1784
+ // clients use it to open or focus the file post-change; pointing at
1785
+ // the (now-deleted) source navigates to nothing.
1786
+ path: result.change.newPath ?? resolvedPath,
1779
1787
  firstChangedLine: diffResult.firstChangedLine,
1780
1788
  diagnostics: mergedDiagnostics,
1781
1789
  op,
1782
1790
  move: effectiveRename,
1783
1791
  meta,
1792
+ oldText,
1793
+ newText,
1784
1794
  },
1785
1795
  };
1786
1796
  }
@@ -1094,9 +1094,12 @@ export async function executeReplaceSingle(
1094
1094
  content: [{ type: "text", text: resultText }],
1095
1095
  details: {
1096
1096
  diff: diffResult.diff,
1097
+ path: absolutePath,
1097
1098
  firstChangedLine: diffResult.firstChangedLine,
1098
1099
  diagnostics,
1099
1100
  meta,
1101
+ oldText: rawContent,
1102
+ newText: finalContent,
1100
1103
  },
1101
1104
  };
1102
1105
  }
@@ -55,6 +55,10 @@ export interface EditToolPerFileResult {
55
55
  * Set when the underlying error carries a `displayMessage` (e.g. {@link HashlineMismatchError}). */
56
56
  displayErrorText?: string;
57
57
  meta?: OutputMeta;
58
+ /** Source-of-truth content before the edit; `undefined` for create operations. */
59
+ oldText?: string;
60
+ /** Source-of-truth content after the edit; `undefined` for delete operations. */
61
+ newText?: string;
58
62
  }
59
63
 
60
64
  export interface EditToolDetails {
@@ -72,6 +76,12 @@ export interface EditToolDetails {
72
76
  meta?: OutputMeta;
73
77
  /** Per-file results (multi-file edits) */
74
78
  perFileResults?: EditToolPerFileResult[];
79
+ /** Absolute file path for single-file edit results. Required by ACP diff metadata consumers. */
80
+ path?: string;
81
+ /** Source-of-truth content before the edit; `undefined` for create operations. */
82
+ oldText?: string;
83
+ /** Source-of-truth content after the edit; `undefined` for delete operations. */
84
+ newText?: string;
75
85
  }
76
86
 
77
87
  // ═══════════════════════════════════════════════════════════════════════════
@@ -287,7 +287,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
287
287
  sections = splitHashlineInputs(args.input, { cwd: ctx.cwd, path: args.path });
288
288
  } catch {
289
289
  // Single-section fallback keeps the original error rendering for the
290
- // "haven't typed `@PATH` yet" case.
290
+ // "haven't typed `@@ PATH` yet" case.
291
291
  const result = await computeHashlineDiff({ input: args.input, path: args.path }, ctx.cwd, {
292
292
  autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
293
293
  });