diff --git a/ui-tui/src/__tests__/text.test.ts b/ui-tui/src/__tests__/text.test.ts index ffd8f849da2..566d1e41cf6 100644 --- a/ui-tui/src/__tests__/text.test.ts +++ b/ui-tui/src/__tests__/text.test.ts @@ -97,12 +97,24 @@ describe('ANSI sanitizers', () => { expect(stripAnsi(sample)).toBe('ABCD') }) + it('strips incomplete CSI prefixes and carriage returns', () => { + const sample = `A${ESC}[31mB${ESC}[12;${ESC}[CD\rE` + + expect(stripAnsi(sample)).toBe('ABDE') + }) + it('keeps SGR color spans but removes cursor controls for Ansi rendering', () => { const sample = `A${ESC}[31mB${ESC}[39m${ESC}[2J${ESC}]0;title${BEL}${ESC}[?25lC` expect(sanitizeAnsiForRender(sample)).toBe(`A${ESC}[31mB${ESC}[39mC`) }) + it('keeps valid SGR while removing dangling CSI and carriage returns', () => { + const sample = `A${ESC}[31mB${ESC}[12;${ESC}[39mC\rD` + + expect(sanitizeAnsiForRender(sample)).toBe(`A${ESC}[31mB${ESC}[39mCD`) + }) + it('detects non-CSI escape prefixes too', () => { expect(hasAnsi(`ok${ESC}Ppayload${ESC}\\`)).toBe(true) }) diff --git a/ui-tui/src/lib/text.ts b/ui-tui/src/lib/text.ts index 46dd1f67e15..5a5bdce603d 100644 --- a/ui-tui/src/lib/text.ts +++ b/ui-tui/src/lib/text.ts @@ -12,20 +12,30 @@ const ESC = String.fromCharCode(27) const BEL = String.fromCharCode(7) const ANSI_CSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, 'g') const ANSI_CSI_WITH_CMD_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*([@-~])`, 'g') +const ANSI_INCOMPLETE_CSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*(?=${ESC}|\\n|$)`, 'g') const ANSI_OSC_RE = new RegExp(`${ESC}\\][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g') const ANSI_STRING_RE = new RegExp(`${ESC}[PX^_][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g') const ANSI_STRAY_ESC_RE = new RegExp(`${ESC}(?!\\[)[\\s\\S]?`, 'g') -const CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F]/g +const CONTROL_RE = /[\x00-\x08\x0B\x0C\x0D\x0E-\x1A\x1C-\x1F\x7F]/g const WS_RE = /\s+/g export const stripAnsi = (s: string) => - s.replace(ANSI_OSC_RE, '').replace(ANSI_STRING_RE, '').replace(ANSI_CSI_RE, '').replace(ANSI_STRAY_ESC_RE, '').replace(CONTROL_RE, '') + s + .replace(ANSI_OSC_RE, '') + .replace(ANSI_STRING_RE, '') + .replace(ANSI_INCOMPLETE_CSI_RE, '') + .replace(ANSI_CSI_RE, '') + .replace(ANSI_INCOMPLETE_CSI_RE, '') + .replace(ANSI_STRAY_ESC_RE, '') + .replace(CONTROL_RE, '') export const sanitizeAnsiForRender = (s: string) => s .replace(ANSI_OSC_RE, '') .replace(ANSI_STRING_RE, '') + .replace(ANSI_INCOMPLETE_CSI_RE, '') .replace(ANSI_CSI_WITH_CMD_RE, (seq, cmd: string) => (cmd === 'm' ? seq : '')) + .replace(ANSI_INCOMPLETE_CSI_RE, '') .replace(ANSI_STRAY_ESC_RE, '') .replace(CONTROL_RE, '')