Skip to content

feat(dom): opt-in linkify for clickable URLs in terminal output#42

Open
den1k wants to merge 11 commits into
vercel-labs:mainfrom
den1k:feat/dom-linkify
Open

feat(dom): opt-in linkify for clickable URLs in terminal output#42
den1k wants to merge 11 commits into
vercel-labs:mainfrom
den1k:feat/dom-linkify

Conversation

@den1k
Copy link
Copy Markdown

@den1k den1k commented Apr 22, 2026

Summary

Adds an opt-in linkify option to @wterm/dom's WTerm / Renderer that turns http(s)://… URLs in terminal output into real <a href> anchors. Pure renderer-side change — no wasm, no @wterm/core, no Cell-struct changes.

Design

  • New module packages/@wterm/dom/src/linkify.ts: DEFAULT_URL_PATTERN, pure findUrls() / trimTrailing() helpers, LinkifyOption / LinkifyConfig types.
  • Renderer._buildRowContent gains a pre-pass that identifies URL ranges in the row text and flushes runs at URL boundaries (in addition to style boundaries). URL-internal runs are appended to a shared <a class="term-link" target="_blank" rel="noopener noreferrer"> element; onClick fires before the browser's default navigation.
  • CSS rules under .wterm a.term-link give a subtle dotted-underline-on-hover treatment that inherits the terminal foreground color.
  • Default is off — existing consumers see no change unless they pass linkify: true.

Scope / limitations

  • Per-row scoping: URLs that wrap across terminal lines are rendered as two separate (broken) anchors. Documented.
  • Explicit-scheme only (https?://). No www.-prefix autodetection or OSC 8 hyperlink support — the latter would need a new Cell slot on the core side and is out of scope for this PR.

Tests

Unit (vitest + jsdom): packages/@wterm/dom/src/__tests__/linkify.test.ts (13 helper tests) and additions to renderer.test.ts covering: single URL, multiple URLs, styled URL, trailing-punctuation trimming, linkify disabled, custom regex, cursor inside anchor, scrollback-row anchors, block-glyph col alignment, onClick. Total +23 tests; @wterm/dom goes from 68 → 91 passing. No Playwright e2e added — anchor rendering is fully observable in jsdom and browser-native behavior (target=\"_blank\", CSS hover) isn't our code to test.

Docs

  • packages/@wterm/dom/README.md — new "Clickable links" section.
  • apps/docs/src/app/api-reference/page.mdxlinkify row in Terminal Options table.
  • apps/docs/src/app/vanilla/page.mdx — "Clickable Links" subsection with examples.
  • CHANGELOG.md — "Unreleased" entry.

Test plan

  • pnpm -r test green (@wterm/react has 14 pre-existing test failures on main unrelated to this change — React 19 + Node 24 + testing-library compat)
  • pnpm -r type-check green
  • pnpm -r build green

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

@den1k is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread packages/@wterm/dom/src/renderer.ts Outdated
den1k and others added 2 commits April 24, 2026 21:23
Preserves the col→rowText 1:1 mapping that urlIdxAt() relies on.
Code points above U+FFFF (emoji, supplementary plane) encode as
surrogate pairs in JS strings (2 UTF-16 code units), which would
make a single cell contribute 2 string indices and shift every
subsequent column's URL mapping.

Per VADE review on vercel-labs#42.
Linkify previously ran per-row, so a URL that hard-wrapped at the column
boundary became two anchors with two truncated hrefs (or none, when the
second row started without https://). Group consecutive rows where the
last column was written into ('continuesNext' heuristic), run the URL
regex once on the joined text, and emit one anchor per row segment all
sharing the same full href. Repaint a row when its URL ranges change so
edits on row N+1 propagate to row N's anchor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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