Skip to content

apply_patch forces a trailing newline onto files that lacked one #45912

Open

Description

@skyzhao1223

Describe the bug

packages/core/src/patch.ts derive() unconditionally appends a trailing newline to the result: if (updated.at(-1) !== "") updated.push("").

So apply_patch on a file that lacks a trailing newline adds one even when the patch doesn't touch the last line. patchFile then diffs before (no trailing newline) against after (with one), reporting a change the model never requested.

Repro: derive("f", [{ oldLines: ["line1"], newLines: ["LINE1"] }], "line1\nline2\nline3") → "LINE1\nline2\nline3\n".

Question

Is the forced trailing newline intentional (normalize EOF newline), or should the result preserve whether the original file ended with a newline?

Environment

  • opencode version: latest dev

Activity

  1. github-actions commented on Aug 28, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of (or closely related to) an existing issue. Please check:

  2. maplemuse commented on Sep 1, 2026

    @maplemuse

    I work in a large repo where we have a .editorconfig setting which prohibits trailing newlines at the end of the file. When we use our formatter, it then triggers on every file apply_patch is applied to, which adds significant churn. Is there a way to ignore changes to the end of the file if none were made, or to otherwise follow repository conventions?

  3. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the clear report. I could reproduce this.

    Steps: a project with f.txt containing line1\nline2\nline3 and no final newline, and a simulated GPT model that calls the patch tool to change only line1 to LINE1. After the patch:

    • the tool's own "Patched f.txt" diff shows line3 removed and added again;
    • git diff shows \ No newline at end of file only on the old side;
    • od -c shows the file now ends with 3 \n.

    The recording and screenshots below show this, and the script below reproduces it. It happens on 2.2.0 and still happens on the latest dev (506ac20). The cause is the line in derive() (now in packages/util/src/patch.ts) that always adds a final newline, whatever the original file ended with.

    Related:

    recording.mp4

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#45912: the patch (apply_patch) tool adds a trailing
    // newline to a file that had none, even though the patch only edits line 1.
    // Run: OPENCODE_DRIVE_MEDIA_DIR=/workspace/media opencode-drive run drive.ts
    // (set OC_DEV=/path/to/opencode/checkout to run from source)
    import { Effect, Stream } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const patchText = "*** Begin Patch\n*** Update File: f.txt\n@@\n-line1\n+LINE1\n*** End Patch"
    const check = "git diff f.txt; od -c f.txt"
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true },
        // f.txt has NO trailing newline
        project: { git: true, files: { "f.txt": "line1\nline2\nline3" } },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          let step = 0
          yield* llm.serve((request: any) => {
            if (!request.body?.tools?.length || JSON.stringify(request.body?.messages ?? "").includes("You name conversations")) return Stream.make(Llm.text("Patch EOF newline")) // title request
            step++
            if (step === 1)
              return Stream.make(Llm.toolCall({ index: 0, id: "call_patch", name: "patch", input: { patchText } }), Llm.finish("tool-calls"))
            if (step === 2)
              return Stream.make(
                Llm.toolCall({ index: 0, id: "call_check", name: "shell", input: { command: check, description: "Show diff and bytes" } }),
                Llm.finish("tool-calls"),
              )
            return Stream.make(Llm.text("Done checking."))
          })
          yield* ui.submit("Uppercase line1 in f.txt")
          yield* ui.waitFor("Done checking.")
          yield* Effect.sleep("2 seconds")
          yield* ui.screenshot("after-patch")
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions