From b780d20aa17c4144ace7bff728150993236b88ee Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 5 Aug 2026 07:50:37 -0400 Subject: [PATCH] fix(tui): close truncated OSC 8 links (#7657) When truncateToWidth() cut through an OSC 8 hyperlink label, the retained prefix could leave the hyperlink open. Close an active hyperlink before the existing reset and ellipsis, preserving its BEL or ST terminator. Added a focused regression test for the reported BEL-terminated case. --- packages/tui/src/utils.ts | 23 +++++++++++++++++++-- packages/tui/test/truncate-to-width.test.ts | 8 +++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index a3cc7fc6a..1d6f6a809 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -153,13 +153,14 @@ function finalizeTruncatedResult( pad: boolean, ): string { const reset = "\x1b[0m"; + const hyperlinkClose = getActiveOsc8Close(prefix); const visibleWidth = prefixWidth + ellipsisWidth; let result: string; if (ellipsis.length > 0) { - result = `${prefix}${reset}${ellipsis}${reset}`; + result = `${prefix}${hyperlinkClose}${reset}${ellipsis}${reset}`; } else { - result = `${prefix}${reset}`; + result = `${prefix}${hyperlinkClose}${reset}`; } return pad ? result + " ".repeat(Math.max(0, maxWidth - visibleWidth)) : result; @@ -478,6 +479,24 @@ function formatOsc8Close(terminator: Osc8Terminator): string { return `\x1b]8;;${terminator}`; } +function getActiveOsc8Close(prefix: string): string { + let activeHyperlink: ActiveHyperlink | null = null; + let i = 0; + while (i < prefix.length) { + const ansi = extractAnsiCode(prefix, i); + if (ansi) { + const hyperlink = parseOsc8Hyperlink(ansi.code); + if (hyperlink !== undefined) { + activeHyperlink = hyperlink; + } + i += ansi.length; + } else { + i++; + } + } + return activeHyperlink ? formatOsc8Close(activeHyperlink.terminator) : ""; +} + /** * Track active ANSI SGR codes to preserve styling across line breaks. */ diff --git a/packages/tui/test/truncate-to-width.test.ts b/packages/tui/test/truncate-to-width.test.ts index 31f80a379..6240eca64 100644 --- a/packages/tui/test/truncate-to-width.test.ts +++ b/packages/tui/test/truncate-to-width.test.ts @@ -20,6 +20,14 @@ describe("truncateToWidth", () => { assert.strictEqual(truncated.endsWith("\x1b[0m…\x1b[0m"), true); }); + it("closes a BEL-terminated OSC 8 link when truncating its label", () => { + const open = "\x1b]8;;https://example.com\x07"; + const close = "\x1b]8;;\x07"; + const text = `${open}some-longer-label-here${close}`; + + assert.strictEqual(truncateToWidth(text, 15), `${open}some-longer-${close}\x1b[0m...\x1b[0m`); + }); + it("handles malformed ANSI escape prefixes without hanging", () => { const text = `abc\x1bnot-ansi ${"🙂".repeat(1000)}`; const truncated = truncateToWidth(text, 20, "…");