Summary
This issue is specifically about the streaming model-response output rendered by the CLI's markdown renderer — i.e. the assistant text that is parsed and printed token-by-token while the model is replying. It is not about specific one-off flows such as MCP OAuth URLs or the /bug slash command (those have already been handled in #3489 / #3393 / #3257 by wrapping a single known URL at the call site).
When the model emits a long URL inside a markdown link (e.g. [label](https://very.long.example.com/...)) in its streamed answer, terminals usually wrap the URL across multiple lines. Most terminal emulators detect URLs by scanning each visible line for an unbroken http(s)://... substring, so a wrapped URL is no longer recognized as one link and cannot be clicked. Today this happens for every long URL the model produces in normal conversation, even though OSC 8 is already used in a handful of specific code paths.
The fix exists in the terminal world: OSC 8 hyperlinks. When the URL is emitted inside an ESC ] 8 ;; <url> ESC \\ … ESC ] 8 ;; ESC \\ envelope, the link target is decoupled from the visible label, and the terminal treats the entire wrapped label as one clickable region regardless of where it wraps.
Scope (what this issue is asking for)
Apply OSC 8 wrapping inside the streaming markdown renderer that prints assistant content, so every link the model produces benefits — not just URLs from specific hardcoded flows. Concretely:
- Inline markdown links:
[label](url) → emit ESC]8;;<url>ESC\<label>ESC]8;;ESC\ around the rendered label.
- Bare autolinks / raw URLs detected during rendering (
<https://...> and inline https://... runs) — same problem, same treatment.
- Must work correctly under streaming/partial input: a link may arrive split across multiple chunks (
[lab … el](htt … ps://…)), so the wrapping has to happen after the link node is fully parsed, not on the raw byte stream.
Out of scope (already covered elsewhere and not what this issue is about):
Proposal
In the markdown renderer used for streamed assistant output:
- Detect that the host terminal supports OSC 8 (iTerm2, WezTerm, Kitty, Alacritty ≥ 0.11, GNOME Terminal/VTE, Windows Terminal, etc. —
supports-hyperlinks on npm encapsulates this check).
- When emitting a link node, wrap the rendered label as:
\x1b]8;;<url>\x1b\\<label>\x1b]8;;\x1b\\
- Fall back to the current rendering (plain
label (url) or just the URL) when the terminal does not advertise support, or when NO_COLOR / FORCE_COLOR=0 / non-TTY stdout is detected (so piping to a file doesn't embed escape codes).
- Make sure the wrapping is applied at the parsed-AST level, not by post-processing the printed string, so that streaming partial chunks can't split an OSC 8 envelope across flushes.
Why this matters
- Long signed URLs (S3 presigned, OAuth callbacks, doc deep-links, GitHub permalinks with line ranges, etc.) are extremely common in model answers and almost always wrap.
- Today users have to copy the URL by hand across line boundaries, which is error-prone — many terminals' "copy URL" action only grabs the visible-line fragment.
- OSC 8 is purely additive — terminals that don't understand the sequence ignore it, so there's no regression risk for unsupported environments as long as the support check is in place.
Acceptance criteria
- A markdown link emitted by the model whose URL is long enough to wrap is clickable as a single link in OSC-8-capable terminals, regardless of where in the streamed output it appears.
- The visible label is unchanged (still styled/colored as before).
- Streaming partial chunks never produce a broken OSC 8 envelope (no half-written escape sequences on the wire).
- In terminals without OSC 8 support, output is byte-identical to today.
- Behavior is suppressed under
NO_COLOR / non-TTY stdout.
References
Summary
This issue is specifically about the streaming model-response output rendered by the CLI's markdown renderer — i.e. the assistant text that is parsed and printed token-by-token while the model is replying. It is not about specific one-off flows such as MCP OAuth URLs or the
/bugslash command (those have already been handled in #3489 / #3393 / #3257 by wrapping a single known URL at the call site).When the model emits a long URL inside a markdown link (e.g.
[label](https://very.long.example.com/...)) in its streamed answer, terminals usually wrap the URL across multiple lines. Most terminal emulators detect URLs by scanning each visible line for an unbrokenhttp(s)://...substring, so a wrapped URL is no longer recognized as one link and cannot be clicked. Today this happens for every long URL the model produces in normal conversation, even though OSC 8 is already used in a handful of specific code paths.The fix exists in the terminal world: OSC 8 hyperlinks. When the URL is emitted inside an
ESC ] 8 ;; <url> ESC \\…ESC ] 8 ;; ESC \\envelope, the link target is decoupled from the visible label, and the terminal treats the entire wrapped label as one clickable region regardless of where it wraps.Scope (what this issue is asking for)
Apply OSC 8 wrapping inside the streaming markdown renderer that prints assistant content, so every link the model produces benefits — not just URLs from specific hardcoded flows. Concretely:
[label](url)→ emitESC]8;;<url>ESC\<label>ESC]8;;ESC\around the rendered label.<https://...>and inlinehttps://...runs) — same problem, same treatment.[lab…el](htt…ps://…)), so the wrapping has to happen after the link node is fully parsed, not on the raw byte stream.Out of scope (already covered elsewhere and not what this issue is about):
/bugslash-command output (/bug is hard to open in terminals without reliable hyperlink support #3256 / fix(cli): make /bug easier to open in terminals without hyperlink support #3257)Proposal
In the markdown renderer used for streamed assistant output:
supports-hyperlinkson npm encapsulates this check).label (url)or just the URL) when the terminal does not advertise support, or whenNO_COLOR/FORCE_COLOR=0/ non-TTY stdout is detected (so piping to a file doesn't embed escape codes).Why this matters
Acceptance criteria
NO_COLOR/ non-TTY stdout.References
supports-hyperlinks: https://www.npmjs.com/package/supports-hyperlinksansi-escapes.link()in theansi-escapespackage