Skip to content

fix(pkg-r): make viz bookmark restore survive serialization#255

Open
gadenbuie wants to merge 2 commits into
mainfrom
fix/viz-card-bookmarking
Open

fix(pkg-r): make viz bookmark restore survive serialization#255
gadenbuie wants to merge 2 commits into
mainfrom
fix/viz-card-bookmarking

Conversation

@gadenbuie

@gadenbuie gadenbuie commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Relates to posit-dev/shinychat#261

Problem

With bookmarking enabled, restoring a session whose chat history contains a querychat_visualize tool call breaks on reload. Two distinct failures stack up:

  1. could not find function "tag_require_client_side" — the visualize tool embeds live bslib/htmltools tags (notably a bslib::input_code_editor() in the footer) in the ContentToolResult extra$display. shinychat bookmarks chat turns with jsonlite::serializeJSON() and restores them with unserializeJSON(), which rebuilds embedded closures from deparsed text and drops their environment. The render hooks on those tags can then no longer reach bslib internals, so re-rendering the restored tool card throws. (Root-caused in detail in Bookmark restore drops render-hook environments: tool-result displays with live bslib/htmltools tags fail to re-render shinychat#261.)

  2. $ operator is invalid for atomic vectors — a URL bookmark round-trips querychat_viz_widgets through jsonlite, which simplifies the JSON array of objects to a data.frame. restore_viz_widgets() then iterated its columns (atomic vectors), so the transcript chart never re-rendered.

Changes

  • freeze_tags() (new, utils-html.R) resolves a tag's render hooks to inert HTML + html_dependency objects up front — while the hooks can still reach those internals — so the display survives the bookmark serialize/deserialize round-trip. Applied to the visualize tool's display$html and display$footer. The frozen footer carries the prism-code-editor / bslib-code-editor dependencies through the round-trip, so the editor still hydrates on restore (no explicit dependency injection needed).
  • restore_record_list() (new, querychat_module.R) rebuilds the list-of-lists shape from the URL-decoded data.frame row by row (dropping absent NA fields); the querychat_viz_widgets restore is wrapped with it.

Verification

Reproduced and confirmed fixed with this app:

library(shiny)
qc <- querychat::QueryChat$new(
  mtcars,
  tools = "visualize",
  greeting = "Ask me about the `mtcars` dataset.",
  client = ellmer::chat_openai(model = "gpt-5.4-nano")
)
shiny::enableBookmarking("url")
qc$app_obj()

Submit a prompt that produces a chart (e.g. "plot how many cars have 6 or 8 cylinders"), wait for it to render, then reload the page to restore the bookmark. Before: both errors above (the chat transcript fails to re-render). After: no errors, and the transcript chart and code editor both re-render on restore.

Notes

The querychat_visualize tool embeds live bslib/htmltools tags (notably a
bslib::input_code_editor() in the footer) in the ContentToolResult
extra$display. shinychat bookmarks chat turns via jsonlite::serializeJSON()
and restores them with unserializeJSON(), which rebuilds embedded closures
from deparsed text and drops their environment. The render hooks on those
tags then cannot reach bslib internals, so restoring a bookmark that contains
a visualization fails with "could not find function tag_require_client_side".

Add freeze_tags() to resolve render hooks to inert HTML plus html_dependency
objects up front -- while the hooks can still reach those internals -- and
apply it to the visualize display html and footer. The frozen output carries
the code-editor dependencies through the round-trip, so the component still
hydrates on restore.

See posit-dev/shinychat#261.
…rame

A URL bookmark round-trips querychat_viz_widgets through jsonlite, which
simplifies the JSON array of objects to a data.frame. restore_viz_widgets()
then iterated its columns (atomic vectors) and failed with "$ operator is
invalid for atomic vectors", so the transcript chart never re-rendered.

Add restore_record_list() to rebuild the list-of-lists shape row by row and
wrap the querychat_viz_widgets restore with it.
@gadenbuie gadenbuie requested a review from cpsievert June 22, 2026 20:02
@gadenbuie gadenbuie marked this pull request as ready for review June 22, 2026 20:03
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.

2 participants