Skip to content

feat(cli): preserve original text encoding when editing files - #8587

Merged
chrarnoldus merged 34 commits into
mainfrom
feat/encoding-preservation
Apr 23, 2026
Merged

chrarnoldus merged 34 commits into
mainfrom
feat/encoding-preservation

Conversation

@kilo-code-bot

@kilo-code-bot kilo-code-bot Bot commented Apr 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds encoding detection and preservation to all agent file tools (read, edit, write, apply_patch, patch module)
  • Uses jschardet (v3.1.4) for statistical encoding detection and iconv-lite (v0.7.2) for encode/decode
  • Supports all major encodings: UTF-8/16/32, Shift-JIS, EUC-JP, GB2312, Big5, EUC-KR, Windows-1251, KOI8-R, ISO-8859 family, and many more
  • Previously every tool assumed UTF-8, which would corrupt files in any other encoding and display garbled text when reading

Changes

New files

  • packages/opencode/src/kilocode/encoding.ts — Encoding namespace with detect/decode/encode/read/write functions. BOM detection is explicit; statistical detection delegates to jschardet; encode/decode uses iconv-lite
  • packages/opencode/test/kilocode/encoding.test.ts — 34 tests covering BOM detection, CJK round-trips (Shift-JIS, EUC-JP, Big5, GB2312, EUC-KR), Cyrillic round-trips (Windows-1251, KOI8-R), and file I/O preservation

Modified files

  • packages/opencode/src/util/filesystem.ts — Added readEncoded() and writeEncoded() helpers
  • packages/opencode/src/tool/read.ts — Uses encoding-aware decoding so non-UTF-8 files display correctly; binary detection updated to not falsely flag UTF-16 or CJK-encoded files
  • packages/opencode/src/tool/edit.ts — Uses encoding-aware read/write to preserve file encoding during edits
  • packages/opencode/src/tool/write.ts — Uses encoding-aware read/write to preserve existing file encoding when overwriting
  • packages/opencode/src/tool/apply_patch.ts — Uses encoding-aware read/write for all patch operations
  • packages/opencode/src/patch/index.ts — deriveNewContentsFromChunks now returns encoding info; applyHunksToFiles uses encoding-aware writes
  • packages/opencode/package.json — Added jschardet and iconv-lite dependencies

How it works

  1. Files are read as raw bytes
  2. BOM signatures are checked first (UTF-8, UTF-16 LE/BE, UTF-32 LE/BE)
  3. For non-BOM files, jschardet performs statistical analysis to detect the encoding (Shift-JIS, GB2312, Big5, EUC-KR, Windows-1251, etc.)
  4. ASCII detection is normalized to UTF-8 (superset)
  5. The detected encoding flows through the tool pipeline
  6. On write, iconv-lite re-encodes to the original encoding with BOM restored if originally present
  7. New files default to UTF-8 (no BOM)
  8. ReadTool decodes files with the detected encoding so the LLM sees correct text
  9. Binary detection now checks encoding first — UTF-16 files with null bytes and CJK files are no longer falsely rejected

Introduce kilocode Encoding module and integrate into IO paths
implement readEncoded and writeEncoded in filesystem for
encoding-aware file IO
propagate encoding through patching workflows (patch, apply_patch, edit, write)
add tests for encoding utilities
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
return { encoding: "utf-16be", bom: true }
}
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: UTF-32 LE BOM is misdetected as UTF-16 LE

Files that start with FF FE 00 00 also satisfy this branch, so decode() / encode() will rewrite a UTF-32 file as UTF-16 and corrupt it. Check the 4-byte UTF-32 BOM before returning UTF-16 here, or reject unsupported UTF-32 explicitly so the tool does not silently rewrite it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a289f94 — the UTF-16 LE BOM check now explicitly tests whether bytes 2–3 are both zero and skips the match if so, preventing the UTF-32 LE BOM (FF FE 00 00) from being returned as UTF-16 LE.

@kilo-code-bot

kilo-code-bot Bot commented Apr 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 5
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/kilocode/encoding.ts 35 UTF-32 LE BOM is no longer returned immediately as UTF-16 LE, but unsupported UTF-32 still falls into later detection paths and can be rewritten incorrectly.
packages/opencode/src/kilocode/encoding.ts 49 The new UTF-32 guard only recognizes some UTF-32 layouts, so short BOM-less UTF-32 files or BOM-less non-ASCII UTF-32 can still fall through and be rewritten as UTF-16.
packages/opencode/src/kilocode/encoding.ts 71 The BOM-less UTF-16 heuristic still classifies some unsupported UTF-32 byte patterns as UTF-16 LE, so editing UTF-32 content remains corrupting in edge cases.
packages/opencode/src/tool/read.ts 149 ReadTool now decodes the entire file before applying offset/limit/output caps, which can blow up memory on very large text files.
packages/opencode/src/tool/read.ts 282 Binary detection now trusts heuristic charset guesses enough to let some real binary files be decoded as text.
Other Observations (not in diff)

No additional issues found outside the diff.

Files Reviewed (54 files)
  • packages/opencode/src/kilocode/encoding.ts - 3 issues
  • packages/opencode/src/tool/read.ts - 2 issues
  • packages/kilo-vscode/src/KiloProvider.ts - 0 issues
  • packages/kilo-vscode/src/agent-manager/PRStatusPoller.ts - 0 issues
  • packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts - 0 issues
  • Remaining changed files reviewed with no additional high-confidence issues

Reviewed by gpt-5.4-20260305 · 2,559,141 tokens

…tion

Address review bot warning: FF FE 00 00 (UTF-32 LE BOM) was being
misdetected as UTF-16 LE. Now checks whether the two bytes after FF FE
are both zero and skips the UTF-16 LE match if so.
Check for 4-byte null patterns before the 2-byte UTF-16 heuristic to
prevent UTF-32 LE/BE content from being misclassified.
kilo-code-bot Bot and others added 4 commits April 8, 2026 12:48
Replace hand-rolled BOM/heuristic detection with jschardet (v3.1.4) for
statistical encoding detection and iconv-lite (v0.7.2) for encode/decode.
This adds support for Shift-JIS, EUC-JP, GB2312, Big5, EUC-KR,
Windows-1251, KOI8-R, and all other encodings these libraries support.

BOM detection is still handled explicitly for precise round-trip
fidelity including UTF-32 LE/BE.
…non-Latin files

ReadTool now uses Encoding.detect + iconv-lite to decode files in any
supported encoding (Shift-JIS, GB2312, Big5, EUC-KR, etc.) instead of
hardcoding UTF-8. The binary detection heuristic now checks encoding
first so UTF-16 files with null bytes and CJK encoded files are no
longer falsely rejected as binary.
Comment thread packages/opencode/src/tool/read.ts Resolved
Comment thread packages/opencode/src/tool/read.ts Resolved
@chrarnoldus
chrarnoldus marked this pull request as draft April 9, 2026 16:13
@chrarnoldus
chrarnoldus force-pushed the feat/encoding-preservation branch from 02a819e to 227af07 Compare April 15, 2026 08:36
kilo-code-bot Bot and others added 12 commits April 15, 2026 08:51
…ation

# Conflicts:
#	packages/opencode/src/tool/apply_patch.ts
#	packages/opencode/src/tool/edit.ts
#	packages/opencode/src/tool/read.ts
#	packages/opencode/src/tool/write.ts
Drop manual BOM parsing, UTF-32, and UTF-16-without-BOM detection: rely on
TextDecoder + jschardet + iconv-lite which already handle BOM round-tripping
for UTF-16 LE/BE. Move the encoding helpers and Effect wrappers into
packages/opencode/src/kilocode/ so shared tool files only carry targeted
kilocode_change markers.
Exercise Read/Write/Edit/ApplyPatch tools directly against files in
UTF-8, UTF-16 LE/BE (with BOM), Shift_JIS, EUC-JP, GB2312, Big5, EUC-KR,
Windows-1251, and KOI8-R to verify the tools decode input correctly and
round-trip bytes back to the original encoding. Also ensure UTF-16
BOMs survive the write pipeline.
This reverts commit ce80c09.
- Restore the streaming createInterface loop in the `lines` helper so the
  diff against main stays small; decode bytes with iconv-lite up front and
  feed the resulting text through Readable.from.
- Drop 'new file' kilocode_change markers from files under kilocode/
  directories (those paths are exempt from the annotation check).
- Tighten the changeset copy to sound like a user-facing release note.
kilo-code-bot Bot added 9 commits April 21, 2026 13:32
iconv-lite's utf-8 codec strips any leading BOM on decode and never emits
one on encode, so files that started with EF BB BF would lose their BOM
after an edit. Track UTF-8-with-BOM as a distinct synthetic encoding label
and re-prepend the BOM bytes on write so the round-trip keeps the file
byte-identical (modulo any actual edits).
- Wire Readable.from through a named `stream` variable so the `lines`
  helper can destroy it in the finally block, matching the previous
  createReadStream pattern.
- Restore the 'Create parent directories', 'Handle file move', 'Regular
  update', 'Read original file content', and 'For delete, we need to read
  the current content' comments that were dropped when switching to
  Encoding.write / Encoding.read. The explicit fs.mkdir calls are no
  longer needed because Encoding.write mkdirs recursively, but the intent
  comments still apply.
- Encoding.detect: lowercase-compare UTF-32 rejection so jschardet's
  uppercase 'UTF-32'/'UTF-32BE'/'UTF-32LE' labels no longer slip through
  (iconv-lite happens to have utf-32 codecs that would then be accepted).
- Encoding.read: drop redundant Buffer.from wrap around readFile's
  already-Buffer return value.
- EncodedIO: switch to Effect.tryPromise so I/O failures surface as
  typed errors that .pipe(Effect.catch(...)) can recover from; the
  apply_patch delete branch relies on this to translate read errors
  into 'apply_patch verification failed'.
- Shrink kilocode_change blocks in edit.ts, write.ts, apply_patch.ts,
  patch/index.ts, and read.ts to per-line inline markers where each
  block was only wrapping 1-3 changed lines.
- Collapse the 'if (exists) { let contentOld; let encoding; ... }'
  blocks in edit.ts and write.ts into a single ternary + destructure
  that keeps all three lines self-contained and each independently
  annotated.
- Remove the unused 'latin1' sample and fix the 'providEncoded' typo
  in the encoding integration test file.
- Strengthen the apply_patch Shift_JIS test to assert exact bytes
  instead of the weak 'does not contain these two UTF-8 bytes' check.
- Drop the stray .kilo/ and .kilocode/ lockfiles that were re-added
  by an upstream 'wip' commit.
Per review: CJK and legacy single-byte encodings already passed the old
control-char heuristic (their bytes are all >= 0x80, never NUL), so the
full Encoding.detect pass in isBinaryFile was solving a non-problem for
them. The only realistic regression in the old heuristic is UTF-16 with
BOM, where the second byte of every ASCII character is 0x00 and fires
the NUL-byte early-return.

Replace the detect call with a 2-byte BOM check via a new
Encoding.hasUtf16Bom helper. Also reword the move/add inline markers in
patch/index.ts to note that Encoding.write handles mkdir.
Main moved the Patch namespace out of packages/opencode/src/patch/index.ts
into patch/patch.ts and switched tool imports to the new
@opencode-ai/shared/filesystem and ../../src/tool barrels. Reapply the
encoding-aware read/write changes on top of the refactored patch.ts, and
update the integration test imports to match the new module layout.
- Drop the dead 'encoding = pre.encoding' initial value that was only
  read if deriveNewContentsFromChunks threw, and that code path
  returns before encoding is ever used.
- Inline pre.text into the read line so we don't bind a 'pre' local
  just to destructure it once.
Strip a leading U+FEFF from text before prepending a UTF-8/UTF-16 BOM so
round-tripping content that already carries a BOM character emits exactly
one BOM. Also clarifies the changeset that new files default to UTF-8.
@chrarnoldus
chrarnoldus marked this pull request as ready for review April 23, 2026 12:46
…ation

# Conflicts:
#	packages/opencode/src/patch/patch.ts
#	packages/opencode/src/tool/edit.ts
#	packages/opencode/src/tool/write.ts
@chrarnoldus
chrarnoldus merged commit 529c3a5 into main Apr 23, 2026
15 checks passed
@chrarnoldus
chrarnoldus deleted the feat/encoding-preservation branch April 23, 2026 14:20
jliounis pushed a commit to jliounis/kilocode that referenced this pull request May 18, 2026
…tion

feat(cli): preserve original text encoding when editing files
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
…tion

feat(cli): preserve original text encoding when editing files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants