feat: web search, fetch, and citation UI - #280
Open
cpsievert wants to merge 33 commits into
Open
Conversation
cpsievert
force-pushed
the
feat/web-citations-rendering
branch
from
July 27, 2026 21:08
1f8e035 to
0fd0c6e
Compare
cpsievert
marked this pull request as ready for review
July 28, 2026 21:37
cpsievert
marked this pull request as draft
July 28, 2026 21:37
cpsievert
commented
Jul 29, 2026
Introduces a reusable sidenote pill + popover component: any assistant
message can carry inline <shiny-sidenote label="..." url="..." icon="...">
tags anywhere in its markdown, with the popover body as the tag's own
markdown children (supporting both a simple inline body and, via a
blank-line-separated block body, richer content like lists and
paragraphs). Entries are grouped per end-of-block into a labeled chip
(or count-fallback pill) that expands into a hover/click/focus popover
with prev/next navigation across grouped entries. This is a
general-purpose building block, not tied to any particular content
source (e.g. chatlas web search/fetch citations, a follow-up PR, will
build on top of it).
Anonymous sidenotes are numbered sequentially across the whole message,
and that message-scoped index is shown on the pill. The popover opens
after a short grace period and is rendered through a @floating-ui/react
portal so it escapes the message list's scrolling container, flipping
and shifting into view rather than clipping.
Accessibility and safety details:
- Multi-source labeled pills include the overflow count ("+N more") in
their accessible name.
- The `icon` attribute is URL-sanitized the same way `url` is.
- rehypeGroupSidenotes runs in both the markdown and HTML content
pipelines, so sidenotes also work under content_type="html".
- A literal `"` inside label/url/body must be HTML-entity-escaped, since
these are ordinary HTML attributes parsed by rehype-raw/parse5.
The component was originally built and named <shiny-footnote>, then
renamed to <shiny-sidenote> across the JS component, markdown pipeline,
Python/R packages, and docs/tests before landing.
Includes JS/CSS unit tests, a Python Playwright app + test, R docs, and
rebuilt web assets for the Python and R packages.
…closing tags While a response streams, a sidenote pill no longer flashes mid-sentence and jitters as text streams past it. It now appears only once its surrounding block has settled — either a later block has started or the stream has ended. Mirrors the existing streaming suggestion-card pattern: a new rehypeMarkTrailingSidenotes marks groups in the still-open trailing block as data-pending (SidenoteGroup renders nothing while pending), and finalizePendingSidenotes clears the markers on the non-streaming render path in hastToReact via an immutable path-copy that never mutates the cached HAST. Also fixes a bug where a self-closing <shiny-sidenote/> was rewritten to a lone <template …/>, which — like any non-void element — ignores the slash and swallows the text after it into the popover body. Normalizes <shiny-sidenote …/> to an open/close pair before the template rewrite, skipping over quoted attribute values so a slash inside url="https://…" isn't mistaken for the self-close. Also corrects the broken-icon test to assert the icon <img> unmounts (matching the intentional switch away from display:none hiding), and documents in the Python/R sidenote help that the favicon is fetched at render time from DuckDuckGo and that an explicit `icon` bypasses that third-party request.
Previously, grouping same-label sidenotes within a block kept only the first entry and discarded the rest. Now every entry is kept and the group pages through them (prev/next), matching the existing multi-source popover behavior. The overflow "+N more" badge is now hidden when all grouped entries share a single label, since paging already conveys that there's more than one entry — the badge is only shown for mixed-label groups where the count isn't otherwise visible. Updates the Python/R chat_append docs to describe same-label paging and the now-conditional overflow badge, corrects CHANGELOG/NEWS entries that still described the old dedup behavior, adds a test covering entry-count overflow in a mixed-label group, and rebuilds the JS assets for both packages.
"Aside" better describes the convention: a small pill/popover attached to a paragraph or list item, not limited to citation-style notes.
Replace hand-rolled hover/pin/blur/keydown handling with useHover, useFocus, useClick, useDismiss, useRole, and FloatingFocusManager so open/close/pin state, outside-click dismissal, and focus trapping are handled by Floating UI instead of bespoke DOM containment checks.
cpsievert
force-pushed
the
feat/sidenote-markup
branch
from
July 30, 2026 14:24
7b961a2 to
aa2f1a1
Compare
cpsievert
force-pushed
the
feat/web-citations-rendering
branch
from
July 30, 2026 14:32
0fd0c6e to
e3c8fde
Compare
# Conflicts: # js/dist/shinychat.css # js/dist/shinychat.css.map # js/dist/shinychat.js # js/dist/shinychat.js.map # js/src/chat/ChatApp.tsx # js/src/chat/chat-entry.ts # js/src/chat/chat-tools.scss # js/tests/markdown/MarkdownContent.test.tsx # pkg-py/CHANGELOG.md # pkg-py/src/shinychat/www/GIT_VERSION # pkg-py/src/shinychat/www/shinychat.css # pkg-py/src/shinychat/www/shinychat.css.map # pkg-py/src/shinychat/www/shinychat.js # pkg-py/src/shinychat/www/shinychat.js.map # pkg-r/NEWS.md # pkg-r/inst/lib/shiny/GIT_VERSION # pkg-r/inst/lib/shiny/shinychat.css # pkg-r/inst/lib/shiny/shinychat.css.map # pkg-r/inst/lib/shiny/shinychat.js # pkg-r/inst/lib/shiny/shinychat.js.map # pkg-r/tests/testthat/test-chat.R
… Sources summary Web search/fetch citations now render as deduped <shiny-aside> pills inline, plus a single Sources summary pill per message that aggregates every citation so users get an at-a-glance list without re-reading the whole response. - Add rehypeDedupeCitations and wire it into the markdown/html processors - Normalize web citations (incl. chatlas) to shiny-aside markup on the Python side - Add a pure citation model, message-scoped collector context, and whole-message merge - Add the SourcesSummary pill + popover, sharing useDismissiblePopover with AsideGroup - Cover the new behavior with unit, integration, and Playwright/e2e tests - Rebuild and distribute JS/CSS assets to pkg-py and pkg-r
chatlas 0.20.0 (posit-dev/chatlas#318) dropped WebSource.domain and replaced ContentCitation's flat url/title with a nested, optional source. Adjust to the new shape so streaming web-search results and citations no longer raise AttributeError.
The disclosure chevron, the timeline's rail, and its dots were each positioned with independently eyeballed offsets, so the rail missed the dot centers and sat off from the chevron. Extract the chevron (already solved correctly in ThinkingDisplay) into a shared component and derive every rail/dot offset in SCSS from its fixed box, so the column is correct by construction. Also draw the rail per node instead of spanning the whole timeline, so it always terminates on a dot instead of overshooting into a stub below the last one.
cpsievert
force-pushed
the
feat/web-citations-rendering
branch
from
August 7, 2026 23:53
27d575b to
d8f4e13
Compare
# Conflicts: # js/dist/shinychat.css # js/dist/shinychat.css.map # js/dist/shinychat.js # js/dist/shinychat.js.map # js/src/chat/AsideGroup.tsx # pkg-py/src/shinychat/www/GIT_VERSION # pkg-py/src/shinychat/www/shinychat.css # pkg-py/src/shinychat/www/shinychat.css.map # pkg-py/src/shinychat/www/shinychat.js # pkg-py/src/shinychat/www/shinychat.js.map # pkg-r/inst/lib/shiny/GIT_VERSION # pkg-r/inst/lib/shiny/shinychat.css # pkg-r/inst/lib/shiny/shinychat.css.map # pkg-r/inst/lib/shiny/shinychat.js # pkg-r/inst/lib/shiny/shinychat.js.map
# Conflicts: # pkg-py/CHANGELOG.md # pkg-r/NEWS.md
# Conflicts: # js/dist/shinychat.js # js/dist/shinychat.js.map # pkg-py/src/shinychat/www/GIT_VERSION # pkg-py/src/shinychat/www/shinychat.js # pkg-py/src/shinychat/www/shinychat.js.map # pkg-r/inst/lib/shiny/GIT_VERSION # pkg-r/inst/lib/shiny/shinychat.js # pkg-r/inst/lib/shiny/shinychat.js.map
# Conflicts: # js/dist/shinychat.js.map # pkg-py/src/shinychat/www/shinychat.js.map # pkg-r/inst/lib/shiny/shinychat.js.map
…ndering # Conflicts: # js/dist/shinychat.css # js/dist/shinychat.css.map # js/dist/shinychat.js # js/dist/shinychat.js.map # js/src/chat/AsideGroup.tsx # js/src/chat/chat-tools.scss # js/src/chat/chatTagToComponentMap.ts # js/src/markdown/processors.ts # js/src/markdown/urlSanitize.ts # js/tests/chat/AsideGroup.test.tsx # pkg-py/CHANGELOG.md # pkg-py/src/shinychat/www/GIT_VERSION # pkg-py/src/shinychat/www/shinychat.css # pkg-py/src/shinychat/www/shinychat.css.map # pkg-py/src/shinychat/www/shinychat.js # pkg-py/src/shinychat/www/shinychat.js.map # pkg-r/NEWS.md # pkg-r/inst/lib/shiny/GIT_VERSION # pkg-r/inst/lib/shiny/shinychat.css # pkg-r/inst/lib/shiny/shinychat.css.map # pkg-r/inst/lib/shiny/shinychat.js # pkg-r/inst/lib/shiny/shinychat.js.map
…ndering # Conflicts: # js/dist/shinychat.css # js/dist/shinychat.css.map # js/dist/shinychat.js # js/dist/shinychat.js.map # pkg-py/src/shinychat/www/GIT_VERSION # pkg-py/src/shinychat/www/shinychat.css # pkg-py/src/shinychat/www/shinychat.css.map # pkg-py/src/shinychat/www/shinychat.js # pkg-py/src/shinychat/www/shinychat.js.map # pkg-r/inst/lib/shiny/GIT_VERSION # pkg-r/inst/lib/shiny/shinychat.css # pkg-r/inst/lib/shiny/shinychat.css.map # pkg-r/inst/lib/shiny/shinychat.js # pkg-r/inst/lib/shiny/shinychat.js.map
cpsievert
marked this pull request as ready for review
August 10, 2026 23:17
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.
Why this matters
Web-enabled models can return search activity, fetched pages, and structured citations. Without dedicated rendering, that context competes with the answer itself. This PR keeps responses readable while users can inspect evidence beside a claim or from one message-wide Sources summary.
User experience
ContentCitationrenders through the public<shiny-aside>component from feat: <shiny-aside> markup for source pills + popovers #278.grounded_span, shinychat maps it to the publicgrounded-spanattribute. Opening or paging the aside highlights the matching answer text.Component boundary
grounded-spanmatching and active-popover highlighting.ContentCitationdata into that public component.data-citationopts an aside into the message-wide Sources summary. It is not required for grounded highlighting.cited_quoteremains separate from generic aside state.R support
The R path uses
ellmer::ContentCitationand depends on the corresponding citation-content support in ellmer.Verification
cd js && npm test -- --run— 994 passed, 23 skippedcd js && npm run lintcd js && npm run builduv run pytest pkg-py/tests/pytest pkg-py/tests/test_*.py -q— 410 passeduv run pytest pkg-py/tests/test_normalize_web.py -q— 13 passeduv run pytest pkg-py/tests/playwright/chat/web_citations/test_web_citations.py -q— 2 passedcd pkg-r && Rscript -e "devtools::test()"— 841 passed, 1 browser-dependent skip