From 5d22b29838f8ce665fa74527c18b13e26aa0fd92 Mon Sep 17 00:00:00 2001 From: LifeJiggy Date: Tue, 26 May 2026 01:37:33 +0100 Subject: [PATCH] fix(shell): preview Unicode truncation uses byte length instead of char length The preview function used text.length (UTF-16 code units) to check if output exceeds the metadata limit, but the actual truncation should be based on byte length for correct handling of multi-byte characters (CJK, emoji, etc.). Use Buffer.byteLength for the check and ensure truncated output does not split a multi-byte character. --- packages/opencode/src/tool/shell.ts | 7 +++++-- packages/opencode/test/tool/shell.test.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index b6a95b5c0970..a412f760fb3d 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -221,8 +221,11 @@ function pathArgs(list: Part[], ps: boolean, cmd = false) { } function preview(text: string) { - if (text.length <= MAX_METADATA_LENGTH) return text - return "...\n\n" + text.slice(-MAX_METADATA_LENGTH) + if (Buffer.byteLength(text, "utf-8") <= MAX_METADATA_LENGTH) return text + let buf = Buffer.from(text, "utf-8") + buf = buf.subarray(-MAX_METADATA_LENGTH) + while (buf.length > 0 && (buf[0] & 0xc0) === 0x80) buf = buf.subarray(1) + return "...\n\n" + buf.toString("utf-8") } function tail(text: string, maxLines: number, maxBytes: number) { diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index ddaa5c2ec7b1..11bf2397b2d5 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -1227,4 +1227,21 @@ describe("tool.shell truncation", () => { }), ), ) + + it.live("preview keeps valid UTF-8 for multi-byte output", () => + runIn( + projectRoot, + Effect.gen(function* () { + const n = 12000 + const result = yield* run({ + command: `${bin} -e ${evalarg(`process.stdout.write('\\u4e2d'.repeat(${n}))`)} ${n}`, + description: "Generate Unicode output", + }) + const meta = result.metadata as { output?: string } + if (meta.output) { + expect(meta.output.includes("\uFFFD")).toBe(false) + } + }), + ), + ) })