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.
This commit is contained in:
Kurt Himebauch 2026-08-05 07:50:37 -04:00 • committed by GitHub
parent 6bfd50261d
commit b780d20aa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 29 additions and 2 deletions

View file

@ -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.
*/

View file

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