mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-29 00:13:36 +00:00
fix(agents): truncate exec command detail on code-point boundaries (#96963)
* fix(agents): truncate exec command detail on code-point boundaries compactRawCommand in src/agents/tool-display-exec.ts middle-truncated the one-line command with raw String.prototype.slice. When the head or tail boundary fell between the two UTF-16 code units of a surrogate pair (e.g. an emoji like U+1F600), the slice kept a lone surrogate, which renders as the replacement character in the tool-call summary shown in chat/transcripts. Use the existing sliceUtf16Safe helper for both ends so the boundary falls on a code-point boundary, dropping the whole emoji instead of half of it. This is behavior-preserving for non-surrogate input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: move surrogate-safe slice helpers to browser-safe module ClawSweeper flagged that importing sliceUtf16Safe from src/utils.ts into tool-display-exec.ts pulls node:fs/os/path into the Control UI browser-shared bundle (ui/vite.config.ts treats tool-display-exec.ts as browser-shared). Move sliceUtf16Safe/truncateUtf16Safe into a self-contained, dependency-free module src/shared/text/surrogate-safe-slice.ts. src/utils.ts re-exports them (zero churn for existing Node-side callers), and tool-display-exec.ts now imports directly from the node-free module so no Node built-ins can leak into the browser bundle. * fix(agents): use shared utf16 helper in exec display --------- Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { asOptionalObjectRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { redactToolPayloadText } from "../logging/redact.js";
|
||||
import { sliceUtf16Safe } from "../shared/utf16-slice.js";
|
||||
import {
|
||||
binaryName,
|
||||
firstPositional,
|
||||
@@ -442,7 +443,7 @@ function compactRawCommand(raw: string, maxLength = 120): string {
|
||||
return oneLine;
|
||||
}
|
||||
const half = Math.floor((maxLength - 1) / 2);
|
||||
return `${oneLine.slice(0, half)}…${oneLine.slice(-(maxLength - 1 - half))}`;
|
||||
return `${sliceUtf16Safe(oneLine, 0, half)}…${sliceUtf16Safe(oneLine, -(maxLength - 1 - half))}`;
|
||||
}
|
||||
|
||||
export type ToolDetailMode = "explain" | "raw";
|
||||
|
||||
@@ -562,6 +562,28 @@ describe("compactRawCommand middle truncation", () => {
|
||||
expect(result).not.toContain("AKIDABCDEFGHIJKLMNOP1234567890");
|
||||
expect(result).toContain("AKIDAB…7890");
|
||||
});
|
||||
|
||||
it("does not split a surrogate pair when the head boundary lands on an emoji", () => {
|
||||
// The one-line form is 140 UTF-16 units. With the default maxLength=120 the head
|
||||
// slice ends at index 59, but the 😀 emoji (U+1F600, a surrogate pair) occupies
|
||||
// indices 58-59 — so a raw .slice(0, 59) would keep the high surrogate and drop
|
||||
// its low half, leaving a lone surrogate that renders as the replacement char.
|
||||
const emoji = String.fromCodePoint(0x1f600);
|
||||
// Unknown binary so resolveExecDetail returns the compact raw form directly.
|
||||
const longCommand = `/opt/custom/bin/run ${"a".repeat(38)}${emoji}${"b".repeat(80)}`;
|
||||
const result = resolveExecDetail({ command: longCommand });
|
||||
|
||||
expect(result).toBeDefined();
|
||||
// The whole emoji is dropped at the boundary rather than half of it.
|
||||
expect(result).not.toContain(emoji);
|
||||
// No dangling/lone surrogate code units remain in the rendered detail.
|
||||
expect(result).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/);
|
||||
expect(result).not.toMatch(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/);
|
||||
// Start and end of the command are still preserved around the ellipsis.
|
||||
expect(result).toContain("/opt/custom/bin/run");
|
||||
expect(result).toContain("…");
|
||||
expect(result).toMatch(/b{4}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("coerceDisplayValue middle truncation", () => {
|
||||
|
||||
Reference in New Issue
Block a user