Skip to content

ACP: completed tool_call_update diff content is derived from tool input schema, not result metadata — write/patch and plugin edit tools never emit diffs #52636

Closed

Description

@gaojunran

Description

In v2, the ACP layer builds the tool_call_update (completed) diff content block only from top-level input.oldString/input.newString:

packages/cli/src/acp/tool.ts (verified on 2.0.21), completedToolUpdate:

const oldText = stringValue(input.input.oldString)
const newText = stringValue(input.input.newString)
const diff: ToolCallContent[] =
  oldText === undefined || newText === undefined
    ? [] : [{ type: "diff", path: ..., oldText, newText }]

So the diff content block is emitted only when the tool's input schema happens to have top-level oldString/newString. Consequences:

  1. The built-in write tool ({filePath, content}) never emits a diff content block.
  2. The built-in patch tool ({path, patchText}) never emits one either (v1 had the same gap for apply_patch, fixed in [FEATURE]: ACP: Send diff on apply_patch #30913 — this regressed with the v2 rewrite).
  3. Any plugin that replaces/wraps edit with a different schema (e.g. a batch edits: [{oldString, newString, ...}] array) never emits one.

Clients that render per-turn diffs solely from that block show nothing. Zed is affected this way — its inline diff cards come from the diff content (also confirmed in the discussion on #52466; client-side context in zed-industries/zed#54918).

The data is already there: tools report their changes in result metadata as typed FileDiff.Info ({file, patch, additions, deletions, status} — packages/schema/src/file-diff.ts), and the ACP layer already consumes that field elsewhere (permission.ts syncEditedFiles reads metadata.files). Only completedToolUpdate ignores it and guesses from the input schema instead.

Suggested fix: derive the diff content from result metadata (metadata.files[] / unified patch) rather than the input schema. The unified-patch → per-file oldText/newText conversion already exists in permission.ts patchPreviews (Patch.parse + Patch.derive) and could be reused here.

Plugins

@cortexkit/aft-opencode 0.58.0 (reproduced with and without it — the bug is not plugin-specific, but the plugin's batch-edit schema makes it visible for edit too)

OpenCode version

2.0.21 (opencode acp, v2 line: packages/cli/src/acp/)

Steps to reproduce

  1. Run opencode acp with Zed (or any ACP client that renders diff content).
    2a. Prompt the agent to use write to create/modify a file → the completed tool_call_update contains only a text item, no {type: "diff"} block → no per-turn diff in Zed.
    2b. With a plugin registering an edit tool whose schema is {path, edits: [{oldString, newString}]} (instead of top-level fields), the same happens for edits.
  2. For contrast, the built-in edit with top-level oldString/newString does emit the diff block — confirming the mapping depends on the input schema rather than the actual change.

Verified by capturing the ACP traffic directly: completed update content is ['content', 'diff'] with the built-in edit, and ['content'] for write and for the plugin edit, while rawOutput.metadata contains the full unified patch in all three cases.

Operating System

macOS 15.5

Activity

  1. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

    This issue might overlap with a previously reported ACP diff issue. Please check:

    • #30913: Requested ACP diff content for apply_patch; this issue reports the same missing diff for patch tools in v2, following the earlier v1 fix.
  2. gaojunran commented on Oct 2, 2026

    @gaojunran
    Author

    Follow-up clarifying scope (verified empirically by capturing ACP traffic + reading tool results):

    • Built-in edit/write/patch already report their changes in result metadata as metadata.files[] (FileDiff.Info[] with full patches) — no tool-side change needed for them. Fixing completedToolUpdate to derive diff content from that field restores diffs for all three at once.

    • The wrinkle is plugin tools: tool metadata is not documented as a contract, so plugins invent their own fields. The plugin I reproduced this with (a batch-edit edit replacement) reports diffs as metadata.diff (unified patch string) + metadata.filediff (array with patches) instead of metadata.files[] — so a fix reading only metadata.files[] would still leave plugin tools without diffs.

    Suggested as two parts:

    1. ACP layer derives diff content from metadata.files[] (fixes the built-in tools).
    2. Formalize files: FileDiff.Info[] as the documented tool-result metadata contract for reporting file changes (or expose a helper on the Tool API for it), so plugin authors can comply. Until plugins adopt it, their tools would still lack diff content — but that becomes plugin conformance rather than an ACP bug.
  3. neriousy commented on Oct 20, 2026

    @neriousy
    Member

    Thanks for the detailed report! Starting in 2.0.25, ACP builds completed tool diffs from file-change metadata, restoring write and patch diffs without depending on the tool's input schema; plugin tools should report changes through metadata.files too. Update with opencode upgrade or npm i -g @opencode/cli@latest (desktop: update the app). Please comment or reopen if this still happens on that version or newer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions