diff --git a/src/acp/event-mapper.test.ts b/src/acp/event-mapper.test.ts index 88650811a53a..8d599b455fd5 100644 --- a/src/acp/event-mapper.test.ts +++ b/src/acp/event-mapper.test.ts @@ -1,6 +1,6 @@ /** Tests ACP tool-call location extraction limits. */ import { describe, expect, it } from "vitest"; -import { extractToolCallLocations } from "./event-mapper.js"; +import { extractToolCallLocations, formatToolTitle } from "./event-mapper.js"; describe("extractToolCallLocations", () => { it("enforces the global node visit cap across nested structures", () => { @@ -18,3 +18,13 @@ describe("extractToolCallLocations", () => { expect(locations).toEqual([{ path: "/tmp/file-0.txt" }, { path: "/tmp/file-1.txt" }]); }); }); + +describe("formatToolTitle", () => { + it("does not split surrogate pairs when truncating argument values", () => { + const title = formatToolTitle("exec", { + command: `${"x".repeat(99)}🚀tail`, + }); + + expect(title).toBe(`exec: command: ${"x".repeat(99)}...`); + }); +}); diff --git a/src/acp/event-mapper.ts b/src/acp/event-mapper.ts index 2862a8ae21cd..3a7b5600d90c 100644 --- a/src/acp/event-mapper.ts +++ b/src/acp/event-mapper.ts @@ -14,6 +14,7 @@ import { normalizeOptionalString, readStringValue, } from "@openclaw/normalization-core/string-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; type GatewayAttachment = { type: string; @@ -309,7 +310,7 @@ export function formatToolTitle( } const parts = Object.entries(args).map(([key, value]) => { const raw = typeof value === "string" ? value : JSON.stringify(value); - const safe = raw.length > 100 ? `${raw.slice(0, 100)}...` : raw; + const safe = raw.length > 100 ? `${truncateUtf16Safe(raw, 100)}...` : raw; return `${key}: ${safe}`; }); // Sanitize at the source so session updates and permission requests never