From 205ab8d4bd7a9347bba5cecd5dd9de6c87a5e327 Mon Sep 17 00:00:00 2001 From: Anson_H <153708448+yyzquwu@users.noreply.github.com> Date: Sat, 13 Jun 2026 06:54:53 -0700 Subject: [PATCH] perf(terminal): reuse ANSI truncation scanner Reuse one module-level ANSI/OSC scanner during visible-width truncation and reset scanner state between calls. Keeps styled, plain, and OSC-8 truncation behavior covered by regression tests. --- packages/terminal-core/src/ansi.test.ts | 9 +++++++++ packages/terminal-core/src/ansi.ts | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/terminal-core/src/ansi.test.ts b/packages/terminal-core/src/ansi.test.ts index 2b5f86a03b3..f43061776a7 100644 --- a/packages/terminal-core/src/ansi.test.ts +++ b/packages/terminal-core/src/ansi.test.ts @@ -60,4 +60,13 @@ describe("terminal ansi helpers", () => { expect(truncateToVisibleWidth("表文", 1)).toBe(""); expect(visibleWidth(truncateToVisibleWidth("表文", 1))).toBe(0); }); + + it("reuses the ANSI scanner across truncation calls", () => { + expect(truncateToVisibleWidth("\u001B[31mabc\u001B[0m", 2)).toBe("\u001B[31mab\u001B[0m"); + expect(truncateToVisibleWidth("plain", 3)).toBe("pla"); + expect( + truncateToVisibleWidth("\u001B]8;;https://openclaw.ai\u001B\\link\u001B]8;;\u001B\\", 2), + ).toBe("\u001B]8;;https://openclaw.ai\u001B\\li\u001B]8;;\u001B\\"); + expect(truncateToVisibleWidth("\u001B[32mxy\u001B[0m", 1)).toBe("\u001B[32mx\u001B[0m"); + }); }); diff --git a/packages/terminal-core/src/ansi.ts b/packages/terminal-core/src/ansi.ts index bb5c164d87c..0739344df7a 100644 --- a/packages/terminal-core/src/ansi.ts +++ b/packages/terminal-core/src/ansi.ts @@ -6,6 +6,7 @@ const ANSI_OSC_PATTERN = "\\x1b\\][^\\x07\\x1b]*(?:\\x1b\\\\|\\x07)"; const ANSI_CSI_REGEX = new RegExp(ANSI_CSI_PATTERN, "g"); const ANSI_OSC_REGEX = new RegExp(ANSI_OSC_PATTERN, "g"); +const ANSI_SEQUENCE_REGEX = new RegExp(`${ANSI_OSC_PATTERN}|${ANSI_CSI_PATTERN}`, "g"); const graphemeSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) @@ -134,7 +135,7 @@ export function truncateToVisibleWidth(input: string, maxWidth: number): string if (visibleWidth(input) <= maxWidth) { return input; } - const ansi = new RegExp(`${ANSI_OSC_PATTERN}|${ANSI_CSI_PATTERN}`, "g"); + ANSI_SEQUENCE_REGEX.lastIndex = 0; let out = ""; let used = 0; let pos = 0; @@ -157,7 +158,7 @@ export function truncateToVisibleWidth(input: string, maxWidth: number): string } }; let match: RegExpExecArray | null; - while ((match = ansi.exec(input)) !== null) { + while ((match = ANSI_SEQUENCE_REGEX.exec(input)) !== null) { appendVisible(input.slice(pos, match.index)); out += match[0]; pos = match.index + match[0].length;