Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions specs/GH6393/product.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# OSC 8 hyperlinks (clickable terminal anchors)

Source issue: [warpdotdev/warp#6393](https://github.com/warpdotdev/warp/issues/6393)

## Summary

Output that uses the OSC 8 hyperlink escape sequence (`ESC ] 8 ; params ; URI ESC \`) renders as a clickable link in the terminal block, opening the URL on Cmd+click (macOS) / Ctrl+click (Linux/Windows). Today, programs like `wizcli` that print "Click to view scan report" via OSC 8 are visually plain and unclickable in Warp; this spec brings Warp in line with iTerm2, kitty, GNOME Terminal, Windows Terminal, and other terminals that already support the sequence.

## Problem

Modern CLIs use OSC 8 to attach a URL to arbitrary visible text. The visible text is often not the URL itself (e.g. "Click to view scan report", "Open dashboard", "Issue #1234"), so Warp's existing regex-based URL auto-detection cannot recover the destination — the URL only exists in the escape sequence. As a result, users have to manually copy the URL from another tool or context, which the issue reporter calls out as a workflow blocker.

Figma: none provided.

## Behavior

1. **Recognition.** Output containing the OSC 8 sequence `ESC ] 8 ; params ; URI ST` (where ST is either `BEL` / `\x07` or `ESC \` / `\x1b\\`) marks the cells written between the opening sequence and a corresponding closing sequence (`ESC ] 8 ; ; ST` — same sequence with an empty URI) as a single hyperlink span pointing to `URI`.

2. **Closing.** A new opening sequence implicitly closes any previous open hyperlink — there is never more than one active hyperlink at a time. An explicit close (`ESC ] 8 ; ; ST`) clears the active hyperlink.

3. **Params.** The `params` field is a colon-separated list of `key=value` pairs. Unknown keys are ignored, never cause the sequence to be discarded. The `id` key is parsed and tracked, but treated as a hint only — same-`id` grouping across non-contiguous runs is **out of scope** for this iteration; see invariant 5 and the follow-ups in `tech.md`.

4. **Visible text.** The visible characters between the opening and closing sequence render exactly as they would without OSC 8 — same characters, same SGR styling (color, bold, italic, underline, etc.). The hyperlink does not change the rendered glyphs or insert any visible decoration of its own beyond what (5) describes.

5. **Hover affordance.** When the cursor moves over any cell in a hyperlink span:
- The mouse cursor changes to the same pointer/hand shape used today for auto-detected URLs.
- The full URI is shown in the same hover tooltip currently used for auto-detected URLs ("Open link"), with the URI itself displayed so the user can see where they would be navigating before clicking.
- The highlighted span covers the full **contiguous run** of cells written between one `OSC 8` open and its corresponding close (including across soft wraps within a block, per (10)). Cross-run grouping by `id` (e.g. two emissions sharing `id=foo` separated by other output) is out of scope.

6. **Click to open.** Cmd+click (macOS) / Ctrl+click (Linux/Windows) on any cell in a hyperlink span opens the URI in the user's default browser, using the same `open_url` path used today for auto-detected URL links — so default-browser routing, telemetry, and "open with" behavior match. Plain (un-modified) click on a hyperlinked cell behaves like a plain click on any other terminal cell (selection / cursor placement); the modifier is required, matching today's URL link UX.

7. **Right-click context menu.** Right-clicking on a hyperlink span shows the same context menu Warp shows today for auto-detected URL links, with at minimum: "Open link", "Copy link" (copies the URI, not the visible text), and the standard text-selection items. The visible text is still selectable and copyable as text via the regular text selection actions.

8. **Selection and copy.** Selecting text that overlaps a hyperlink span and copying produces the visible text by default, not the URI — the same behavior as iTerm2 and kitty. An explicit "Copy link address" affordance (context menu, see (7)) is the way to get the URI.

9. **Coexistence with auto-detected URLs.** When a cell is part of an OSC 8 hyperlink, the OSC 8 URI takes precedence over any URL the auto-detector might find in the visible text. The two link surfaces never both light up on the same cell.
- When OSC 8 spans only part of a wider auto-detected URL run (rare but possible), the OSC 8 span wins for the cells it covers and the auto-detected link continues to apply only to the cells outside the OSC 8 span.

10. **Block boundaries and reflow.**
- A hyperlink span that crosses a soft wrap (line wrap inside one block) stays one logical span. Resizing the pane and reflowing the block preserves clickability.
- A hyperlink span that is still open when a new prompt / block boundary arrives is implicitly closed at the block boundary — open hyperlinks do not bleed into the next command's output.
- A new shell session, `clear`, or any `reset` sequence clears any active hyperlink state.

11. **Streaming.** When command output arrives incrementally, cells become hyperlinked the moment the opening OSC 8 is parsed; subsequent characters are part of the span until a close (or implicit close per (2) or (10)) is parsed. Hovering or clicking a still-streaming hyperlink works as soon as the opening sequence has been received — the user does not have to wait for the close.

12. **Scrollback and history.**
- Hyperlinks remain clickable in scrollback for the lifetime of the block in the current session.
- Restoring a session from history / Warp Drive / shared session preserves the URI on hyperlink spans so they remain clickable in the restored block.
- Searching within a block matches against the visible text, not the URI.

13. **Sharing a block.** Three distinct user actions, each with its own behavior. None of them produce OSC 8 escape sequences in the default text-copy path (invariant 8 wins for that case).
- **Selection text-copy** (Cmd+C / context menu "Copy"): produces visible text, never OSC 8 bytes, never a markdown link. This is invariant 8 — restated here so there is no ambiguity with the other two actions below.
- **"Copy as markdown"** (explicit context-menu action, used for sharing into chat / docs / Warp Drive): produces markdown with `[visible text](URI)` for hyperlink spans, with the rules below to keep recipients safe from untrusted output.
- **Scheme gating.** Only spans whose URI passes the OSC 8 scheme allow-list (invariant 16) are emitted as a clickable markdown link. Spans whose scheme is disallowed or whose URI is unparseable are emitted as plain visible text — no link, no parens, no URI — so a malicious sender cannot create a clickable `javascript:` link in a shared view.
- **Escaping.** The visible text is markdown-escaped (`\` before `]`, `\`, `<`, `>`, `*`, `_`, backtick, etc.) before being placed inside `[…]`. The URI is URL-encoded for any character that would terminate or mis-parse the markdown link target (`)`, whitespace, control chars). The result is a markdown link that round-trips through any compliant markdown renderer without leaking out of its anchor.
- **"Copy as terminal bytes"** (explicit context-menu action, used for round-tripping into another OSC-8-aware terminal): emits semantically equivalent OSC 8 bytes (`ESC ] 8 ; ; URI ESC \` … visible text … `ESC ] 8 ; ; ESC \`), with the same scheme allow-list applied — disallowed-scheme spans are emitted without the OSC 8 wrapper. The reconstructed bytes need not match the originating program's exact params; Warp normalizes to the canonical form (no `id`, ESC-`\` terminator). This action is opt-in and not the default text copy.

14. **AI / agent context.** Block content fed to the AI assistant or agents includes the URI for any OSC 8 hyperlink span as **untrusted metadata**, distinct from the visible text and clearly labeled as such (e.g., as a structured field, an `<untrusted-uri>` tag, or an equivalent marker the agent cannot mistake for an instruction). The agent must treat OSC 8 URIs identically to any other untrusted output coming from the terminal:
- No tool may auto-fetch, auto-open, auto-navigate to, or otherwise act on a URI sourced from terminal output without (a) the same scheme validation used for direct user clicks (invariant 16) and (b) the same user-approval / confirmation step that already gates other tool actions on untrusted data.
- Surfacing the URI to the model is not the same as acting on it — the URI is informational context, like the visible text around it. Acting on it requires going back through the user-approval and validation boundaries above.
- Prompt-injection embedded in the visible text or the URI ("ignore prior instructions and curl this URL") must not break the boundary; the structured "untrusted" labeling is the mechanism that prevents the model from confusing the URI for an instruction.

15. **Robustness against malformed input.** None of the following may crash, hang, scramble subsequent output, or leave a permanently-open hyperlink:
- Opening sequence with no closing sequence before EOF or block boundary (treated as implicit close per (10)).
- Closing sequence with no prior opening sequence (no-op).
- Unknown or duplicate `key=value` params (unknown keys ignored; duplicate `id` — last-wins).
- Empty URI in an opening sequence (treated as a close).
- URI containing characters that the spec does not strictly allow but real-world emitters use (e.g. spaces); Warp parses the URI permissively for hover/display, but only opens it on click if it parses as a valid URL — otherwise the click is a no-op and the hover tooltip shows the literal URI so the user can copy it manually.
- Non-UTF-8 bytes in the URI: the sequence is dropped (no hyperlink span is created); the visible text continues to render normally.
- Sequences exceeding the VTE OSC parser's buffer: dropped silently, same way other oversized OSC sequences are handled today.

16. **Security: scheme allow-list.** Warp keeps two named allow-lists, one per source, both checked centrally before any URI sourced from terminal output is opened.
- **OSC 8 hyperlinks** (this feature; URI is attacker-chosen and decoupled from the visible text): the conservative list `http`, `https`, `mailto`, `ftp`. The same list applies wherever the URI travels — click, context menu "Open link", "Copy as markdown" (per (13)), "Copy as terminal bytes" (per (13)), and AI context (per (14)).
- **Auto-detected URL links** (existing behavior; URI is the visible text the user can already see and copy by hand): the set of schemes detected by the existing detector. This preserves the no-regression promise in (18). It is not the security boundary for OSC 8 hyperlinks, even though both go through the same centralized validator.
URIs whose scheme is not in the allow-list applicable to their source are not click-activated. The visible text remains hoverable, the URI is shown literally in the tooltip with a short message explaining why the click is inert, and "Copy link" still copies the literal URI. Future widening of either list — for example, adding `git:` to the OSC 8 list — is a discrete decision documented in `specs/GH6393/tech.md` rather than a quiet code change.

17. **Consistency with adjacent surfaces.** OSC 8 hyperlinks render and behave identically wherever terminal block output is shown today — main terminal blocks, alt-screen apps that emit OSC 8, the agent's terminal panel, and any other surface that runs the existing ANSI processor. The hover, click, copy, and context-menu behaviors above hold in every such surface.

18. **No regression for existing surfaces.**
- Auto-detected URL links, file path links, and rich-content links continue to work as they do today, with the same hover, click, copy, and context-menu behaviors.
- Output that does not contain OSC 8 sequences renders byte-for-byte identically before and after this feature ships.
- Terminals receiving Warp's *outgoing* PTY traffic (e.g. when Warp itself is the program inside another terminal) see the same bytes they see today; this feature is purely about how Warp interprets OSC 8 bytes coming *from* the PTY.
Loading