Repository navigation
[Bug]: Terminal URL detection swallows CJK text — non-ASCII characters are never treated as URL terminators #10571
Description
Operating system
macOS
Orca version
v1.4.155
Details
Short summary:
Terminal URL detection never terminates a URL on non-ASCII characters, so any CJK text that directly follows a URL (full-width parentheses, ideographic space, 、。・「」, …) gets swallowed into the link. The underline is drawn over the whole run, and ⌘-click / ⇧⌘-click opens a percent-encoded garbage URL instead of the real one.
What happened?
Claude Code (and any tool that annotates a URL in Japanese/Chinese) frequently prints lines like:
- PR: https://github.com/stablyai/orca/pull/12345(created・waiting for merge)
Orca underlines https://github.com/stablyai/orca/pull/12345(created・waiting for merge) as a single link, and opening it navigates to:
https://github.com/stablyai/orca/pull/12345%EF%BC%88created%E3%83%BBwaiting%20for%20merge%EF%BC%89
which 404s. The same happens with 「…」 quotes, 、 and a full-width space (U+3000) between the URL and the following text.
How can we reproduce it?
-
In a terminal pane, run:
echo 'PR: https://github.com/stablyai/orca/pull/12345(作成済み・マージ待ち)' echo '詳細は「https://example.com/docs」を参照' echo 'PR: https://github.com/stablyai/orca/pull/12345 (全角スペース区切り)'
-
Hover / ⌘-click (or ⇧⌘-click for the system browser) on the URL.
-
The link range extends past the URL into the CJK text, and the opened URL contains the percent-encoded Japanese text.
Control case that works correctly: with a half-width space or half-width parentheses, the boundary is detected properly:
echo 'PR: https://github.com/stablyai/orca/pull/12345 (作成済み)' # OK
echo 'PR: https://github.com/stablyai/orca/pull/12345 (作成済み)' # OKRoot cause
In the renderer bundle, iterateTerminalHttpUrlCandidates() grows the candidate from the http(s):// scheme until isHttpUrlBodyTerminator() returns true, then trims with isHttpUrlTrailingPunctuation(). Both predicates are ASCII-only:
function isHttpUrlBodyTerminator(code) {
return isAsciiWhitespace(code) || code === 34 || code === 39 || code === 33
|| code === 42 || code === 40 || code === 41 || code === 123 || code === 125
|| code === 124 || code === 92 || code === 94 || code === 60 || code === 62
|| code === 96;
}
function isAsciiWhitespace(code) {
return code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32;
}No code point above U+007F can ever terminate the candidate, and the trailing trim (: , . ? [ ] ~ ( ) …) is ASCII-only as well, so nothing is stripped afterwards. new URL() then happily parses the result (the CJK text is percent-encoded), so the candidate passes validation in extractTerminalHttpLinks() and becomes a clickable link.
I extracted those functions from the shipped 1.4.155 bundle and ran them directly:
| Input line | Detected link |
|---|---|
.../pull/2 (作成済み…) (half-width space) |
✅ .../pull/2 |
.../pull/2(作成済み・マージ待ち) |
❌ .../pull/2%EF%BC%88作成済み・マージ待ち%EF%BC%89 |
.../pull/2 (作成済み…) (U+3000) |
❌ includes U+3000 + the whole comment |
詳細は「https://example.com/docs」を参照 |
❌ https://example.com/docs%E3%80%8D%E3%82%92%E5%8F%82%E7%85%A7 |
.../pull/2 (作成済み) (half-width paren) |
✅ .../pull/2 |
Suggested fix
Make the boundary logic Unicode-aware rather than ASCII-only, e.g.:
- treat any Unicode whitespace (at least U+3000 IDEOGRAPHIC SPACE) as a body terminator;
- treat CJK brackets/quotes as terminators the same way their ASCII counterparts are:
()〔〕【】〈〉《》「」『』[]{}(U+FF08/FF09, U+3008–U+3011, U+300C–U+300F, …); - add CJK punctuation to the trailing-trim set:
、。,.!?:;・…(U+3001, U+3002, U+FF0C, U+FF0E, U+FF01, U+FF1F, U+FF1A, U+FF1B, U+30FB, U+2026).
Note that a fullwidth-form check alone is not enough — 「」 and 、 are in the CJK Symbols and Punctuation block, not the Halfwidth and Fullwidth Forms block.
Anything else that might help
Related, already-fixed boundary bugs in the same scanner: #8832 (next line's text glued on) and #8126 (ANSI reset code glued to the URL). This one is the CJK counterpart, and it hits every Japanese/Chinese user whose agent annotates URLs inline — which Claude Code does constantly.
Activity
AmethystLiang commentedon Jul 25, 2026
looking into it
AmethystLiang commentedon Jul 25, 2026
Update after digging into this — the diagnosis in the report is correct, and there's one more failure mode worth recording.
Confirmed root cause.
isHttpUrlBodyTerminatorandisHttpUrlTrailingPunctuationinsrc/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.tscompare only ASCII code points, so no character above U+007F can end a link candidate. The scan runs to end of line andnew URL()accepts the result.Not always a 404. When the URL has no path, the WHATWG parser folds the trailing CJK into the authority rather than the path and IDNA-encodes it:
see https://example.com、それからhttps://example.xn--com-6y3b8ordxnwa/https://example.com。次の行https://example.com.xn--u9j508ocks/Those are syntactically valid, registrable domains with no relationship to
example.com. Verified by calling the live module in a running renderer, not just a harness. So the ⌘-click can navigate somewhere a third party is free to register — a link-spoofing surface, not only a formatting problem. This wasn't in the original report and it's what sets the shape of the fix.Second detector.
src/renderer/src/lib/pane-manager/pane-dom-creation.tsbuildsWebLinksAddonwith nourlRegex, so the hover underline and tooltip come from xterm's default regex while the click re-resolves throughextractTerminalHttpLinks. The two disagree in both directions today: for U+3000 the addon terminates correctly and Orca doesn't; for…com、それからthe addon draws no underline while the click still opens the punycode host.Plan — two PRs.
\p{White_Space},\p{P},\p{Sm}, ASCII predicates untouched) plus a mixed-script hostname guard that truncates back to the ASCII prefix. Two mechanisms rather than one, because CJK letters legitimately appear in URL paths and so can't be blanket terminators — the guard is what closes the authority case regardless of which terminators are in the set. Currently 13/13 on the case set includinghttps://ja.wikipedia.org/wiki/日本語,https://例え.jp/andhttps://example.com/café.WebLinksAddondetection with an Orca-owned link provider so underline, tooltip and click derive from one resolution. Separate PR — it touches the hover path of every pane, andterminal-link-provider-guard.tsexists because a provider throw wedged the renderer before.#10573 from @innocarpe is a good-faith fix that resolves the three lines in this report, but the CJK punctuation lands in the trailing-trim set rather than the terminator set, which makes it inert for the general case — details on that PR. I'll carry it forward with co-author credit rather than asking for a rewrite.
Particle-glue (
詳細はhttps://example.com/docsを参照, no punctuation between) is deliberately out of scope:https://ja.wikipedia.org/wiki/日本語を参照is unsolvable under any rule, and with the hostname guard in place what remains degrades to a 404.tomihe0720 commentedon Jul 26, 2026
Thanks for digging in — the authority-folding case is a much better find than
anything in my report, and it changes how I'd think about the risk here.
One counterexample to the terminator set, since I think it's a live regression
rather than a scope question.
\p{P}and\p{Sm}cover several characters that appear inside real paths:・\p{P}=\p{Sm}〜\p{P}https://ja.wikipedia.org/wiki/ジャン=リュック・ゴダールis a live article URLcontaining both (verified against the ja.wikipedia API —
ジャン・ポール・サルトルalso redirects to
ジャン=ポール・サルトル, so both forms are in active use).Under the proposed set it truncates to
https://ja.wikipedia.org/wiki/ジャン,which works correctly today.
・in particular is the standard separator fortransliterated personal names, so this is not a rare title shape.
https://ja.wikipedia.org/wiki/日本語passes because it happens to contain nopunctuation — I think that's why the case set doesn't catch it.
Suggests a three-way split rather than two: brackets/quotes/full-stop-class
(
「」()、。) as terminators, and connector-class punctuation that occurs insidepaths (
・=〜-) as trailing-trim only. The failure mode you accepted forparticle-glue applies here too — with
・trim-only,.../docs・参照degrades toa 404 with the authority intact, which seems like the right side of the trade
versus breaking URLs that resolve today.
Happy to add these to the case set, and I can verify a build against real
Claude Code output once the first PR is up — Japanese CJK-annotated URLs are
most of what my terminal prints. And for the record, dropping particle-glue is
the right call from a native-usage standpoint; no rule recovers
.../wiki/日本語を参照.nwparker commentedon Jul 27, 2026
Repro status: has_repro (code-path, conclusive)
Summary
HTTP URL detection terminators are ASCII-only, so CJK full-width punctuation / ideographic space / trailing text after a URL is swallowed into the link.
Code evidence
src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts:isAsciiWhitespaceonly U+0009–U+000D and U+0020 (not U+3000)isHttpUrlBodyTerminatoronly ASCII whitespace + a fixed ASCII punctuation set — no full-width()」etc.Executable proof (mirrors current predicates)
.../pull/12345 (作成)half-width space.../pull/12345(作成済み・マージ待ち)「https://example.com/docs」を参照」を参照UI steps
Hover / ⌘-click → link range and opened URL include percent-encoded Japanese.
fix(terminal): terminate HTTP links on CJK brackets and whitespace
fix(terminal): end a url at CJK punctuation instead of swallowing it (#…
fix(terminal): end a url at CJK punctuation instead of swallowing it (s…
fix(terminal): end a url at CJK punctuation instead of swallowing it (s…
fix(terminal): end a url at CJK punctuation instead of swallowing it (s…