diff --git a/src/app/formatters/summary-formatter.ts b/src/app/formatters/summary-formatter.ts index 7d83f2237..8f37e8843 100644 --- a/src/app/formatters/summary-formatter.ts +++ b/src/app/formatters/summary-formatter.ts @@ -359,6 +359,10 @@ function formatDiff(diff: string): string { return formattedLines.join("\n"); } +// Unlabelled .txt attachments are guessed at by the reader; without this mark Notepad +// and Android browsers fall back to the system code page and render UTF-8 as mojibake +const UTF8_BOM = "\uFEFF"; + export function prepareCodeFile( content: string, filePath: string, @@ -386,7 +390,7 @@ export function prepareCodeFile( : t("tool.file_header.edit", { path: displayPath }); const fullContent = header + processedContent; - const buffer = Buffer.from(fullContent, "utf8"); + const buffer = Buffer.from(UTF8_BOM + fullContent, "utf8"); const basename = path.basename(filePath); const filename = `${operation}_${basename}.txt`; diff --git a/tests/bot/messages/summary-message-formatter.test.ts b/tests/bot/messages/summary-message-formatter.test.ts index 76154a35a..fc43a25d1 100644 --- a/tests/bot/messages/summary-message-formatter.test.ts +++ b/tests/bot/messages/summary-message-formatter.test.ts @@ -349,6 +349,22 @@ describe("bot/messages/summary-message-formatter", () => { expect(oversized).toBeNull(); }); + it("starts file payloads with a UTF-8 BOM so readers do not guess the system code page", () => { + const writeFile = prepareCodeFile("const x = 1;", "src/app.ts", "write"); + expect(writeFile?.buffer.subarray(0, 3).toString("hex")).toBe("efbbbf"); + expect(writeFile?.buffer.subarray(3).toString("utf8")).toBe( + "Write File/Path: src/app.ts\n============================================================\n\nconst x = 1;", + ); + + const editFile = prepareCodeFile("@@ -1,1 +1,1 @@\n-line1\n+line2", "src/app.ts", "edit"); + expect(editFile?.buffer.subarray(0, 3).toString("hex")).toBe("efbbbf"); + expect(editFile?.buffer.subarray(3).toString("utf8")).toContain("Edit File/Path: src/app.ts"); + + const cyrillic = prepareCodeFile('const привет = "мир";', "src/app.ts", "write"); + expect(cyrillic?.buffer.subarray(0, 3).toString("hex")).toBe("efbbbf"); + expect(cyrillic?.buffer.subarray(3).toString("utf8")).toContain('const привет = "мир";'); + }); + it("normalizes absolute paths to project-relative form", () => { const writeText = formatToolInfo({ sessionId: "s1",