feat(core): support clickable Markdown links - #1261
Draft
kitlangton wants to merge 4 commits into
Draft
Conversation
kitlangton
force-pushed
the
markdown-links
branch
2 times, most recently
from
July 12, 2026 03:40
17ea52b to
0d13c28
Compare
kitlangton
force-pushed
the
markdown-links
branch
from
July 12, 2026 04:19
0d13c28 to
4684f65
Compare
# Conflicts: # packages/core/package.json
Collaborator
|
Reworked this after tracing the actual failure path and measuring the complete conversion/native pipeline.
Verification: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Markdown labels and visible bare HTTP(S) URLs now carry OSC 8 terminal hyperlink metadata. Wrapped links keep one non-empty target ID across rows. Terminals without hyperlink support render the same readable text without OSC 8 bytes.
Before
The text is readable but not clickable.
After
OpenTUI emits OSC 8 metadata; the terminal owns interaction. macOS terminals commonly use Cmd-click, while terminals on other platforms commonly use Ctrl-click or a configured hyperlink gesture.
upstream/mainata0b90640Artifacts: main screen text, branch screen text, main raw ANSI + termctrl recording, branch raw ANSI + termctrl recording.
These captures predate the final hardening commit but remain representative of its benign visual and OSC behavior. Final-commit renderer-byte tests refresh the changed security evidence for table/image controls, entity concealment, exclusion adjacency, supported capability, unsupported capability, and wrapped IDs.
Behavior
detectLinksexport/signature&or numeric entities in a destinationHTTP:///HTTPS://TextChunk.link; control-bearing Marked fallback text cannot emit the attacker sequence[a&b](...)a&bcell remains linkedTable images remain incidental parser-owned behavior: when Marked emits an image token its admitted target is linked. Ordinary tree-sitter rendering does not reconstruct image or reference destinations.
How
admitLinkTargetis the single metadata boundary for tree-sitter explicit links, tree-sitter bare links, Marked link tokens, table links, and image tokens. It decodes Markdown/HTML entities, then rejects empty targets and C0, DEL, or C1 controls.detect-links.tsuses a case-insensitive scheme index only to locate small candidate regions. Marked 17's inline lexer owns GFM recognition, punctuation backpedaling, and the exactrawspan; no duplicate URL grammar remains.treeSitterToTextChunksapplies source-range metadata while constructing chunks. Conceal replacements now use that same path, preserving metadata across visible entity replacements.CodeRenderablewithfiletype === "markdown"enables source link detection. The publicCodeOptions.detectLinksfield and internal state were removed.LinkPool, wrapping, cell attributes, capability handling, and OSC 8 rendering are unchanged.Security
OSC 8 targets cross a terminal-control boundary. The centralized admission function decodes entities first so encoded controls cannot bypass validation, then rejects empty targets, C0 (
U+0000-U+001F), DEL (U+007F), and C1 (U+0080-U+009F). This boundary is used before every newTextChunk.linkmetadata entry path.The final native-byte regression sends a BEL + ESC OSC sequence through a bare table URL and entity-encoded BEL/ESC through an image token. It verifies that neither path creates an attacker target and that the attacker control sequence does not appear in renderer output. Unsupported terminals remain readable and emit no OSC 8.
This PR does not redesign or harden native
LinkPool; native renderer lifecycle/frame-reset work remains out of scope.Performance
Detector medians use 20 timed samples after 5 warmups on the same test hardware. The detector comparison is against the pre-PR implementation; times are milliseconds. The corrected URL-free rerun contains no URL at any position.
)]}suffixThe preserved public
detectLinksAPI was measured separately around the range-application cursor change. Each generated explicit link contributes two sorted source ranges and two source-ordered chunks, one forlabel-${i}and one forhttps://target-${i}.test/path.detectLinksdense explicitThe final public path remains near-linear as the fixture doubles, while the former search-from-range-zero application trends toward quadratic. The dense 1 MiB detector cost is about 50 ms on this hardware.
Real OpenCode workload sizes
Only aggregate character counts from local OpenCode histories were measured; no content is included. No prompts, text, identifiers, titles, paths, timestamps, projects, model names, or examples are included.
All main V1 figures are sampled; counts are not extrapolated to the full database. The full historical V1 dev scan had one 73,347-character text part at or above 64 KiB, but Markdown split it into smaller top-level blocks.
These are detector-only local medians over persisted final text, not full renderer timings and not a reconstruction of every streaming snapshot.
A 1 MiB block is a stress-test size: none was observed in the full V2 history, the full historical V1 dev history, or the sampled main V1 rows. The main V1 sample does not prove that no 1 MiB block exists in unsampled rows. Real block p99 is around 1–1.4 KiB; the meaningful residual risk remains an unusually large, continuously growing, URL-dense block. The synthetic worst-case benchmarks remain because they establish scaling and security boundaries.
The removed benchmark can be reproduced with these exact detector fixtures (truncated to the requested byte size):
The former “URL-free” generator selected
line % Infinity === 0; line zero therefore inserted one URL. Sparse and growing fixtures use the same line generator with frequency 100. The growing result is the cumulative cost of rescanning one single growing block at every 16 KiB snapshot through 1 MiB: 64 scans ofcontent.slice(0, end). Each snapshot is linear, but their cumulative work remains quadratic across snapshots. Normal incremental Markdown usually reuses stable blocks and limits rescans to trailing unstable blocks.Parser-owned GFM recognition has a measurable constant-factor cost while remaining sub-millisecond for genuinely URL-free 1 MiB and near-linear for static sparse, dense, and explicit inputs. The adversarial suffix path changes from roughly 4x time per 2x input to roughly 2x, and 48k falls from 4.42 seconds to 2.66 milliseconds. Residual risk remains for very large, continuously growing URL-dense blocks because the trailing unstable block must still be rescanned.
Scope
Simplify
The final simplify pass:
CodeOptions.detectLinksand its internal statedetectLinksrange-zero searches with a monotonic cursor+471/-179)Final line split: production/config
+196/-102; tests+275/-77. No reusable existing renderer-byte utility or entity decoder existed;entitiesis now an explicit runtime dependency for complete named/numeric entity decoding.Testing
bun run test:js: 4,987 passed, 23 existing skips, 0 failedbun run build:lib: passed, including declarations and copied tree-sitter queriesbun run lint:ci: passed with zero warningsbun run fmt:check: passedgit diff --check: passeddetectLinks1k/2k/4k/8k before/after cursor matrix plus corrected URL-free and dense 1 MiB detector reruns: final medians abovebun run test:native: attempted with repository-pinned Zig 0.15.2; this macOS/Xcode host hits the Zig build-runner linker failure (__availability_version_check,_abort, and other libSystem symbols) before repository code compilesbun run test:dist: blocked at its native build by the same Zig linker failure;build:libpassespackages/ssh/dist/package.jsonThe 158-line renderer-byte suite drives real Markdown through tree-sitter/Marked,
TextChunk, native text buffering/wrapping, capability handling,NativeSpanFeed, and captured stdout. It asserts exact non-empty IDs and targets rather than accepting arbitrary strings.