Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/src/file-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ const layer = Layer.effect(
const current = yield* fs
.readFile(input.target.canonical)
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
// ANRCODE_CHANGE {"issue":368,"branch":"anr/368/strip-bom-ai-content","date":"2026-07-30"}
yield* fs.writeWithDirs(
input.target.canonical,
joinBom(next.text, Boolean(current && hasUtf8Bom(current)) || next.bom),
joinBom(next.text, Boolean(current && hasUtf8Bom(current))),
)
return writeResult(input.target, current !== undefined)
}),
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export function derive(path: string, chunks: ReadonlyArray<UpdateFileChunk>, ori
for (const [start, remove, insert] of replacements.toReversed()) updated.splice(start, remove, ...insert)
if (updated.at(-1) !== "") updated.push("")
const next = splitBom(updated.join("\n"))
return { content: next.text, bom: source.bom || next.bom }
// ANRCODE_CHANGE {"issue":368,"branch":"anr/368/strip-bom-ai-content","date":"2026-07-30"}
return { content: next.text, bom: source.bom }
}

export function joinBom(text: string, bom: boolean) {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,12 @@ const layer = Layer.effectDiscard(
{ additions: 0, deletions: 0 },
)
const next = splitBom(replaced)
// ANRCODE_CHANGE {"issue":368,"branch":"anr/368/strip-bom-ai-content","date":"2026-07-30"}
const result = yield* unableToEdit(
files.writeIfUnchanged({
target,
expected: source.content,
content: joinBom(next.text, source.bom || next.bom),
content: joinBom(next.text, source.bom),
}),
)
return {
Expand Down
24 changes: 22 additions & 2 deletions packages/core/test/file-mutation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe("FileMutation", () => {
),
)

it.live("preserves exactly one BOM for text writes and normalizes created text", () =>
it.live("preserves BOM from existing files but strips BOM from new content", () =>
withTmp((directory) =>
Effect.gen(function* () {
const preservedPath = path.join(directory, "preserved.txt")
Expand All @@ -84,7 +84,27 @@ describe("FileMutation", () => {
yield* files.writeTextPreservingBom({ target: created, content: "\uFEFF\uFEFF\uFEFFcreated" })

expect(yield* Effect.promise(() => fs.readFile(preservedPath, "utf8"))).toBe("\uFEFFafter")
expect(yield* Effect.promise(() => fs.readFile(created.canonical, "utf8"))).toBe("\uFEFFcreated")
expect(yield* Effect.promise(() => fs.readFile(created.canonical, "utf8"))).toBe("created")
}).pipe(provide(directory)),
),
)

it.live("strips BOM from AI-generated content in new files (Issue #368)", () =>
withTmp((directory) =>
Effect.gen(function* () {
const files = yield* FileMutation.Service
const mutation = yield* LocationMutation.Service
const testPath = path.join(directory, "ai-generated.txt")
const target = yield* mutation.resolve({ path: "ai-generated.txt" })

// AI generates content with BOM
const aiGeneratedContent = "\uFEFF# Test Script\necho 'Hello'\n"
yield* files.writeTextPreservingBom({ target, content: aiGeneratedContent })

// File should not have BOM
const result = yield* Effect.promise(() => fs.readFile(testPath, "utf8"))
expect(result).toBe("# Test Script\necho 'Hello'\n")
expect(result.startsWith("\uFEFF")).toBe(false)
}).pipe(provide(directory)),
),
)
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/patch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,11 @@ export function deriveNewContentsFromChunks(
// Generate unified diff
const unifiedDiff = generateUnifiedDiff(originalContent.text, newContent)

// ANRCODE_CHANGE {"issue":368,"branch":"anr/368/strip-bom-ai-content","date":"2026-07-30"}
return {
unified_diff: unifiedDiff,
content: newContent,
bom: originalContent.bom || next.bom,
bom: originalContent.bom,
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ export const EditTool = Tool.define(
)
}
const next = Bom.split(params.newString)
const desiredBom = next.bom
// ANRCODE_CHANGE {"issue":368,"branch":"anr/368/strip-bom-ai-content","date":"2026-07-30"}
const desiredBom = false
contentOld = ""
contentNew = next.text
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
Expand Down Expand Up @@ -131,7 +132,8 @@ export const EditTool = Tool.define(
const replacement = convertToLineEnding(normalizeLineEndings(params.newString), ending)

const next = Bom.split(replace(contentOld, old, replacement, params.replaceAll))
const desiredBom = source.bom || next.bom
// ANRCODE_CHANGE {"issue":368,"branch":"anr/368/strip-bom-ai-content","date":"2026-07-30"}
const desiredBom = source.bom
contentNew = next.text

diff = trimDiff(
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const WriteTool = Tool.define(
const exists = yield* fs.existsSafe(filepath)
const source = exists ? yield* Bom.readFile(fs, filepath) : { bom: false, text: "" }
const next = Bom.split(params.content)
const desiredBom = source.bom || next.bom
// ANRCODE_CHANGE {"issue":368,"branch":"anr/368/strip-bom-ai-content","date":"2026-07-30"}
const desiredBom = source.bom
const contentOld = source.text
const contentNew = next.text

Expand Down
Loading