diff --git a/extensions/opencode/session-catalog.test.ts b/extensions/opencode/session-catalog.test.ts index 785d9821e853..bffa750ef465 100644 --- a/extensions/opencode/session-catalog.test.ts +++ b/extensions/opencode/session-catalog.test.ts @@ -86,6 +86,7 @@ function captureOpenCodeSessionRegistrations(pluginConfig: unknown = {}) { async function installFakeOpenCode( assistantText = "hi", sessionTitle = "Catalog session", + toolInput: unknown = { command: "pwd" }, ): Promise { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-opencode-catalog-")); temporaryDirectories.push(directory); @@ -125,7 +126,7 @@ async function installFakeOpenCode( id: "prt_tool", type: "tool", tool: "bash", - state: { status: "completed", input: { command: "pwd" }, output: "/workspace" }, + state: { status: "completed", input: toolInput, output: "/workspace" }, }, ], }, @@ -299,6 +300,25 @@ describe("OpenCode session catalog", () => { }, ); + it.runIf(process.platform !== "win32")( + "keeps truncated tool input on a valid UTF-16 boundary", + async () => { + await installFakeOpenCode("hi", "Catalog session", { + value: `${"x".repeat(19_989)}🎉`, + }); + const transcript = await readLocalOpenCodeTranscriptPage({ + threadId: "ses_test", + limit: 20, + }); + const toolCall = transcript.items.find((item) => item.type === "toolCall"); + + expect(toolCall?.text).toMatch(/…$/u); + expect(toolCall?.text).not.toMatch( + /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { diff --git a/extensions/opencode/session-catalog.ts b/extensions/opencode/session-catalog.ts index f748460018b9..c7342ef33e84 100644 --- a/extensions/opencode/session-catalog.ts +++ b/extensions/opencode/session-catalog.ts @@ -6,6 +6,7 @@ import type { SessionsCatalogReadResult, } from "openclaw/plugin-sdk/session-catalog"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { materializeWindowsSpawnProgram, resolveWindowsSpawnProgram, @@ -352,7 +353,7 @@ export async function listLocalOpenCodeSessionPage(value?: unknown): Promise maxLength ? `${text.slice(0, maxLength)}…` : text; + return text.length > maxLength ? `${truncateUtf16Safe(text, maxLength)}…` : text; } catch { return undefined; }