diff --git a/src/StdUtils.ts b/src/StdUtils.ts index 3d9e2159..25ead637 100644 --- a/src/StdUtils.ts +++ b/src/StdUtils.ts @@ -1,4 +1,5 @@ import {Readable, Writable} from "node:stream"; +import {StringDecoder} from "node:string_decoder"; import {Emitter} from "vscode-jsonrpc/node"; import type {DataCallback, Disposable, Message, MessageReader, MessageWriter, PartialMessageInfo} from "vscode-jsonrpc/node"; import * as acp from "@agentclientprotocol/sdk"; @@ -33,8 +34,9 @@ export function createJSONRPCReader(readable: Readable): MessageReader { return { listen(callback: DataCallback): Disposable { let buf = ''; + const decoder = new StringDecoder('utf8'); const onData = (chunk: Buffer) => { - buf += chunk.toString(); + buf += decoder.write(chunk); for (;;) { const i = buf.indexOf('\n'); if (i < 0) break; diff --git a/src/__tests__/StdUtils.test.ts b/src/__tests__/StdUtils.test.ts new file mode 100644 index 00000000..5c151365 --- /dev/null +++ b/src/__tests__/StdUtils.test.ts @@ -0,0 +1,24 @@ +import {PassThrough} from "node:stream"; +import {describe, expect, it, vi} from "vitest"; + +import {createJSONRPCReader} from "../StdUtils"; + +describe("createJSONRPCReader", () => { + it("preserves UTF-8 characters split across stdout chunks", () => { + const stdout = new PassThrough(); + const onMessage = vi.fn(); + createJSONRPCReader(stdout).listen(onMessage); + + const prefix = Buffer.from('{"method":"test","params":{"title":"'); + const firstCharacter = Buffer.from("\u4F60"); + stdout.write(Buffer.concat([prefix, firstCharacter.subarray(0, 2)])); + stdout.write(Buffer.concat([firstCharacter.subarray(2), Buffer.from('\u597D"}}\n')])); + + expect(onMessage).toHaveBeenCalledOnce(); + expect(onMessage).toHaveBeenCalledWith({ + jsonrpc: "2.0", + method: "test", + params: {title: "\u4F60\u597D"}, + }); + }); +});