From f943d697de9171d81749ea55c5b0bdbf523233b8 Mon Sep 17 00:00:00 2001 From: Yangshun Date: Fri, 16 Oct 2026 12:51:44 +0800 Subject: [PATCH 1/2] fix(cli): preserve full URL targets in truncated box titles --- packages/void/src/cli/output.ts | 7 ++++++- .../void/test/integration/terminal-links.test.ts | 13 +++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/void/src/cli/output.ts b/packages/void/src/cli/output.ts index 609fa03bb..ee286332c 100644 --- a/packages/void/src/cli/output.ts +++ b/packages/void/src/cli/output.ts @@ -127,7 +127,12 @@ export function note(...args: Parameters): void { // Insert the link after layout: the renderer counts OSC 8 destinations // toward the title width and can otherwise truncate a URL that fits. if (header && urlTitle) { - chunk = chunk.replace(` ${urlTitle} `, ` ${formatUrl(urlTitle)} `); + const label = chunk.includes(urlTitle) + ? urlTitle + : stripVTControlCharacters(chunk).match(/ (\S*…)/)?.[1]; + if (label) { + chunk = chunk.replace(label, () => terminalLink(label, urlTitle, { fallback: false })); + } } header = false; return output.write(chunk); diff --git a/packages/void/test/integration/terminal-links.test.ts b/packages/void/test/integration/terminal-links.test.ts index b8b48409f..cbe85f07c 100644 --- a/packages/void/test/integration/terminal-links.test.ts +++ b/packages/void/test/integration/terminal-links.test.ts @@ -5,13 +5,13 @@ import { describe, expect, it } from 'vite-plus/test'; const outputModule = new URL('../../src/cli/output.ts', import.meta.url).href; const url = 'https://app.example.com/hello?name=void&mode=preview#content'; -function render(hyperlinks: boolean): { url: string; named: string; box: string } { +function render(hyperlinks: boolean, columns = 80): { url: string; named: string; box: string } { const script = ` import { formatLink, formatUrl, note } from ${JSON.stringify(outputModule)}; const url = ${JSON.stringify(url)}; let box = ''; note('Static Site — 1 asset', url, { - output: { columns: 80, write(chunk) { box += chunk; return true; } }, + output: { columns: ${columns}, write(chunk) { box += chunk; return true; } }, }); console.log(JSON.stringify({ url: formatUrl(url), named: formatLink('Dashboard', url), box })); `; @@ -31,15 +31,16 @@ console.log(JSON.stringify({ url: formatUrl(url), named: formatLink('Dashboard', } describe('terminal links (real capability detection)', () => { - it('emits OSC 8 links with the exact destination and keeps box padding outside the link', () => { - const output = render(true); + it.each([40, 80])('keeps the complete link destination in a %s-column box title', (columns) => { + const output = render(true, columns); // ansi-escapes uses BEL to delimit OSC 8 control sequences. const opening = `\u001b]8;;${url}\u0007`; const closing = '\u001b]8;;\u0007'; expect(output.url).toBe(`${opening}${url}${closing}`); expect(output.named).toBe(`${opening}Dashboard${closing}`); - expect(output.box).toContain(` ${opening}${url}${closing} `); - expect(stripVTControlCharacters(output.box).split('\n')[0]).toContain(` ${url} `); + const label = columns === 40 ? `${url.slice(0, 34)}…` : url; + expect(output.box).toContain(` ${opening}${label}${closing}`); + expect(stripVTControlCharacters(output.box).split('\n')[0]).toContain(` ${label}`); }); it('prints each URL once as plain text when hyperlinks are unsupported or output is redirected', () => { From 43ada2056f554d9acdbc45ba36a946f6fce5a486 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Sun, 18 Oct 2026 14:13:40 +0800 Subject: [PATCH 2/2] fix(cli): find the cut URL in a box title without a regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The title is matched against the URL itself: the longest start of the URL followed by "…". Any other word that ends in "…" can no longer be taken for the link label. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/void/src/cli/output.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/void/src/cli/output.ts b/packages/void/src/cli/output.ts index ee286332c..5dc9bb7e2 100644 --- a/packages/void/src/cli/output.ts +++ b/packages/void/src/cli/output.ts @@ -98,6 +98,21 @@ export function createSpinner( }; } +/** The URL as the box title shows it: whole, or its longest start cut with "…". */ +function shownUrl(chunk: string, url: string): string | undefined { + if (chunk.includes(url)) { + return url; + } + const plain = stripVTControlCharacters(chunk); + for (let end = url.length - 1; end > 0; end--) { + const label = `${url.slice(0, end)}…`; + if (plain.includes(label)) { + return label; + } + } + return undefined; +} + /** Narrowest terminal the shared box renderer can draw a note in, guide bar included. */ const MIN_NOTE_COLUMNS = 6; @@ -127,9 +142,7 @@ export function note(...args: Parameters): void { // Insert the link after layout: the renderer counts OSC 8 destinations // toward the title width and can otherwise truncate a URL that fits. if (header && urlTitle) { - const label = chunk.includes(urlTitle) - ? urlTitle - : stripVTControlCharacters(chunk).match(/ (\S*…)/)?.[1]; + const label = shownUrl(chunk, urlTitle); if (label) { chunk = chunk.replace(label, () => terminalLink(label, urlTitle, { fallback: false })); }