Skip to content

fix(app): terminal links open with ⌘-click, say so on hover, and OSC 8 links open too - #48

Merged
sean-brydon merged 2 commits into
mainfrom
fix/terminal-links
Oct 10, 2026
Merged

sean-brydon merged 2 commits into
mainfrom
fix/terminal-links

Conversation

@sean-brydon

Copy link
Copy Markdown
Collaborator

What was broken: "I can't click links in the terminal at all." On the default renderer (ghostty-web 0.4), hovering a URL underlined it and showed the pointing hand, but a plain click did nothing. ghostty-web only opens links on ⌘/Ctrl-click, and nothing said so. OSC 8 hyperlinks (text a program links somewhere) never opened, not even with ⌘-click.

Cause:

  • ghostty-web's built-in providers set the hover affordance (underline and cursor: pointer) without any modifier held, but activate only runs with ctrl/meta.
  • Its OSC 8 provider gets its address from wasmTerm.getHyperlinkUri(), which in 0.4 is a stub that returns null. So those links were underlined on hover but had nothing to open.
  • In xterm.js (the fallback renderer), OSC 8 links had no linkHandler. They fell back to confirm() plus a blank window.open(), and the app's webview supports neither.
  • I checked the rest and none of it was at fault in a browser, WebKit or simulated-Tauri run: window.open → openUrl → plugin:shell|open gets the right URL, the shell scope allows http(s), the predict overlay is pointer-events: none, coordinates line up, and tmux's real attach stream (alternate screen, mouse reporting, DECLRMM, sync output) is fine.

Fix:

  • lib/terminal.ts (ghostty): replaces ghostty-web's link providers with our own:
    • a URL finder that reads the row one character per cell (lib/term-links.findUrls), so empty cells and wide characters don't shift the columns; the file-path finder now uses the same text
    • an OSC 8 provider whose addresses come from a small parser fed by the terminal's write (HyperlinkTracker); it handles sequences split across chunks and passes over output with no OSC 8 in it after a quick scan
  • Both open through openUrl, and only http(s)/mailto addresses open.
  • The pointing hand shows only while ⌘ is held (Ctrl off a Mac). Resting on a link shows a small "⌘-click to open" hint (TerminalView).
  • xterm.js gets a linkHandler so its OSC 8 links open through openUrl like its web links.
  • New e2e terminal-links.spec.ts replays tmux's attach output and checks:
    • hover shows the hint
    • ⌘-click opens the URL without the full stop after it
    • an OSC 8 link opens its own address, not its text
    • xterm.js opens both
  • New unit tests in term-links.test.ts.

Note: tmux on boxes strips OSC 8 unless terminal-features has xterm*:hyperlinks (tmux 3.4+). I left the box's tmux.conf alone because of older tmux on boxes. Plain URLs are what agents print in practice.

Checks: pnpm typecheck:plugins, check:titles, check:csp, check:themes, pnpm test (306 pass), pnpm build, npx tsc -p e2e, docs-site build. E2E: terminal-links (also --repeat-each=5, and once in Playwright WebKit), plus home-terminal, look, idle, drop, keyboard, slow-link, helper-tabs and predict: 43 pass. 1 fails: predict.spec "on a slow link…". It also fails on main because it types "gpu" before the box picker has loaded its list. Not checked in a real Tauri build.

🤖 Generated with Claude Code

…8 links open too

ghostty-web 0.4 underlined a link and showed the pointing hand on a plain
hover, where a plain click did nothing (it opens only on ⌘/Ctrl-click), and
its OSC 8 links had no address at all (it can't read the URI back from its
WASM). Its link providers are replaced: addresses are found per cell, OSC 8
addresses are read from the output on the way in, and both open through
openUrl. The hand shows only while ⌘ is held; resting on a link shows
"⌘-click to open". xterm.js gets a linkHandler so its OSC 8 links open
without confirm() and a blank window.open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
berth Ready Ready Preview Oct 10, 2026 11:42am UTC
berth-docs Ready Ready Preview Oct 10, 2026 11:42am UTC

Request Review

Codex's ChatGPT sign-in link is a few hundred characters and wraps over
several rows of a pane. ghostty-web's links were found one row at a time,
so ⌘-click on it found nothing or opened a cut-off address. A row is now
read with the rows it runs on over: the ones the terminal soft-wrapped,
and, since a box's tmux redraws long lines row by row with the cursor
(nothing is marked wrapped), a row full to its last column with address
characters there that the next row picks up without a space. The link
underlines across all its rows and opens whole from any of them. File
paths are found the same way, and an OSC 8 link that wraps is one link.

ghostty-web 0.4's isWrapped says a row goes on from the one above (as in
xterm.js), not that it wraps onto the next as its docs say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sean-brydon
sean-brydon merged commit 0317b3b into main Oct 10, 2026
8 checks passed
@linear-code

linear-code Bot commented Oct 10, 2026

Copy link
Copy Markdown

ENG-5871

This branch was successfully deployed

2 active deployments
Preview – berth-docs — d69e6ba9 Deployed Oct 10, 2026 by vercel[bot]
Preview – berth — d69e6ba9 Deployed Oct 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant