Make links in Claude's output clickable #59
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
83c9c24951 |
test: give two synthesised clicks the detail a real click carries
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 9s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m43s
Build App (Preview) / build-linux (pull_request) Successful in 7m58s
Build App (Preview) / build-windows (pull_request) Successful in 4m54s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
The previous commit tightened the gate's click-count check from `> 1` to
`!== 1`, which two tests in the wiring block did not survive: they built
`new MouseEvent("click", { button: 0 })` directly rather than through the
`click()` helper, so `detail` defaulted to 0 and the gate refused them.
The gate is right and the tests were wrong -- a mouseup derived from a real
click always carries `detail >= 1`, and 0 is exactly the synthetic-event
shape the tightening was for. Both now pass `detail: 1`.
I pushed the previous commit without noticing this, having read a truncated
test summary that hid the failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c6f9c1d43f |
fix: tighten the click-count check and stop three comments overstating
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 5s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-linux (pull_request) Successful in 6m6s
Build App (Preview) / build-windows (pull_request) Successful in 5m0s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
Third-round review polish; no behaviour change beyond the first item. `detail > 1` was justified in a comment by noting a synthesised event carries `detail` 0 -- which is an argument for letting untrusted synthetic events through the click-count half of the gate. A mouseup derived from a real click always carries `detail >= 1`, so the check is now `!== 1`. Nothing in the container can dispatch a DOM event, so this is hardening rather than a hole; the comment now says that instead of the reverse. Three comments claimed more than they hold. The selection check's paragraph read as though it caught every copy gesture: it sees a drag only once the drag has spanned a cell, so a press and release inside one character cell -- or a drag walked back to its start -- still opens the link. That is the gap the rejected mousedown/mouseup distance check would have closed, and it is now recorded beside the reason for rejecting it. `?1002l` was described as taking effect synchronously with the write; it takes effect when xterm parses it, on its queued write task. And `modifierPromised` was described as written on every hover, when `hover()` clears and returns early with no host element -- which leaves it false, the stricter direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
593b8168eb |
fix: a selection is not a request to leave the app
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 7s
Build App (Preview) / create-release (pull_request) Successful in 4s
Build App (Preview) / build-macos (pull_request) Successful in 2m42s
Build App (Preview) / build-linux (pull_request) Successful in 6m39s
Build App (Preview) / build-windows (pull_request) Successful in 5m0s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
Re-review found the gate did not cover the gesture users actually make. xterm's `Linkifier._handleMouseUp` has no click-count check, no distance threshold and no timestamp, so it activates on the mouseup that *ends a selection* as readily as on a click. Double-clicking a word or dragging across a few characters inside an OSC 8 link therefore opened the browser. Worse with a program holding the mouse: the only way to select text there is Shift/Option+drag, which is byte-identical to the gesture the gate accepted as a deliberate request to open. A container wrapping each output row in a link would have harvested every legitimate copy. `term.hasSelection()` is the load-bearing check: a drag is one press and one release, so its click count is 1 and `detail` cannot see it. `detail > 1` is belt-and-braces for the case where the selection came out empty, and for not depending on the selection model being written before the Linkifier's listener runs -- it is, but the check costs nothing. Drag distance was rejected rather than forgotten: xterm hands `activate` only the mouseup, so measuring it means binding our own listener and keeping a second source of truth about one gesture. The hover card's promise is now sticky. The hint was computed once at hover while the gate re-read the mode at mouseup, so a card reading "Shift+click to open" could be on screen while a bare click opened the link. The gate now requires the modifier if either the card asked for it or the live mode does. The same gate is applied to the WebLinksAddon branch, which had none. That also closes a real bypass: `OscLinkProvider` drops non-http(s) OSC 8 targets before `linkHandler` sees them, so a `javascript:` target with an `https://evil.tld` label fell through to WebLinks and opened ungated. What is not closed, and is now recorded rather than papered over: the mouse mode is a permission the container grants itself. It can drop tracking before the pointer arrives and hold it off through the click. The selection and click-count checks hold either way, so the mass-harvest variant is gone, but the real fix needs a signal the container cannot write and this pane does not have one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ac50c38891 |
fix: gate OSC 8 link activation instead of merely hinting at it
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 5s
Secret Scan / scan (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m42s
Build App (Preview) / build-windows (pull_request) Successful in 5m3s
Build App (Preview) / build-linux (pull_request) Successful in 7m13s
Build App (Preview) / prune-previews (pull_request) Successful in 3s
Review of this branch found its central premise was false. The claim was that xterm cancels a mousedown before the link layer while a program holds the mouse, so only a Shift+click could reach a link. None of that holds: `cancel()` is `if (this.options.cancelEvents || force)` and `cancelEvents` defaults to false and is never set here, so it does nothing; the mouse reporting listeners bind to `.xterm` while the Linkifier is constructed on `screenElement`, a descendant, so the link layer sees the event first regardless; and `_handleMouseUp` checks neither the modifier nor the button before calling `activate`. So a plain click opened the link, and so did a right-click. That is not a missing convenience. OSC 8 lets the container wrap any clickable TUI widget -- a menu row, a "1. Yes", a file chip -- in a link to anywhere, and because the mouse report still reaches the program afterwards the widget responds too and nothing looks wrong. The hover card was the only mitigation, and it assumes a user deliberately reaching for a link. `opensOnClick` is now a real gate: primary button only, and while a program tracks the mouse the force-selection modifier is required -- the gesture the user already has for "this click is for the terminal, not the program". With nothing tracking, a bare click opens, which is what WebLinksAddon already does for plain-text URLs in the same buffer. The mode is read per click through a getter rather than captured, and `syncMouseCapture` and the gate share one expression, because a gate that disagreed with the badge would be the hole again. The gate and the hint also share one modifier predicate, and the hint is conditional on tracking, so it can never name a key that does nothing. Three more from the same review. The origin span had `flexShrink: 0`, which beats `overflowWrap` under flexbox, so an attacker-controlled 600-character origin ran off the pane and hid the registrable domain -- the same spoof as an ellipsis, without one; it now wraps and the remainder is what gives way. The card had no `pointerEvents: none`, and `xterm-hover` is inert at this placement, so a card under the pointer took `mouseleave` from screenElement and made bottom-row links flicker and refuse to activate at all. And the design doc comment had come adrift from its function. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f311ca1990 |
feat: make links in Claude's output clickable
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m47s
Build App (Preview) / build-linux (pull_request) Successful in 8m8s
Build App (Preview) / build-windows (pull_request) Failing after 13m30s
Build App (Preview) / prune-previews (pull_request) Skipped
Claude Code prints links as OSC 8 hyperlinks whose visible text is hard-wrapped into terminal-width pieces -- urlDetector's header records a 346-character sign-in URL arriving as five emissions, each carrying the whole URL in its parameter and about 80 characters on screen. WebLinksAddon regex-matches the painted characters row by row, so against Claude it matches a fragment or nothing, which is why the URL toast exists. xterm 5.5 hands over the exact parameter through `linkHandler`, so the slicing stops mattering. WebLinksAddon stays for plain-text URLs in ordinary shell output; the two cover different cases and neither replaces the other. Both now share one failure reporter and one validator. No new key handling was needed. xterm's mousedown handler is `if (areMouseEventsActive && !shouldForceSelection(e)) return cancel(e)`, so holding the force-selection modifier lets the event reach the link layer while Claude still holds the mouse -- Shift+click, or Option+click on macOS, which this terminal already enables for text selection. The hover card is the security half rather than decoration. OSC 8 decouples the label from the target completely: a container can print `https://claude.ai` and link it anywhere, which is strictly worse than the userinfo spoofing already guarded against and which invalidated the justification for opening a click without confirmation ("a deliberate act on visible text"). Hovering now shows the real origin, in full and never truncated, because truncating it is the spoof. A target that fails validation says so and deliberately echoes nothing of itself. The hint names the modifier for the platform, from xterm's own `isMac` list, so it cannot tell a Mac user to press a key that does nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |