Symbol anchors UX: make it consistent across the entire app
Symbol anchors UX: make it consistent across the entire app
Background
The issues detail page has a polished, production-quality Symbol Anchors panel:
- Each anchor is a clickable deep-link that navigates directly to the symbol in the blob view (
ar.blob_url()) - Addresses are parsed and structured via
parse_symbol_anchor()— rendered asfile_path :: symbol_name, not a raw truncated string - Cross-repo references are handled —
owner/repo::file::symboldisplays with the repo prefix - Each anchor carries live intelligence stats: blast radius (
⬡ N), churn last 30d (↻ N), last author — sourced fromanchor_intel - A
↗arrow gives a clear visual affordance that the item is navigable
The mist detail page previously had a symbol anchors panel that was the opposite: raw address strings, truncated to 50 chars, all starting with the filename so they looked identical, and not clickable. It has been removed (issue #103 covers replacing it with a proper markdown TOC). But the underlying problem is architectural: there is no shared component for symbol anchors — each page that wants to show them has to reimplement the panel from scratch, and the mist/proposal pages never got the full implementation.
The correct fix is to extract the issues-page symbol anchors panel into a reusable Jinja2 macro and apply it consistently everywhere symbol anchors can appear.
Goal
- One canonical symbol anchors panel implementation, defined once
- Applied consistently to: mist detail, proposal detail, and any future page that surfaces
symbol_anchors - Issues detail page continues to work exactly as today — it becomes the reference implementation
- No page-specific hacks, no raw address strings, no truncation
Existing superior implementation (issues detail)
Source: musehub/templates/musehub/pages/issue_detail.html, around line 174
{% for anchor in symbol_anchors %}
{%- set ar = parse_symbol_anchor(anchor) %}
{%- set blob_href = ar.blob_url(site, owner, repo_slug) %}
{%- set ai = anchor_intel.get(anchor, {}) %}
<a href="{{ blob_href }}" class="isd-anchor{% if ar.is_cross_repo %} isd-anchor--cross-repo{% endif %}">
<span class="isd-anchor-addr">
{%- if ar.is_cross_repo %}
<span class="isd-anchor-repo">{{ ar.owner }}/{{ ar.repo }}</span>
<span class="isd-anchor-sep">::</span>
{%- endif %}
<span class="isd-anchor-file">{{ ar.file_path }}</span>
{%- if ar.symbol_name %}
<span class="isd-anchor-sep">::</span>
<span class="isd-anchor-sym">{{ ar.symbol_name }}</span>
{%- endif %}
{%- if ar.ref %}<span class="isd-anchor-ref">@{{ ar.ref }}</span>{%- endif %}
</span>
<span class="isd-anchor-stats">
{%- if blast is not none %}<span class="isd-anchor-stat" title="Blast radius">⬡ {{ blast }}</span>{%- endif %}
{%- if churn is not none %}<span class="isd-anchor-stat" title="Churn last 30d">↻ {{ churn }}</span>{%- endif %}
{%- if ai.last_author %}<span class="isd-anchor-author">{{ ai.last_author }}</span>{%- endif %}
</span>
<span class="isd-anchor-arrow">↗</span>
</a>
{% endfor %}
This is the design spec. The macro must produce identical output.
Design
Shared macro
Extract the above into a reusable macro in a new partial:
musehub/templates/musehub/partials/symbol_anchors_panel.html
{% macro symbol_anchors_panel(symbol_anchors, anchor_intel, owner, repo_slug) %}
{% if symbol_anchors and symbol_anchors | length > 0 %}
<div class="ms-sidebar-card">
<div class="ms-sidebar-card-title">
{{ icon("radio", 13) }}
Symbol Anchors
<span class="ms-sidebar-count">{{ symbol_anchors | length | fmtnum }}</span>
</div>
<div class="ms-anchor-list">
{% for anchor in symbol_anchors %}
{%- set ar = parse_symbol_anchor(anchor) %}
{%- set site = site_base_url() %}
{%- set blob_href = ar.blob_url(site, owner, repo_slug) %}
{%- set ai = anchor_intel.get(anchor, {}) if anchor_intel else {} %}
<a href="{{ blob_href }}" class="ms-anchor{% if ar.is_cross_repo %} ms-anchor--cross-repo{% endif %}">
<span class="ms-anchor-addr">
{%- if ar.is_cross_repo %}
<span class="ms-anchor-repo">{{ ar.owner }}/{{ ar.repo }}</span>
<span class="ms-anchor-sep">::</span>
{%- endif %}
<span class="ms-anchor-file">{{ ar.file_path }}</span>
{%- if ar.symbol_name %}
<span class="ms-anchor-sep">::</span>
<span class="ms-anchor-sym">{{ ar.symbol_name }}</span>
{%- endif %}
{%- if ar.ref %}
<span class="ms-anchor-ref">@{{ ar.ref }}</span>
{%- endif %}
</span>
{%- set blast = ai.blast if ai.blast is defined and ai.blast is not none else none %}
{%- set churn = ai.churn_30d if ai.churn_30d is defined and ai.churn_30d is not none else none %}
<span class="ms-anchor-stats">
{%- if blast is not none %}<span class="ms-anchor-stat" title="Blast radius">⬡ {{ blast | fmtnum }}</span>{%- endif %}
{%- if churn is not none %}<span class="ms-anchor-stat" title="Churn last 30d">↻ {{ churn | fmtnum }}</span>{%- endif %}
{%- if ai.last_author is defined and ai.last_author %}<span class="ms-anchor-author">{{ ai.last_author }}</span>{%- endif %}
</span>
<span class="ms-anchor-arrow">↗</span>
</a>
{% endfor %}
</div>
</div>
{% endif %}
{% endmacro %}
Route changes
Each route that renders a page with symbol anchors must supply anchor_intel — the
dict of {anchor_address: {blast, churn_30d, last_author}} already computed for the
issues page. Where that data is not yet fetched, pass an empty dict ({}) as a safe
default — the panel will render without stats but still be fully clickable.
Pages and routes to update:
| Page | Route file | Status |
|---|---|---|
| Issue detail | ui_issues.py (or equivalent) |
✅ already correct — reference impl |
| Mist detail | mists.py |
❌ panel removed; needs macro + anchor_intel from route |
| Proposal detail | proposal route | ❌ unknown — audit needed |
CSS
The existing isd-anchor* classes on issues are page-scoped. The macro uses
ms-anchor* (the global namespace). Either:
a) Alias isd-anchor → ms-anchor in CSS (one rename, both pages consistent), or
b) Duplicate the styles under ms-anchor* and remove isd-anchor* later
Option (a) is cleaner — do it in one pass.
Phases
Phase 1 — Extract macro and update mist detail
SA_01Createmusehub/templates/musehub/partials/symbol_anchors_panel.htmlwith the shared macroSA_02Updatemuse_detail.htmlto import and call the macro; passsymbol_anchorsandanchor_intel(empty dict default) from the routeSA_03Updatemists.pyroute to compute and passanchor_intelfor mist symbol anchorsSA_04CSS: ensurems-anchor*styles are defined (alias or copy fromisd-anchor*)SA_05Smoke test: open a mist withsymbol_anchorson staging — confirm panel renders, links resolve to correct blob+symbol URL
Phase 2 — Proposal detail
SA_06Audit proposal detail template for existing symbol anchors renderingSA_07Apply macro to proposal detail; update route to passanchor_intelSA_08Smoke test: open a proposal with linked symbols on staging
Phase 3 — Issues page CSS cleanup
SA_09Migrateisd-anchor*→ms-anchor*inissue_detail.htmland its CSSSA_10Verify issue detail page is visually identical after the rename
Acceptance criteria
- A mist with
symbol_anchorsshows the same panel design as the issues page: clickable links,file :: symbolstructure, blast/churn stats where available - A mist with no
symbol_anchors(or an empty list) shows no panel — no empty card - A proposal with
symbol_anchorsshows the same panel - There is exactly one place in the template codebase where the symbol anchors panel is defined
Out of scope
- Adding symbol anchor extraction to mists that don't already have them (separate feature)
- The markdown heading TOC for mists — that is issue #103
- Blob view symbol navigation — that is issue #100 (
#::Namesyntax)