Repository navigation
fix(terminal): re-linkify streamed URLs so links underline/click on first hover - #9320
Conversation
…irst hover Terminal URL links (e.g. PR links an agent CLI prints) frequently stayed un-underlined and un-clickable until the user clicked the pane a few times. Root cause: xterm's Linkifier recomputes a link's underline only on mousemove when the hovered buffer cell changes, and caches provider replies per line with no content-change invalidation. A URL streamed in under a stationary pointer is never linkified — and native activation stays dead (shares the _currentLink precondition) — until the pointer crosses to a different line. Orca already had resetTerminalLinkifierHoverState for this, but it was wired only to visibility-resume (#9061), not to output streamed into a visible pane. Add installTerminalLinkifierHoverResetOnWrite: a leading-edge-throttled (150ms) onWriteParsed listener that clears the linkifier hover cache when output lands, so the next pointer move re-linkifies the fresh URL. A guard skips the reset while a link is actively hovered (_currentLink set) so an in-progress hover never clears+re-queries (no underline/tooltip flicker). Installed in openTerminal, disposed in disposePane. Co-authored-by: Orca <help@stably.ai>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds active-link detection for xterm linkifier internals and a throttled 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… hover Previously flush() dropped the pending reset when it fired while a link was hovered, relying on a later write to re-arm it. If that was the last chunk of a burst and it appended a link to the hovered line, the reset was lost and the link stayed dead until a line change — the same symptom class this fix targets. Re-arm the throttle instead so the reset runs once the hover ends, even if output has gone quiet. Timer stays non-null during the retry so a concurrent write cannot stack a second timer.
…irst hover (stablyai#9320) Invalidate xterm's linkifier hover cache (throttled) when output streams into a visible pane, so a URL printed under a stationary pointer underlines and becomes Cmd/Ctrl+clickable on the next pointer move instead of after several clicks. Skips while a link is hovered (no flicker) and re-arms so the reset is never dropped mid-hover. Reuses the visibility-resume primitive (stablyai#9061).
What & why
Terminal URL links (e.g. the PR links an agent CLI like Claude Code prints) frequently stayed un-underlined and un-clickable until you clicked the terminal a few times. Then they'd "wake up" and become underlined +
⌘/Ctrl+click-able.ELI5
xterm only draws the underline under a link when your mouse moves over it, and it remembers "this row has no link" from the last time your mouse was there. When an agent prints a link into the row your cursor is already resting on, xterm keeps trusting that stale "no link here" memory — so the link looks like plain text. Wiggling/clicking eventually moves the mouse across a row boundary, which makes xterm look again and finally notice the link.
This PR simply wipes that stale memory whenever new output arrives, so the very next mouse move notices the link. One move instead of several clicks.
Root cause
xterm's
Linkifierrecomputes a link's hover underline only onmousemovewhen the hovered buffer cell changes, and caches provider replies per line with no content-change invalidation (onRenderedViewportChangere-linkify is armed only while a link is already hovered). A URL streamed in under a stationary pointer is therefore never linkified — and native click activation stays dead too (it shares the same_currentLinkprecondition) — until the pointer crosses to a different line.Orca already had the exact primitive for this —
resetTerminalLinkifierHoverState(clears_lastBufferCell/_activeLine) — but it was wired only to visibility-resume (#9061, the "scroll to fix on tab switch" case), never to output streaming into an already-visible pane. That gap is the reported symptom.The fix
installTerminalLinkifierHoverResetOnWrite: a leading-edge-throttled (150 ms)terminal.onWriteParsedlistener that clears the linkifier hover cache when output lands, so the next pointer move re-linkifies the fresh URL. Installed inopenTerminal, disposed indisposePane.isTerminalLinkifierHoverActive): the reset is skipped while a link is currently hovered (_currentLinkset), so an in-progress hover is never cleared + re-queried (which would flicker the underline/tooltip for the async file-path provider). This provably doesn't reintroduce the bug — the reported scenario has the pointer over blank/non-link text, where_currentLinkis null.Evidence
Diagnosis verified against the xterm 6.1.0-beta.287 source (
Linkifier.ts): underline ismousemove-only; per-line reply cache is invalidated only on cross-line moves/resize; activation requires an existing_currentLink. Mouse-reporting mode / focus / pointer-events were all investigated and ruled out.Tests (
npx vitest run --config config/vitest.config.ts src/renderer/src/lib/pane-manager/→ 564 pass, 1 skipped):openTerminalinstall +disposePanedispose are asserted against the real subscribe path (not the guarded no-op)._currentLinkis set and resumes once it clears.onWriteParsed/internals are absent.Adversarial review: 2 rounds of independent fresh reviewers. Round 1 surfaced 3 real issues (silently-untested wiring, throttle undetectable by tests, mid-hover flicker) — all fixed here. Round 2 came back clean.
Perf (hot path — agents stream large volumes): negligible. Output is coalesced by Orca's scheduler and xterm's
WriteBufferfiresonWriteParsedat most ~80/s per terminal regardless of throughput; the listener is atimer !== nullcheck + return in the common case, one timer per 150 ms/pane, zero per-event allocation, and clearing the cache does noprovideLinks/render work until a real mousemove (verified in xterm source). ≈ <20 µs CPU/s per streaming pane.tc✓ ·oxlint✓ ·oxfmt✓.User-facing regression tradeoffs
Target: zero — and I believe it is zero.
One honest micro-edge-case (not a regression vs. today): if you are hovering a link on a row and that exact row is rewritten in place to a different link while your pointer stays put, the guard defers the refresh until you move to another row. This is astronomically rare, matches xterm's own pre-existing behavior for in-place same-row rewrites, and self-heals on the next move.
Known separate limitation (out of scope, not introduced here)
URL underlines still come solely from the stock WebLinksAddon, which only understands a single physical row. URLs an agent draws inside a box or hard-wrapped across rows may never underline at all (distinct from this "underlines after interaction" bug). Orca's robust box/wrap-aware scanners are wired only to the click fallback, not to a link provider. Happy to do that as a follow-up.
Test plan
pnpm run testonsrc/renderer/src/lib/pane-manager/— 564 pass⌘/Ctrl+clickopens it (previously needed several clicks).Made with Orca 🐋