Skip to content

TUI: make markdown links clickable via OSC 8 hyperlinks #580

@bug-ops

Description

@bug-ops

Problem

Markdown links [text](url) in TUI chat messages are parsed by pulldown-cmark and styled (cyan + underline), but the URL is discarded after styling. The link text is visually distinct but not clickable in the terminal.

Bare URLs (e.g. https://github.com/...) are already clickable via regex detection + OSC 8 escape sequences in hyperlink.rs.

This means long URLs from LLM responses (PR links, documentation links, etc.) cannot be shortened to readable text like [PR #570](https://github.com/bug-ops/zeph/pull/570) — users either see the full URL or a styled but non-clickable label.

Current flow

pulldown-cmark parser
  → MdRenderer captures link_url in Event::Start(Tag::Link)
  → Text styled with theme.link (cyan + underline)
  → Event::End(TagEnd::Link) clears link_url
  → URL is discarded — never reaches hyperlink system

Proposed solution

Collect (rendered_text_range, url) pairs during markdown rendering and merge them into the hyperlink collection alongside regex-detected bare URLs.

Changes

  1. widgets/chat.rsMdRenderer

    • Add a field link_spans: Vec<(String, String)> to accumulate (display_text, url) pairs
    • On Event::End(TagEnd::Link), push the captured text and URL into link_spans
    • Expose link_spans via finish() return value (or a separate accessor)
  2. widgets/chat.rsrender_md() / render_chat_message()

    • Propagate the collected link spans up to the caller
    • Store them alongside rendered lines so collect_from_buffer can use them
  3. hyperlink.rscollect_from_buffer()

    • Accept an additional parameter: pre-resolved markdown link mappings
    • For spans matching a known markdown link text at the expected position, use the stored URL instead of regex detection
    • Regex detection continues to handle bare URLs as before
  4. No changes needed in write_osc8() — it already handles arbitrary HyperlinkSpan entries

Scope

  • ~50-80 lines of changes across 2 files
  • No new dependencies
  • Backward compatible: bare URL detection unchanged

Acceptance criteria

  • [text](url) in assistant messages renders as clickable OSC 8 hyperlink with text as display
  • Bare URLs remain clickable as before
  • Nested markdown formatting inside links preserved (e.g. [**bold link**](url))
  • Existing hyperlink tests pass; new tests cover markdown link collection

Related files

  • crates/zeph-tui/src/hyperlink.rs — URL detection, OSC 8 output
  • crates/zeph-tui/src/widgets/chat.rsMdRenderer, markdown rendering pipeline
  • crates/zeph-tui/src/lib.rs — hyperlink emission in render loop

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttuiTUI dashboard

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions