Skip to content

Markdown renderer externalizes in-document anchors (target="_blank") and emits headings without ids, so TOC links never scroll #57971

Description

@vernikr-personal

Summary

The shared markdown base renderer wraps every link — including in-document
anchors like [Install](#install) — in
<a href="…" class="external-link" target="_blank" rel="noopener noreferrer">
(packages/ui/src/context/marked-base.tsx:6-10), and headings come out of marked's
default renderer with no id attribute. Nothing in packages/session-ui or
packages/app handles #… clicks or resolves heading slugs (a search for
startsWith("#") / anchor scrolling in those packages only matches theme/color
code), and packages/session-ui/src/components/markdown.tsx adds no ids in
post-processing either.

Consequence: a table of contents inside rendered markdown (session timeline, and
anywhere the markdown worker's output is shown) is decorative. Clicking an entry
opens a useless new tab at the app URL plus the hash on web, and on desktop the
popup is denied or produces an empty window; even after navigating, no element
matches the hash because headings carry no ids.

Repro

  1. In any session, render markdown containing a TOC, e.g. an assistant answer or a
    displayed document starting with:

    ## Contents
    - [Install](#install)
    - [Usage](#usage)
    
    ## Install
    …
    ## Usage
    …
  2. Click "Install" in the rendered output.

Expected: the view scrolls to the ## Install heading (the link resolves inside the
document).
Actual: a new tab/window opens at the app origin with #install appended (web) or
nothing happens (desktop); the target heading has no anchor in either case.

Root cause

  • packages/ui/src/context/marked-base.tsx:9 — target="_blank" rel="noopener noreferrer" is applied unconditionally, with no branch for href values that
    start with #.
  • Marked's default heading output has no id; no slug pass exists downstream
    (packages/session-ui/src/components/markdown.tsx post-processes code links and
    favicons, not headings).
  • No click delegation for in-document anchors on any markdown surface, so there is
    no fallback resolution either.

Proposed fix

  1. In createMarkdownBase's link renderer, skip target/rel when
    href.trim().startsWith("#") so anchors stay in-document.
  2. Emit GitHub-style slug ids on headings in the renderer (lowercase, drop
    non-word/non-space punctuation, spaces to dashes, dedupe per document), giving
    anchors a real target.
  3. Where markdown renders inside a scroll container, delegate clicks on #… links
    to scroll that container, with a slug-of-heading-text fallback for documents
    rendered before ids existed.

Downstream impact

The same contract was inherited by downstream apps built around OpenCode's markdown
pipeline: e.g. omnirush-ai/omnirush-gui renders every link with target="_blank"
and no heading ids, and its file-preview surface shipped with dead TOC links until
it added local click handling plus slug lookup
(vernikr/omnirush-gui@947f078). Fixing the renderer contract here would let every
surface — and downstream ports that follow it — inherit working anchors instead of
each re-implementing click delegation.

Activity

  1. opencode-agent commented on Oct 22, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed write-up, @vernikr-personal. You're asking for links inside a document, like a table of contents with [Install](#install), to scroll to the matching heading in rendered markdown. Right now that can't work for two reasons:

    • The shared renderer gives every link target="_blank" and treats it as external.
    • Headings are rendered without ids, so there's nothing for the link to land on.

    That matches how the renderer works today: it was built to treat every link as external and was never meant to support in-document navigation. So I've filed this as a feature request rather than a bug. Your proposal (keeping # links in the page, adding slug ids to headings and scrolling the containing view when one is clicked) is a clear outline of what it would take. Maintainers will consider it.

    Related (not duplicates):

  2. vernikr-personal commented on Oct 22, 2026

    @vernikr-personal
    Author

    Thanks for the fast triage — the feature-request framing is fine; the outcome (working in-document navigation) matters more than the label. A few additions that may help prioritization:

    1. A working reference implementation exists in a shipped app. OmniRush.ai (desktop app built on OpenCode, markdown pipeline following the same contract) hit the same dead-TOC behaviour in its file-preview surface and shipped a fix in vernikr/omnirush-gui@947f078 (release v3.7.4): click delegation on the preview's scroll container plus a GitHub-style slug-of-heading-text fallback for headings without ids (~60 lines, no renderer changes). So the "scroll the containing view" half is proven in production; the renderer half (slug ids on headings + not externalizing # hrefs) is what would make anchors work everywhere, including the session timeline.
    2. Agent-generated documents make this routine. Sessions routinely produce long markdown with a TOC (plans, proposals, READMEs); without in-document anchors every such TOC is decorative in all rendered surfaces. Same motivation as related Desktop: file:// markdown links are not clickable #44902 (file:// markdown links on Desktop).
    3. Happy to contribute a PR for the three steps from the issue body (skip target/rel for # hrefs in createMarkdownBase, slug ids with per-document dedupe in the heading renderer, a small click-delegation helper for scroll containers) — behind a flag if a gradual rollout is preferred.

    I checked #46550, #57211 and #44902: none of them covers anchor resolution or heading ids, so this can stay as the umbrella for in-document navigation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions