Repository navigation
Description
Bug
When opencode requests permission for an edit, the toolCall.content block (type: "diff") is silently dropped whenever the edited hunk sits entirely inside an indented block. ACP clients (Zed, VS Code extensions, JetBrains, CodeCompanion, etc.) then have no diff to show next to the approval prompt.
The actual edit is not affected — it still applies correctly via the fuzzy replacers in edit.ts. This is a preview-only regression.
Root cause
edit.ts trims common indentation from the unified diff (trimDiff) for readability of the in-TUI diff. Then permission.ts::diffContentForPatch tries to reconstruct the result by re-applying that (now indentation-stripped) patch to the file:
const next = applyPatch(content, diff) // fails for indented hunks
if (next === false) return undefined // <- no content block emittedapplyPatch returns false because the context lines no longer match the file (their indentation was stripped by trimDiff). With fuzzFactor: 0 (jsdiff default) there is no tolerance, so the hunk can't be located.
Trigger condition (deterministic, not a race)
The bug fires whenever all non-blank lines in the hunk are indented, i.e. the changed line(s) plus the 4-line context window on each side never include a zero-indent line. This is the common case: any edit inside a function/method body more than ~4 lines deep from its signature.
Standalone repro (no LLM needed)
import { applyPatch, createTwoFilesPatch } from "diff"
// trimDiff, copied verbatim from packages/opencode/src/tool/edit.ts
function trimDiff(diff) {
const lines = diff.split("\n")
const contentLines = lines.filter(
(l) => (l.startsWith("+") || l.startsWith("-") || l.startsWith(" ")) &&
!l.startsWith("---") && !l.startsWith("+++"),
)
if (contentLines.length === 0) return diff
let min = Infinity
for (const line of contentLines) {
const c = line.slice(1)
if (c.trim().length > 0) {
const m = c.match(/^(\s*)/); if (m) min = Math.min(min, m[1].length)
}
}
if (min === Infinity || min === 0) return diff
return lines.map((l) =>
(l.startsWith("+") || l.startsWith("-") || l.startsWith(" ")) &&
!l.startsWith("---") && !l.startsWith("+++")
? l[0] + l.slice(1).slice(min) : l,
).join("\n")
}
// Change happens deep inside a function so the hunk's context window
// never reaches a zero-indent line.
const old =
`function compute() {
const v0 = 0
const v1 = 1
const v2 = 2
const v3 = 3
const v4 = 4
const v5 = 5
const v6 = 6
const v7 = 7
const v8 = 8
const v9 = 9
const v10 = 10
return sum
}
`
const next = old.replace(" const v5 = 5", " const v5 = 50")
const raw = createTwoFilesPatch("f", "f", old, next)
const trimmed = trimDiff(raw)
console.log("applyPatch(raw): ", applyPatch(old, raw) === false ? "FAILED" : "OK")
console.log("applyPatch(trimmed): ", applyPatch(old, trimmed) === false ? "FAILED <- no preview" : "OK")Output:
applyPatch(raw): OK
applyPatch(trimmed): FAILED <- no preview
History
trimDiffwas added inc1250abdf("implemented diff trimming") for readability.diffContentForPatch(theapplyPatchreconstruction) was introduced by fix(acp): enrich permission prompts #34079 ("fix(acp): enrich permission prompts"). The two interact badly:trimDiffmakes the patch non-applicable, soapplyPatchfails.
Related
- fix(acp): enrich permission prompts #34079 — introduced the
applyPatchreconstruction (source of the regression) - fix(acp): include diff content block in edit permission requests #31783 (@ReeSilva) — earlier, simpler fix passing
oldString/newStringdirectly; closed in favor of fix(acp): enrich permission prompts #34079 - [FEATURE]: Expose applyable patch in edit permission metadata #31625 — notes that
trimDiffmakes patches non-applicable - fix(acp): edit/write permission requests don't include diff content block #31781 — original "no diff content block" report (closed by fix(acp): enrich permission prompts #34079)
Activity
github-actions commentedon Sep 16, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.