fix(tasks): keep emoji / surrogate pairs intact during terminal output truncation (#101600)

* fix(tasks): keep emoji / surrogate pairs intact during terminal output truncation

.slice() counts UTF-16 code units; use Array.from() to count full
code points instead, preventing lone surrogates in task list output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tasks): preserve terminal column width

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
maweibin
2026-07-07 19:54:53 +08:00
committed by GitHub
parent cd0d570164
commit 4d5cd05a64
2 changed files with 27 additions and 4 deletions

View File

@@ -24,6 +24,7 @@ import type { OpenClawTestState } from "../test-utils/openclaw-test-state.js";
import {
tasksAuditCommand,
tasksCancelCommand,
tasksListCommand,
tasksMaintenanceCommand,
tasksShowCommand,
} from "./tasks.js";
@@ -601,6 +602,30 @@ describe("tasks commands", () => {
});
});
it("keeps task list summaries within their UTF-16 column limit", async () => {
await withTaskCommandStateDir(async () => {
createTaskRecord({
runtime: "cli",
ownerKey: "agent:main:main",
scopeKind: "session",
runId: "task-utf16-summary",
status: "succeeded",
task: "Inspect task summary",
terminalSummary: `${"y".repeat(78)}🚀xx`,
});
const runtime = createRuntime();
await tasksListCommand({}, runtime);
const output = vi
.mocked(runtime.log)
.mock.calls.map(([line]) => String(line))
.join("\n");
expect(output).toContain(`${"y".repeat(78)}`);
expect(output).not.toContain("🚀");
});
});
it("explains retained lost task cleanup timing in maintenance text output", async () => {
await withTaskCommandStateDir(async () => {
const cleanupAfter = Date.now() + 60_000;

View File

@@ -3,6 +3,7 @@
import { timestampMsToIsoString } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { isRich, theme } from "../../packages/terminal-core/src/theme.js";
import { formatCliCommand } from "../cli/command-format.js";
import { formatLookupMiss } from "../cli/error-format.js";
@@ -205,10 +206,7 @@ function truncate(value: string, maxChars: number) {
if (value.length <= maxChars) {
return value;
}
if (maxChars <= 1) {
return value.slice(0, maxChars);
}
return `${value.slice(0, maxChars - 1)}`;
return maxChars <= 0 ? "" : `${truncateUtf16Safe(value, maxChars - 1)}`;
}
function shortToken(value: string | undefined, maxChars = ID_PAD): string {