Skip to content

feat(tui): use OSC 8 hyperlinks in Markdown when terminal supports them#3248

Merged
badlogic merged 2 commits into
earendil-works:mainfrom
ofa1:feat/tui-osc8-hyperlinks
Apr 16, 2026
Merged

feat(tui): use OSC 8 hyperlinks in Markdown when terminal supports them#3248
badlogic merged 2 commits into
earendil-works:mainfrom
ofa1:feat/tui-osc8-hyperlinks

Conversation

@ofa1

@ofa1 ofa1 commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Closes #3239

The issue feedback was correct: the original approach didn't handle line wrapping. This PR addresses that by extending AnsiCodeTracker in utils.ts to track active OSC 8 URLs alongside SGR state.

How wrapping is handled:

wrapSingleLine already re-emits active SGR codes at each line start via tracker.getActiveCodes(), and closes problematic attributes (underline) before each line break via tracker.getLineEndReset(). This PR extends both methods to cover OSC 8:

  • process() now handles \x1b]8;;url\x1b\\ (open) and \x1b]8;;\x1b\\ (close)
  • getActiveCodes() appends the OSC 8 re-open after SGR codes when a hyperlink is active
  • getLineEndReset() appends the OSC 8 close before each line break

breakLongWord uses the same getActiveCodes() path so it's covered too.

Changes:

  • src/utils.ts: extend AnsiCodeTracker with activeHyperlink tracking
  • src/terminal-image.ts: add hyperlink(text, url) and setCapabilities() for tests
  • src/index.ts: export both
  • src/components/markdown.ts: use hyperlink() in case "link": when getCapabilities().hyperlinks is true
  • test/wrap-ansi.test.ts: 3 new tests for OSC 8 wrapping
  • test/terminal-image.test.ts: 4 new tests for hyperlink()
  • test/markdown.test.ts: 4 new tests (both hyperlink and fallback paths), updated existing link tests to set capabilities explicitly

540/540 tests pass. biome and tsgo --noEmit clean on packages/tui.

TerminalCapabilities already tracks hyperlinks: boolean and returns true
for Ghostty, Kitty, WezTerm, and iTerm2, but nothing generated OSC 8
sequences. This completes that stub.

Changes to packages/tui:
- terminal-image.ts: add hyperlink(text, url) and setCapabilities()
- index.ts: export hyperlink and setCapabilities
- utils.ts: extend AnsiCodeTracker to track active OSC 8 URLs
  - process() now handles OSC 8 open/close sequences
  - getActiveCodes() re-emits the OSC 8 open at each line start
  - getLineEndReset() closes the OSC 8 hyperlink before each line break
  This ensures hyperlinks wrap correctly across multiple lines.
- components/markdown.ts: link renderer uses hyperlink() when
  getCapabilities().hyperlinks is true; falls back to (url) text
- Tests: new wrap-ansi tests for OSC 8 line-wrapping; terminal-image
  tests for hyperlink(); markdown tests covering both code paths;
  table-cell width test pinned to hyperlinks:false (checks raw columns)

closes earendil-works#3239

Co-authored-by: AI (Pi/Claude Sonnet 4.6) <noreply@pi.dev>
@ofa1 ofa1 marked this pull request as draft April 15, 2026 21:59
@badlogic badlogic added the inprogress Issue is being worked on label Apr 16, 2026
@badlogic badlogic marked this pull request as ready for review April 16, 2026 20:13
@badlogic badlogic merged commit e8743e8 into earendil-works:main Apr 16, 2026
badlogic added a commit that referenced this pull request Apr 16, 2026
OSC 8 hyperlinks landed in #3248, but detectCapabilities() returned
hyperlinks: true in the unknown-terminal fallback. Terminals that
silently swallow OSC 8 (most xterm-compatible hosts, tmux/screen
without passthrough) end up dropping the URL from rendered markdown
links entirely, since the fallback 'text (url)' rendering is skipped
whenever hyperlinks is true.

- Unknown terminals now default to hyperlinks: false.
- tmux and screen (TMUX env, TERM starting with tmux/screen) force
  hyperlinks: false even when the outer terminal would otherwise
  advertise OSC 8 support. Image protocols also left disabled.
- Added detectCapabilities tests covering the known-capable set and
  the tmux/screen/unknown cases.
@badlogic

Copy link
Copy Markdown
Collaborator

Merged. Pushed a follow-up in 30a8a41 to address the unknown-terminal default:

  • detectCapabilities() now returns hyperlinks: false for unknown terminals. The previous fallback returned true, which caused terminals that silently swallow OSC 8 to drop the URL from rendered markdown links entirely (the text (url) fallback only runs when hyperlinks is false).
  • tmux and screen force hyperlinks: false (and images: null) regardless of the outer terminal, since OSC 8 passthrough is opt-in and image protocols are unreliable there.
  • Added detectCapabilities tests covering tmux (TMUX env), TERM=tmux-*, TERM=screen-*, unknown terminals, and the positive set (Ghostty, Kitty, WezTerm, iTerm2, VSCode).

550/550 tui tests pass.

@badlogic badlogic removed the inprogress Issue is being worked on label Apr 16, 2026
badlogic added a commit that referenced this pull request Apr 16, 2026
Add missing entries and cross-package duplications:
- after_provider_response extension hook (#3128)
- Compact startup header with Ctrl+O toggle (#3267)
- preset example: restore original state on (none) (#3272)
- OSC 8 hyperlinks in markdown (#3248)
- Hyperlink capability detection tightening (#3248)
- OpenAI Responses session_id headers for proxies (#3264)

Add New Features summary at the top of [Unreleased].
durdn pushed a commit to durdn/pi-mono that referenced this pull request Apr 21, 2026
…em (earendil-works#3248)

TerminalCapabilities already tracks hyperlinks: boolean and returns true
for Ghostty, Kitty, WezTerm, and iTerm2, but nothing generated OSC 8
sequences. This completes that stub.

Changes to packages/tui:
- terminal-image.ts: add hyperlink(text, url) and setCapabilities()
- index.ts: export hyperlink and setCapabilities
- utils.ts: extend AnsiCodeTracker to track active OSC 8 URLs
  - process() now handles OSC 8 open/close sequences
  - getActiveCodes() re-emits the OSC 8 open at each line start
  - getLineEndReset() closes the OSC 8 hyperlink before each line break
  This ensures hyperlinks wrap correctly across multiple lines.
- components/markdown.ts: link renderer uses hyperlink() when
  getCapabilities().hyperlinks is true; falls back to (url) text
- Tests: new wrap-ansi tests for OSC 8 line-wrapping; terminal-image
  tests for hyperlink(); markdown tests covering both code paths;
  table-cell width test pinned to hyperlinks:false (checks raw columns)

closes earendil-works#3239

Co-authored-by: AI (Pi/Claude Sonnet 4.6) <noreply@pi.dev>
Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
durdn pushed a commit to durdn/pi-mono that referenced this pull request Apr 21, 2026
OSC 8 hyperlinks landed in earendil-works#3248, but detectCapabilities() returned
hyperlinks: true in the unknown-terminal fallback. Terminals that
silently swallow OSC 8 (most xterm-compatible hosts, tmux/screen
without passthrough) end up dropping the URL from rendered markdown
links entirely, since the fallback 'text (url)' rendering is skipped
whenever hyperlinks is true.

- Unknown terminals now default to hyperlinks: false.
- tmux and screen (TMUX env, TERM starting with tmux/screen) force
  hyperlinks: false even when the outer terminal would otherwise
  advertise OSC 8 support. Image protocols also left disabled.
- Added detectCapabilities tests covering the known-capable set and
  the tmux/screen/unknown cases.
durdn pushed a commit to durdn/pi-mono that referenced this pull request Apr 21, 2026
Add missing entries and cross-package duplications:
- after_provider_response extension hook (earendil-works#3128)
- Compact startup header with Ctrl+O toggle (earendil-works#3267)
- preset example: restore original state on (none) (earendil-works#3272)
- OSC 8 hyperlinks in markdown (earendil-works#3248)
- Hyperlink capability detection tightening (earendil-works#3248)
- OpenAI Responses session_id headers for proxies (earendil-works#3264)

Add New Features summary at the top of [Unreleased].
byte-rose pushed a commit to byte-rose/pi-mono that referenced this pull request Apr 28, 2026
…em (earendil-works#3248)

TerminalCapabilities already tracks hyperlinks: boolean and returns true
for Ghostty, Kitty, WezTerm, and iTerm2, but nothing generated OSC 8
sequences. This completes that stub.

Changes to packages/tui:
- terminal-image.ts: add hyperlink(text, url) and setCapabilities()
- index.ts: export hyperlink and setCapabilities
- utils.ts: extend AnsiCodeTracker to track active OSC 8 URLs
  - process() now handles OSC 8 open/close sequences
  - getActiveCodes() re-emits the OSC 8 open at each line start
  - getLineEndReset() closes the OSC 8 hyperlink before each line break
  This ensures hyperlinks wrap correctly across multiple lines.
- components/markdown.ts: link renderer uses hyperlink() when
  getCapabilities().hyperlinks is true; falls back to (url) text
- Tests: new wrap-ansi tests for OSC 8 line-wrapping; terminal-image
  tests for hyperlink(); markdown tests covering both code paths;
  table-cell width test pinned to hyperlinks:false (checks raw columns)

closes earendil-works#3239

Co-authored-by: AI (Pi/Claude Sonnet 4.6) <noreply@pi.dev>
Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
byte-rose pushed a commit to byte-rose/pi-mono that referenced this pull request Apr 28, 2026
OSC 8 hyperlinks landed in earendil-works#3248, but detectCapabilities() returned
hyperlinks: true in the unknown-terminal fallback. Terminals that
silently swallow OSC 8 (most xterm-compatible hosts, tmux/screen
without passthrough) end up dropping the URL from rendered markdown
links entirely, since the fallback 'text (url)' rendering is skipped
whenever hyperlinks is true.

- Unknown terminals now default to hyperlinks: false.
- tmux and screen (TMUX env, TERM starting with tmux/screen) force
  hyperlinks: false even when the outer terminal would otherwise
  advertise OSC 8 support. Image protocols also left disabled.
- Added detectCapabilities tests covering the known-capable set and
  the tmux/screen/unknown cases.
byte-rose pushed a commit to byte-rose/pi-mono that referenced this pull request Apr 28, 2026
Add missing entries and cross-package duplications:
- after_provider_response extension hook (earendil-works#3128)
- Compact startup header with Ctrl+O toggle (earendil-works#3267)
- preset example: restore original state on (none) (earendil-works#3272)
- OSC 8 hyperlinks in markdown (earendil-works#3248)
- Hyperlink capability detection tightening (earendil-works#3248)
- OpenAI Responses session_id headers for proxies (earendil-works#3264)

Add New Features summary at the top of [Unreleased].
larsboes pushed a commit to larsboes/pi-mono that referenced this pull request Apr 30, 2026
…em (earendil-works#3248)

TerminalCapabilities already tracks hyperlinks: boolean and returns true
for Ghostty, Kitty, WezTerm, and iTerm2, but nothing generated OSC 8
sequences. This completes that stub.

Changes to packages/tui:
- terminal-image.ts: add hyperlink(text, url) and setCapabilities()
- index.ts: export hyperlink and setCapabilities
- utils.ts: extend AnsiCodeTracker to track active OSC 8 URLs
  - process() now handles OSC 8 open/close sequences
  - getActiveCodes() re-emits the OSC 8 open at each line start
  - getLineEndReset() closes the OSC 8 hyperlink before each line break
  This ensures hyperlinks wrap correctly across multiple lines.
- components/markdown.ts: link renderer uses hyperlink() when
  getCapabilities().hyperlinks is true; falls back to (url) text
- Tests: new wrap-ansi tests for OSC 8 line-wrapping; terminal-image
  tests for hyperlink(); markdown tests covering both code paths;
  table-cell width test pinned to hyperlinks:false (checks raw columns)

closes earendil-works#3239

Co-authored-by: AI (Pi/Claude Sonnet 4.6) <noreply@pi.dev>
Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
larsboes pushed a commit to larsboes/pi-mono that referenced this pull request Apr 30, 2026
OSC 8 hyperlinks landed in earendil-works#3248, but detectCapabilities() returned
hyperlinks: true in the unknown-terminal fallback. Terminals that
silently swallow OSC 8 (most xterm-compatible hosts, tmux/screen
without passthrough) end up dropping the URL from rendered markdown
links entirely, since the fallback 'text (url)' rendering is skipped
whenever hyperlinks is true.

- Unknown terminals now default to hyperlinks: false.
- tmux and screen (TMUX env, TERM starting with tmux/screen) force
  hyperlinks: false even when the outer terminal would otherwise
  advertise OSC 8 support. Image protocols also left disabled.
- Added detectCapabilities tests covering the known-capable set and
  the tmux/screen/unknown cases.
larsboes pushed a commit to larsboes/pi-mono that referenced this pull request Apr 30, 2026
Add missing entries and cross-package duplications:
- after_provider_response extension hook (earendil-works#3128)
- Compact startup header with Ctrl+O toggle (earendil-works#3267)
- preset example: restore original state on (none) (earendil-works#3272)
- OSC 8 hyperlinks in markdown (earendil-works#3248)
- Hyperlink capability detection tightening (earendil-works#3248)
- OpenAI Responses session_id headers for proxies (earendil-works#3264)

Add New Features summary at the top of [Unreleased].
PSU3D0 added a commit to PSU3D0/pi-mono that referenced this pull request May 13, 2026
Consolidates 62 upstream commits (v0.67.4 -> v0.67.68) into the fork.
Preserves all fork-local work (OAuth cloak, antigravity pool, context
tiers, OSC 8 file hyperlinks, gemini3 sig skip, codex_cli_rs headers,
compaction context hooks, gpt-5.4 models).

Conflict resolutions (4 files):

- packages/ai/src/providers/google-gemini-cli.ts: layered upstream's
  onResponse hook call into our pool-aware retry loop; kept our
  finally-block pool.saveNow() path.

- packages/tui/src/utils.ts: adopted upstream's AnsiCodeTracker OSC 8
  impl wholesale. It's cleaner than our f16dfd5 version (preserves
  hyperlink across SGR reset via separate clear(), emits ST-terminated
  sequences). Dropped our 7d75fee SEGMENT_RESET fix since upstream's
  design never introduced the bug.

- packages/tui/src/components/markdown.ts: kept our resolveHref hook
  and wrapHyperlink helper but gated OSC 8 emission on
  getCapabilities().hyperlinks; switched to upstream's hyperlink()
  helper for consistency. Merges file path linking (ours) with
  terminal capability detection (upstream earendil-works#3248, #30a8a41f).

- packages/tui/test/{markdown,wrap-ansi}.test.ts: updated our OSC 8
  assertions to ST-terminated sequences (\x1b\\) to match new emitter;
  added setCapabilities({hyperlinks:true}) gating where needed;
  afterEach resetCapabilitiesCache to isolate test state.

Dependency upgrades from upstream:
- @anthropic-ai/sdk 0.73.0 -> 0.90.0
- @aws-sdk/client-bedrock-runtime 3.983.0 -> 3.1030.0
- @mistralai/mistralai 1.14.1 -> 2.2.0

Upstream features folded in:
- fix(ai): Opus 4.7 adaptive thinking + xhigh effort (earendil-works#3286)
- feat(ai): thinkingDisplay option (summarized/omitted/raw)
- feat(coding-agent): after_provider_response hook (earendil-works#3128)
- fix(ai): trust requested Codex service tier (earendil-works#3307)
- feat(bedrock): Bearer token auth for Converse API (earendil-works#3125)
- feat(agent,coding-agent): per-tool executionMode override (earendil-works#3345)
- feat(tui): OSC 8 hyperlinks with terminal capability detection
  (earendil-works#3248, #30a8a41f)
- feat(coding-agent,tui): argument-hint frontmatter in prompts (earendil-works#2780)
- Plus ~40 smaller fixes across ai/coding-agent/tui/agent.

Verification:
- packages/tui: 559/559 tests pass
- packages/ai: 144/144 OAuth cloak tests pass; all fork-local suites
  pass (antigravity-pool, context-tiers, gemini3-unsigned-tool-call,
  codex-stream, supports-xhigh, etc.). Remaining test failures (13)
  are pre-existing live-API E2E tests (no credentials in this env).
- packages/agent: 39/39 tests pass.
- packages/coding-agent: 1020/1021 tests pass; 1 flaky bash timeout
  test (passes in isolation), unrelated to merge.
- All four packages build cleanly.
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.

feat(tui): use OSC 8 hyperlinks in Markdown when terminal supports them

2 participants