From 4d5cd05a64bf596b750d0486107d9d2aef96d1ad Mon Sep 17 00:00:00 2001 From: maweibin Date: Tue, 7 Jul 2026 19:54:53 +0800 Subject: [PATCH] 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 * fix(tasks): preserve terminal column width --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: Peter Steinberger --- src/commands/tasks.test.ts | 25 +++++++++++++++++++++++++ src/commands/tasks.ts | 6 ++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/commands/tasks.test.ts b/src/commands/tasks.test.ts index bcb84a2654b9..cbc5f74430d7 100644 --- a/src/commands/tasks.test.ts +++ b/src/commands/tasks.test.ts @@ -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; diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts index 79fbb94d3d51..76e45d65753f 100644 --- a/src/commands/tasks.ts +++ b/src/commands/tasks.ts @@ -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 {