Skip to content

fix(chat): render footnote citations under a single Sources header - #987

Merged
spashii merged 3 commits into
mainfrom
fix/footnote-citation-rendering
Aug 10, 2026
Merged

fix(chat): render footnote citations under a single Sources header#987
spashii merged 3 commits into
mainfrom
fix/footnote-citation-rendering

Conversation

@spashii

@spashii spashii commented Aug 10, 2026

Copy link
Copy Markdown
Member

What this fixes

Follow-up to #981. On the reported chat (57493241, dashboard) the agentic reply showed two headings ("Sources" and "Footnotes"), the superscript citation links gave no feedback when clicked, and every source line stacked several arrow icons (the external-link arrow plus one ↩ back-reference per citation).

Why it happened

  • The prompt told the model to write a literal "Sources" header above its footnote definitions. remark-gfm then moves the definitions into its own trailing section headed "Footnotes", so the model's header stays behind as an empty duplicate.
  • The renderer's "Footnotes" heading ships with an sr-only class, which our Tailwind build never generates (no source file uses it), so the heading that is meant to be invisible is visible.
  • The superscripts use fragment navigation. Every message mints the same ids (#user-content-fn-1), a repeated click on the same fragment does nothing, and when the footnote list is already on screen (the exact case in the reported chat, verified against the stored row in prod) nothing visibly happens at all.

What changed

  • Markdown.tsx takes an optional footnote label and id prefix and passes them through remark-rehype: the section heading is ours now (visible, localised) and footnote ids are unique per message. Surfaces that pass no label keep the default "Footnotes" heading but hidden behind dembrane-sr-only, a visually-hidden class we define ourselves in index.css (our own name so it cannot collide with Tailwind's sr-only utility).
  • ChatHistoryMessage.tsx passes Sources (per locale, Bronnen in Dutch, other catalogs fall back to English until translated) plus a per-message id prefix. A superscript click scrolls to the source line and flashes it with the same cyan highlight the transcript page uses for a deep-linked chunk — feedback even when the target was already on screen, no URL rewrite, no history entries. The ↩ back-references are dropped in agentic chat: one icon per source line.
  • agent.py no longer asks the model for a header above the definitions; the renderer owns the heading. Prompt test updated to pin this.

Already-persisted rows

Exactly one row in all of production stores the old model-written header (verified by query): the beta test chat this was reported on. Agentic chat is beta; the row stays as is. No migration, no render-time compatibility code.

Tests

  • AgenticChatPanel.test.tsx: 15 passed, including a new case pinning that footnote definition tags become rich transcript links.
  • agent/tests/test_agent_tools.py: 65 passed.
  • tsc --noEmit and biome lint clean; end-to-end renders verified: one "Sources" heading in chat, hidden "Footnotes" heading on label-less surfaces, unique prefixed ids.

Discussion: https://dembraneworkspace.slack.com/archives/C0884QPQF6W/p1786381186184369

The footnote citations from #981 rendered with two headings: the model
wrote a literal "Sources" line above its footnote definitions, and
remark-gfm then moved the definitions into its own trailing section
headed "Footnotes". That default heading is meant to be visually hidden
behind `sr-only`, a class our Tailwind build never generates because it
appears in no source file, so both headings showed. The superscript
links also gave no feedback: fragment navigation resolves against ids
that repeat in every message (`#user-content-fn-1` in each one), does
nothing on a repeated click of the same fragment, and scrolls nothing
when the target is already on screen.

- Markdown.tsx accepts a footnote label and id prefix and passes them
  through remark-rehype: the section heading is now ours (localised,
  visible) and every message mints unique footnote ids.
- ChatHistoryMessage passes "Sources" (per locale, "Bronnen" in Dutch)
  and a per-message prefix, and intercepts in-message footnote hops to
  scroll directly, leaving the URL and history alone.
- enrichAgenticContent strips a model-written "Sources"/"Footnotes"
  heading when footnote definitions follow, which repairs messages
  already persisted under the old prompt ("*Sources*" verbatim in the
  production row this was reported on).
- agent.py stops asking the model for a header above the definitions;
  the renderer owns the heading now.
The regex that removed a model-written "Sources"/"Footnotes" heading
from already-persisted messages was a shim: it compensated in render
code, forever, for a few rows written in the hours between #981 and
this fix, and it only knew the English words. The handful of affected
rows get a one-off data cleanup instead; the renderer keeps owning the
header for everything written from now on.
…eading app-wide

Three refinements on the footnote citations:

- A superscript click now scrolls to the source line and flashes it with
  the same cyan highlight the transcript page gives a deep-linked chunk,
  so the hop gives feedback even when the target was already on screen.
- The ↩ back-reference links under each source are dropped in agentic
  chat: they stacked a second, third, Nth arrow icon on every source
  line, and after an in-place highlight scroll the superscript that
  brought the reader down is still visible anyway. One icon per source.
- Markdown surfaces that do not name a footnote label now hide the
  renderer's default "Footnotes" heading behind `dembrane-sr-only`, our
  own visually-hidden class in index.css. Tailwind owns `sr-only` and
  only generates classes it sees in source, so runtime-injected markup
  needs a class we define unconditionally.
@spashii
spashii enabled auto-merge August 10, 2026 18:37
@spashii
spashii merged commit c4d184d into main Aug 10, 2026
11 checks passed
@spashii
spashii deleted the fix/footnote-citation-rendering branch August 10, 2026 18:37
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