From 5e37c9490689f9de322e07fafc9b87ac6d4cca70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E8=B6=8A=E7=BE=BD=E6=AF=9B?= <97326386+Icather@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:04:29 +0800 Subject: [PATCH] fix(web): gate ANSI filtering behind resumeParam and reuse TextDecoder Addresses Copilot review: - Reuse a single TextDecoder instance instead of allocating per message - Only filter erase codes during session resume (resumeParam != null) - Extract filter chain into named helper sanitizeResumeOutput() Refs #47313 --- web/src/pages/ChatPage.tsx | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/web/src/pages/ChatPage.tsx b/web/src/pages/ChatPage.tsx index ee1c894465542..3c119c70b23a2 100644 --- a/web/src/pages/ChatPage.tsx +++ b/web/src/pages/ChatPage.tsx @@ -989,18 +989,24 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) { } }; + // Session resume: strip ANSI codes that Ink's two-pass virtual + // scroll unmount emits into the PTY stream (blank-line bursts, + // erase-line, erase-char). Gate on resumeParam so short/new + // sessions never see the filter. + const decoder = new TextDecoder(); + const sanitizeResumeOutput = (raw: string): string => + resumeParam + ? raw.replace(/\n{3,}/g, "\n\n") + .replace(/\x1b\[\d*K/g, "") + .replace(/\x1b\[\d*X/g, "") + : raw; + ws.onmessage = (ev) => { - let text: string; - if (typeof ev.data === "string") { - text = ev.data; - } else { - text = new TextDecoder().decode(new Uint8Array(ev.data as ArrayBuffer)); - } - term.write( - text.replace(/\n{3,}/g, "\n\n") - .replace(/\x1b\[\d*K/g, "") - .replace(/\x1b\[\d*X/g, "") - ); + const text = + typeof ev.data === "string" + ? ev.data + : decoder.decode(new Uint8Array(ev.data as ArrayBuffer)); + term.write(sanitizeResumeOutput(text)); }; ws.onclose = (ev) => {