Skip to content

Add OSC 8 hyperlink activation with confirmation dialog#75

Merged
nedtwigg merged 19 commits into
mainfrom
url-open
May 19, 2026
Merged

Add OSC 8 hyperlink activation with confirmation dialog#75
nedtwigg merged 19 commits into
mainfrom
url-open

Conversation

@nedtwigg
Copy link
Copy Markdown
Member

Summary

  • Enables OSC 8 hyperlink activation in the terminal, routing clicks through a confirmation dialog before opening externally
  • Adds an ExternalLinkDialog with verdict-aware titling, copy action, and classification of mismatches between display text and URL
  • Extracts shared modal primitives in design.tsx and reuses them across the kill confirm and link dialogs

Test plan

  • Click an OSC 8 link in the terminal and confirm the dialog opens with the right verdict
  • Try a link whose display text mismatches its target — verify the warning state
  • Copy URL action works and dismisses the dialog
  • Existing kill-confirm flow still behaves correctly after modal-primitive extraction
  • Storybook stories render (ExternalLinkDialog, long URL variant)

🤖 Generated with Claude Code

nedtwigg and others added 19 commits May 18, 2026 15:29
The scheme metadata pill duplicated the URL prefix and read as a
code field on a security dialog. Replace it with a one-line action
subtitle that answers "where will this go?": browser, local file,
email app, phone app, or unknown scheme handler. Blocked URLs get
a prohibit icon, inline reason, and a single Close button instead
of a disabled Open URL.

Also flattens the nested-card borders on the URL block and the
blocked-reason box, per the bg-only chrome rule in DESIGN.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns match / plain / deceptive based on whether the visible link
text matches the URL, is a generic label, or is URL-shaped with a
different host. The dialog will use this to pick a title, gate the
action, and decide what the user is really being asked.

Subdomain mismatch is treated as deceptive on the conservative side
of the false-positive line; legit redirects between same-host pages
stay plain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xterm's linkHandler.activate gets the URL but not the rendered link
text. Read it from the buffer using the activate range and pass it
through requestExternalLinkConfirmation so the dialog can compare
what the user clicked against what would actually open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
terminal.options.linkHandler = … blew up under the test mock, which
doesn't expose an .options object. Inline the handler in the constructor
options instead. The closure capture of `terminal` is safe because the
callback only fires on user click, well after construction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dialog now answers three questions in its title bar:
- match: "Open <noun>?" where <noun> follows the URL scheme
  (URL, file, email, phone app, SMS app, or "custom protocol
  <prefix>" for unknown schemes).
- plain: 'Open <noun>: "<link text>"?' — title carries the
  human label that was actually clicked.
- deceptive: 'Deceptive link text was "<label>", URL was:' with
  a red WarningOctagon icon. The Open action is replaced with
  "Copy deceptive URL to clipboard" so the user has to paste
  manually if they really want to follow the link.

The merged title also subsumes the previous "Opens in your
browser" subtitle and the generic "terminal output can hide..."
footer; per-link verdict carries that signal now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the deceptive-title pattern: the Prohibit icon + Blocked
message become the title bar, instead of "Open URL?" plus a
separate Blocked row below. One verdict, one place to read it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DESIGN.md disallows nested cards, but in this dialog the framed
URL block IS the artifact the user is being asked to scrutinize,
and a bordered box reads better than the bare bg-shift. Documented
the exception inline so the next pass doesn't re-flatten it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Titles no longer carry blanket font-semibold; the bold weight is
reserved for the inline value being called out (the link label or
the deceptive display text). Surrounding sentence stays regular.

Replaces the previous pattern of wrapping values in "..." with
text-muted color — quotes plus muting made the extracted value
recede instead of pop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Title for unknown schemes is now just "Open URL?" (or "Open URL:
<label>?") — the scheme prefix lives in the URL block and the
"Open vscode://" button, so duplicating it in the title bar was
noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Title is now a header ("Confirm open" or "Confirm open: <label>")
instead of a question ("Open URL?"). The scheme noun only lives in
the button now — title doesn't vary by scheme, which simplifies the
mental model: the title says "you're being asked to confirm", the
URL block shows what, the button names the action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 023a1bd
Status: ✅  Deploy successful!
Preview URL: https://f3cbb6db.mouseterm.pages.dev
Branch Preview URL: https://url-open.mouseterm.pages.dev

View logs

@nedtwigg nedtwigg merged commit b13543c into main May 19, 2026
6 checks passed
@nedtwigg nedtwigg deleted the url-open branch May 19, 2026 06:29
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