Skip to content

[Bug]: Terminal URL detection swallows CJK text — non-ASCII characters are never treated as URL terminators #10571

Closed

Description

@tomihe0720

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?

  1. 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 (全角スペース区切り)'
  2. Hover / ⌘-click (or ⇧⌘-click for the system browser) on the URL.

  3. 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 (作成済み)'    # OK

Root 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

  1. AmethystLiang commented on Jul 25, 2026

    @AmethystLiang
    Contributor

    looking into it

  2. AmethystLiang commented on Jul 25, 2026

    @AmethystLiang
    Contributor

    Update after digging into this — the diagnosis in the report is correct, and there's one more failure mode worth recording.

    Confirmed root cause. isHttpUrlBodyTerminator and isHttpUrlTrailingPunctuation in src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts compare only ASCII code points, so no character above U+007F can end a link candidate. The scan runs to end of line and new 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:

    line opens
    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.ts builds WebLinksAddon with no urlRegex, so the hover underline and tooltip come from xterm's default regex while the click re-resolves through extractTerminalHttpLinks. 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.

    1. Property-based non-ASCII terminators (\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 including https://ja.wikipedia.org/wiki/日本語, https://例え.jp/ and https://example.com/café.
    2. Replace WebLinksAddon detection 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, and terminal-link-provider-guard.ts exists 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.

  3. tomihe0720 commented on Jul 26, 2026

    @tomihe0720
    Author

    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:

    char code point category
    ・ U+30FB \p{P}
    = U+FF1D \p{Sm}
    〜 U+301C \p{P}

    https://ja.wikipedia.org/wiki/ジャン=リュック・ゴダール is a live article URL
    containing 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 for
    transliterated personal names, so this is not a rare title shape.

    https://ja.wikipedia.org/wiki/日本語 passes because it happens to contain no
    punctuation — 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 inside
    paths (・=〜-) as trailing-trim only. The failure mode you accepted for
    particle-glue applies here too — with ・ trim-only, .../docs・参照 degrades to
    a 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/日本語を参照.

  4. nwparker commented on Jul 27, 2026

    @nwparker
    Contributor

    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:

    • isAsciiWhitespace only U+0009–U+000D and U+0020 (not U+3000)
    • isHttpUrlBodyTerminator only ASCII whitespace + a fixed ASCII punctuation set — no full-width ()」 etc.

    Executable proof (mirrors current predicates)

    Input Detected candidate
    .../pull/12345 (作成) half-width space ✅ stops at URL
    .../pull/12345(作成済み・マージ待ち) ❌ includes full-width paren + CJK
    「https://example.com/docs」を参照 ❌ includes 」を参照

    UI steps

    echo 'PR: https://github.com/stablyai/orca/pull/12345(作成済み・マージ待ち)'

    Hover / ⌘-click → link range and opened URL include percent-encoded Japanese.

  5. added
    has_reproConclusive reproduction exists (test/script/CLI evidence). See docs/bug-reproductions/
    on Jul 27, 2026
  6. added a commit that references this issue on Aug 4, 2026
    53501df
  7. added a commit that references this issue on Aug 18, 2026
    2b057eb
  8. added a commit that references this issue on Aug 21, 2026
    2ccf387
  9. added a commit that references this issue on Aug 27, 2026
    b20072c
  10. added a commit that references this issue on Oct 15, 2026
    9264d4a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workinghas_reproConclusive reproduction exists (test/script/CLI evidence). See docs/bug-reproductions/os:macosIssues affecting macOS

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions