From 6e8875df7f49542bda76eaa6fcbdb52a1e31f58d Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:15:32 -0700 Subject: [PATCH] fix(terminal): end a url at CJK punctuation instead of swallowing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminal URL detection treated every non-ASCII character as part of the url, so Japanese or Chinese text written straight after one was absorbed into the link. A line like PR: https://github.com/org/repo/pull/12345(作成済み・マージ待ち) underlined the whole run and opened the annotation percent-encoded onto the end of the url, which 404s. The body terminator listed only ASCII codes, so nothing at or above 0x80 could end a url. Terminating on all non-ASCII is the obvious repair and is wrong: a url path may legitimately carry unencoded CJK, and the wrapped-url tests cover exactly that - a real multi-line url whose continuation row is a Chinese path segment. That repair was written first and broke four of them. The distinction is punctuation, not ASCII. Non-ASCII punctuation, symbols and separators end a url; letters do not. Full-width brackets, an ideographic space, a full-width comma and the katakana middle dot are prose; 文档 in a path is not. Adds the extraction test file the module never had, covering the reporter's three cases, the CJK-path case that must keep working, and ASCII behaviour as a regression guard. Closes #10571 --- .../terminal-http-url-extraction.test.ts | 44 +++++++++++++++++++ .../terminal-http-url-extraction.ts | 9 ++++ 2 files changed, 53 insertions(+) create mode 100644 src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts diff --git a/src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts b/src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts new file mode 100644 index 00000000000..2aac516928c --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-http-url-extraction.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { extractTerminalHttpLinks } from './terminal-http-url-extraction' + +const urlsIn = (line: string): string[] => extractTerminalHttpLinks(line).map((link) => link.url) + +describe('extractTerminalHttpLinks', () => { + it('stops at a full-width parenthesis instead of swallowing the annotation', () => { + expect( + urlsIn('- PR: https://github.com/stablyai/orca/pull/12345(作成済み・マージ待ち)') + ).toEqual(['https://github.com/stablyai/orca/pull/12345']) + }) + + it('stops at a full-width closing quote', () => { + expect(urlsIn('詳細は「https://example.com/docs」を参照')).toEqual(['https://example.com/docs']) + }) + + it('stops at an ideographic space', () => { + expect(urlsIn('PR: https://example.com/a (全角スペース区切り)')).toEqual([ + 'https://example.com/a' + ]) + }) + + it('keeps unencoded CJK letters that are genuinely part of the path', () => { + // Why: wrapped URLs with Chinese path segments are supported; only punctuation ends a url. + expect(urlsIn('https://example.com/文档/路径')).toEqual([ + new URL('https://example.com/文档/路径').toString() + ]) + }) + + it('stops at a full-width comma and full stop between a url and prose', () => { + expect(urlsIn('https://example.com/a、次へ')).toEqual(['https://example.com/a']) + expect(urlsIn('https://example.com/b。')).toEqual(['https://example.com/b']) + }) + + it('still extracts a plain ascii url and keeps its own punctuation', () => { + expect(urlsIn('see https://example.com/a_b-c?d=1&e=2#f end')).toEqual([ + 'https://example.com/a_b-c?d=1&e=2#f' + ]) + }) + + it('still trims ascii trailing punctuation', () => { + expect(urlsIn('see https://example.com/a.')).toEqual(['https://example.com/a']) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/terminal-http-url-extraction.ts b/src/renderer/src/components/terminal-pane/terminal-http-url-extraction.ts index 192c4db15e1..eeaa463d818 100644 --- a/src/renderer/src/components/terminal-pane/terminal-http-url-extraction.ts +++ b/src/renderer/src/components/terminal-pane/terminal-http-url-extraction.ts @@ -98,6 +98,7 @@ function trimHttpUrlTrailingPunctuation( function isHttpUrlBodyTerminator(code: number): boolean { return ( + isNonAsciiUrlTerminator(code) || isAsciiWhitespace(code) || code === 0x22 || code === 0x27 || @@ -142,6 +143,14 @@ function isHttpUrlTrailingPunctuation(code: number): boolean { ) } +// Why not all non-ascii: a url path may legitimately carry unencoded CJK (see the wrapped-url +// tests), so only punctuation and spaces end it - full-width brackets, an ideographic space, `\u30fb`. +const NON_ASCII_URL_TERMINATOR = /[\p{P}\p{S}\p{Z}]/u + +function isNonAsciiUrlTerminator(code: number): boolean { + return code > 0x7e && NON_ASCII_URL_TERMINATOR.test(String.fromCharCode(code)) +} + function isAsciiWhitespace(code: number): boolean { return code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32 }