Skip to content

Emit OSC 8 hyperlinks through the render path#26

Merged
dcosson merged 4 commits into
mainfrom
osc8-hyperlinks
May 24, 2026
Merged

Emit OSC 8 hyperlinks through the render path#26
dcosson merged 4 commits into
mainfrom
osc8-hyperlinks

Conversation

@dcosson
Copy link
Copy Markdown
Owner

@dcosson dcosson commented May 24, 2026

Summary

Emit OSC 8 hyperlinks through h2's render path so links from inner programs (Claude Code file paths, gh, ls --hyperlink=always, etc.) become clickable in the outer terminal. Previously midterm parsed OSC 8 but our renderer reconstructed output cell-by-cell and dropped the URLs.

  • RenderLineFrom walks Region.URLID transitions and emits OSC 8 open/close around each linked run. Each row stands alone (open at start of linked run, close at row end).
  • ScrollHistory entries store the resolved URI per FormatRun so they're self-contained against later midterm state changes; captured at OnScrollback time. renderHistoryEntry emits OSC 8 the same way.
  • URIs are sanitized at the emission boundary — any byte < 0x20 or 0x7F drops the link entirely rather than risk re-injecting attacker bytes into the outer terminal. midterm's OSC parser already terminates at ESC/BEL, but this is defense in depth.
  • go.mod pinned to dcosson/midterm@v0.2.4-dcosson.1 (pre-release suffix keeps us behind any future upstream v0.2.4).

Test plan

  • Unit tests for OSC 8 emission in live + history paths, adjacent-link transitions, malicious-URI rejection, and the sanitizer itself
  • All make check lints clean
  • Verified end-to-end in Ghostty: printf '\e]8;;https://example.com\e\\click here\e]8;;\e\\\n' inside h2 run --command bash renders as a clickable link

🤖 Generated with Claude Code

Danny Cosson and others added 4 commits May 21, 2026 21:03
The renderer reconstructs its output from midterm's cell grid, so OSC 8
sequences from the inner program were being dropped even though the
fork now tracks them per-region. Walk Region.URLID transitions in
RenderLineFrom and emit \033]8;;<URL>\033\\ open/close pairs around
runs sharing a URL. Each row stands alone — opening at the start of a
linked run and closing at row end — so the cursor reposition between
rows doesn't carry a stale link forward.

Scrollback entries store the resolved URI per FormatRun (not just the
URL ID) so they're self-contained against later midterm state changes,
captured at OnScrollback time. renderHistoryEntry emits OSC 8 the same
way as the live path.

go.mod uses a local replace for the midterm fork during development;
needs to be repointed at a pushed dcosson/midterm commit before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A URI containing ESC/BEL/other C0 controls could break out of the OSC 8
envelope and inject arbitrary terminal sequences into the outer terminal.
midterm's parser already terminates OSC at ESC and BEL so the live path
is naturally safe in practice, but the history path stores resolved URI
strings as plain data — defense in depth at the rendering boundary is
cheap and protects against future midterm changes.

writeOSC8BoundaryStr is now the single emission gate; it rejects any
URL containing bytes < 0x20 or 0x7F (DEL) and drops the link entirely
rather than try partial recovery. RenderLineFrom pre-resolves and caches
the sanitized URL per region ID so state-tracking stays consistent.

Repoint go.mod replace from a local checkout to the pushed
dcosson/midterm osc8-hyperlinks branch so other agents and CI can build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetry fix from review: previously lastURL held the raw run.URL even
when writeOSC8BoundaryStr had dropped the URL as unsafe, so an
unsafe-only linked run would emit a harmless final close at row end
without a corresponding open. Pre-sanitize and track the result, just
like RenderLineFrom does, so the state machine reflects what was
actually emitted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the pseudo-version pin with the v0.2.4-dcosson.1 tag on the
dcosson/midterm fork. The pre-release suffix keeps us sorted below any
future upstream v0.2.4 release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dcosson dcosson merged commit 2bcf761 into main May 24, 2026
1 check passed
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