feat(wave2): citation trust layer — resurrect the signature feature#13
Merged
Merged
Conversation
The amber left-rule citation treatment in tokens.css has existed since the
design system landed. It had never rendered in a single chat response —
MessageBubble used plain ReactMarkdown with no renderer. This wave wires it
end to end, fixes the dead source-panel toggle, and rewrites the document
preview on the warm-stone palette.
## Citations actually render now
- MessageBubble parses assistant output for [Source N] / [N] patterns (case-
insensitive, with or without #) and:
- Wraps each sentence containing a citation in .cited with the amber
left-rule from tokens.css
- Replaces each marker with a CitationMarker — a small mono tabular-nums
chip that is clickable, keyboard-focusable, hover-styled, and carries
title=document_name for quick identification
- Degrades gracefully: non-cited sentences and mixed-children markdown
nodes pass through untouched; orphan marker indexes render as a muted
tooltip so we don't fake a source that wasn't retrieved
- AppShell owns a single openSource(source) function shared by:
- Citation marker click in MessageBubble → opens DocumentPreview with
highlight text already populated
- Source card click in SourcePanel → same code path
- Hover is two-way linked via hoveredSourceIndex shared state — hovering a
citation scrolls its card into view in the panel and highlights it; hovering
a card also drives the same visual.
- Citation marker click now passes a full SourceChunk (including notebook_id)
instead of just a path, which also fixes cross-notebook mode where
activeNotebookId was the wrong id for the clicked source.
## Source panel is toggleable and has an empty affordance
- SourcePanel.tsx now respects sourcePanelOpen from the store. Cmd+/ (and
the overflow menu) actually did nothing for the last release; the
component returned null purely based on source count. Fixed.
- When toggled open but sources are empty (pre-first-query), we render a
serif-italic "No sources yet" headline with a muted hint, so the 280px of
right-side real estate isn't unexplained blankness.
- Slide-in + fade animation on mount so toggling back on doesn't feel like
a layout jump; cards lift on hover with a subtle cite-glow.
- Relevance colors migrated to tokens (--color-accent / --color-cite /
--color-text-muted). Score is also clamped to 0-100 defensively — if the
backend ever returns a 0-1 float, bars no longer render invisibly.
- [N] marker added to each card header so the user can map a citation
chip's number to the right card visually.
## DocumentPreview on-system rewrite
- DocumentPreview.css rewritten top-to-bottom on warm-stone tokens. Every
value was a cold neutral (#1f1f1f, #e5e5e5, #a3a3a3) — it now matches the
rest of the app exactly. Radius moved from a hardcoded 12px to
--radius-modal (20px).
- Stopped the double PDF load. Prior code called pdfjs.getDocument(url)
inside onLoadSuccess to scan for highlight text, redownloading the entire
file. Now we use the PDFDocumentProxy react-pdf already loaded.
- Highlight match tightened. The 0.3 word-overlap threshold caused false
positives across whole paragraphs. Replaced with a longest-consecutive-run
heuristic and a min-run floor; the amber highlight now hits the matched
sentences, not every paragraph that shares topic words.
- Overlay is programmatically focused on open (requestAnimationFrame), so
Arrow/Escape/±/= all work without a click-to-focus step. role="dialog" +
aria-modal + aria-label added.
- Replaced 📄/📑 emoji view-toggle with plain text labels ("Single" / "All
pages"). Emoji rendering is inconsistent across OS versions and gave the
UI a cheap feel compared to the rest of the surface.
- PDF text highlight now uses --color-cite + --color-cite-glow (Wave-1-era
amber rebrand was applied to DESIGN.md but the component still shipped
Tailwind warning yellow #fbbf24).
## Chat niceties that ride along
- Aborted assistant messages render with a "stopped" mono chip underneath
and 0.68 opacity on the body so a user can tell a truncated reply from a
full one.
- ChatView auto-scroll stops snapping the user back to bottom when they've
scrolled up — the isScrolledUp signal was already in state, it just wasn't
gating the scroll effect.
- .cite-marker styled as a pill-padded, hover-highlighted superscript with a
focus ring using --color-cite-glow.
## Verified
- \`npm run build\`: clean, 269 modules, 842ms
- No backend changes this wave
Wave 3 (missing actions + error visibility) is next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 18, 2026
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.
Context
Wave 2 of 5 from PLAN.md. Wave 1 was #12 (stop-the-bleeding: data integrity, security, destructive guards).
The audits found that the amber left-rule citation treatment in `tokens.css` — the app's entire visual story around grounded vs synthesized answers — had never rendered in a single chat response. `MessageBubble` used plain ReactMarkdown with no renderer; the CSS classes were pure decoration. This wave wires it end to end.
Citations actually render now
MessageBubble. Assistant output is scanned for `[Source N]` / `[N]` markers (case-insensitive, with/without `#`). Each sentence containing a citation becomes a `.cited` block with the amber left-rule; each marker becomes a clickable `CitationMarker` — mono tabular-nums chip, keyboard-focusable, hover-styled.Source panel actually toggles + has an empty state
DocumentPreview on the warm-stone system
Chat polish riding along
Verification
Not in this PR
🤖 Generated with Claude Code