Skip to content

Commit 1efa8f8

Browse files
committed
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.
1 parent 91aa31e commit 1efa8f8

3 files changed

Lines changed: 57 additions & 47 deletions

File tree

src/agents/tool-display-exec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import { asOptionalObjectRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
77
import { redactToolPayloadText } from "../logging/redact.js";
8-
import { sliceUtf16Safe } from "../utils.js";
8+
import { sliceUtf16Safe } from "../shared/text/surrogate-safe-slice.js";
99
import {
1010
binaryName,
1111
firstPositional,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Browser-safe UTF-16 surrogate-aware slice/truncate helpers.
3+
*
4+
* These have no Node-only dependencies so they can be imported from
5+
* browser-shared Control UI code paths (e.g. the tool-display modules) without
6+
* pulling `node:fs`/`node:os`/`node:path` into the Vite bundle. `src/utils.ts`
7+
* re-exports these for the Node-side callers that already import from there.
8+
*/
9+
10+
function isHighSurrogate(codeUnit: number): boolean {
11+
return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
12+
}
13+
14+
function isLowSurrogate(codeUnit: number): boolean {
15+
return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
16+
}
17+
18+
/** Slices a UTF-16 string without returning dangling surrogate halves at either edge. */
19+
export function sliceUtf16Safe(input: string, start: number, end?: number): string {
20+
const len = input.length;
21+
22+
let from = start < 0 ? Math.max(len + start, 0) : Math.min(start, len);
23+
let to = end === undefined ? len : end < 0 ? Math.max(len + end, 0) : Math.min(end, len);
24+
25+
if (to < from) {
26+
const tmp = from;
27+
from = to;
28+
to = tmp;
29+
}
30+
31+
if (from > 0 && from < len) {
32+
const codeUnit = input.charCodeAt(from);
33+
if (isLowSurrogate(codeUnit) && isHighSurrogate(input.charCodeAt(from - 1))) {
34+
from += 1;
35+
}
36+
}
37+
38+
if (to > 0 && to < len) {
39+
const codeUnit = input.charCodeAt(to - 1);
40+
if (isHighSurrogate(codeUnit) && isLowSurrogate(input.charCodeAt(to))) {
41+
to -= 1;
42+
}
43+
}
44+
45+
return input.slice(from, to);
46+
}
47+
48+
/** Truncates a UTF-16 string without cutting a surrogate pair in half. */
49+
export function truncateUtf16Safe(input: string, maxLen: number): string {
50+
const limit = Math.max(0, Math.floor(maxLen));
51+
if (input.length <= limit) {
52+
return input;
53+
}
54+
return sliceUtf16Safe(input, 0, limit);
55+
}

src/utils.ts

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -69,52 +69,7 @@ export function sleep(ms: number) {
6969
});
7070
}
7171

72-
function isHighSurrogate(codeUnit: number): boolean {
73-
return codeUnit >= 0xd800 && codeUnit <= 0xdbff;
74-
}
75-
76-
function isLowSurrogate(codeUnit: number): boolean {
77-
return codeUnit >= 0xdc00 && codeUnit <= 0xdfff;
78-
}
79-
80-
/** Slices a UTF-16 string without returning dangling surrogate halves at either edge. */
81-
export function sliceUtf16Safe(input: string, start: number, end?: number): string {
82-
const len = input.length;
83-
84-
let from = start < 0 ? Math.max(len + start, 0) : Math.min(start, len);
85-
let to = end === undefined ? len : end < 0 ? Math.max(len + end, 0) : Math.min(end, len);
86-
87-
if (to < from) {
88-
const tmp = from;
89-
from = to;
90-
to = tmp;
91-
}
92-
93-
if (from > 0 && from < len) {
94-
const codeUnit = input.charCodeAt(from);
95-
if (isLowSurrogate(codeUnit) && isHighSurrogate(input.charCodeAt(from - 1))) {
96-
from += 1;
97-
}
98-
}
99-
100-
if (to > 0 && to < len) {
101-
const codeUnit = input.charCodeAt(to - 1);
102-
if (isHighSurrogate(codeUnit) && isLowSurrogate(input.charCodeAt(to))) {
103-
to -= 1;
104-
}
105-
}
106-
107-
return input.slice(from, to);
108-
}
109-
110-
/** Truncates a UTF-16 string without cutting a surrogate pair in half. */
111-
export function truncateUtf16Safe(input: string, maxLen: number): string {
112-
const limit = Math.max(0, Math.floor(maxLen));
113-
if (input.length <= limit) {
114-
return input;
115-
}
116-
return sliceUtf16Safe(input, 0, limit);
117-
}
72+
export { sliceUtf16Safe, truncateUtf16Safe } from "./shared/text/surrogate-safe-slice.js";
11873

11974
/** Resolves `~` and OpenClaw home-relative paths with injectable env/home sources. */
12075
export function resolveUserPath(

0 commit comments

Comments
 (0)