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, "…");