Skip to content

Migrate to shinychat's high-level chat API#267

Merged
cpsievert merged 16 commits into
mainfrom
feat/shinychat-api-migration
Jul 10, 2026
Merged

Migrate to shinychat's high-level chat API#267
cpsievert merged 16 commits into
mainfrom
feat/shinychat-api-migration

Conversation

@cpsievert

@cpsievert cpsievert commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

shinychat's high-level chat API is now available in both R and Python, and shinychat is investing further in it — file attachments, cancellation, conversation history, and editable messages, etc. are all moving to live there rather than being something each consuming package reimplements. Migrating querychat onto that API now means we drop our own hand-rolled ExtendedTask/cancel/restore/bookmark wiring and instead get these features (and future ones) for free as shinychat adds them, in both languages at once.

The most visible outcome of this migration is history/bookmarking config: instead of two overlapping knobs (enable_bookmarking and bookmark_store), there's now a single history parameter that covers both in-memory chat history and Shiny bookmarking, mirroring shinychat's own API.

What changes for users

  • enable_bookmarking is deprecated in favor of history (e.g. history = shinychat::history_options(restore_mode = "bookmark") / history=shinychat.types.HistoryOptions(restore_mode="bookmark")).
  • bookmark_store is removed from $app()/.app(); pass history instead.
  • Behavior change: QueryChat now defaults to history = TRUE/history=True (shinychat's own default, browser-localStorage-backed history) instead of no persistence.
  • Chat greetings now use shinychat's greeting API — schema-aware, generated on demand, preserved across bookmark/restore.

See the updated NEWS.md / CHANGELOG.md entries for full detail.

Test plan

  • R: make r-check
  • Python: make py-check
  • Manual: bookmark/restore a chat in both R and Python example apps

cpsievert and others added 3 commits July 1, 2026 09:21
Replaces the hand-rolled ExtendedTask/cancel/bookmark wiring in both
packages with shinychat's high-level chat_ui()/chat_server()
(R) and client= (Python) entry points, so querychat no longer
duplicates logic that shinychat now owns directly.

- R: mod_ui/mod_server now delegate to chat_mod_ui()/chat_mod_server()
  instead of manually wiring ExtendedTask, cancel, and restore; greeting
  moves to the server side.
- Python: mod_ui/mod_server pass client= to shinychat.Chat and drop the
  manual submit/cancel/bookmark wiring; generate_stream is replaced by
  generate_async, and the greeter is wired into _make_greeting.
- Adds a minimum version requirement for the migration and removes
  argument-default overrides that are no longer needed.
- Incorporates code review fixes: restored moduleServer isolation and
  original formatting in querychat_module.R, corrected greeting logic,
  and removed over-explaining comments left over from the migration.
…ry param

Both packages previously configured chat history persistence through
two separate, overlapping knobs (enable_bookmarking and bookmark_store).
This collapses them into a single history parameter (bool or
HistoryOptions) that mod_server resolves once and uses to register both
table and viz state for bookmarking/restore.

- Python: QueryChatBase stores history verbatim; mod_server resolves it
  and dual-registers table/viz state; enable_bookmarking/bookmark_store
  are deprecated in favor of history, with changelog entries and updated
  playwright fixtures/tests.
- R: mod_server takes the same history param and dual-registers
  table/viz state; enable_bookmarking/bookmark_store are deprecated the
  same way, with NEWS entries and test coverage for history resolution,
  the deprecation path, and app() inference. Dead generate_stream/
  utils-shinychat.R code is removed.
- Always calls Chat.enable_bookmarking() / registers chat_restore()
  hooks rather than conditionally, so history restore no longer depends
  on a legacy shim.
- Pins shinychat to the feat/chat-history branch, which provides the
  history-aware bookmarking hooks this work depends on.
- Follow-up fix: use Optional[...] instead of `| None` for the history
  parameter type, matching this file's existing typing convention.
Comment thread pyproject.toml Outdated
Comment thread pkg-r/DESCRIPTION Outdated
Co-authored-by: Carson Sievert <cpsievert1@gmail.com>

This comment was marked as resolved.

apply_state_snapshot() indexed tables[[name]] for every key in the
restored payload, so a bookmark or history snapshot referencing a
table no longer registered on the current server would error and
abort restore entirely. Iterate over the currently registered tables
instead, matching the Python implementation.
@cpsievert cpsievert marked this pull request as ready for review July 6, 2026 16:16
@cpsievert cpsievert requested a review from gadenbuie July 6, 2026 16:16
- test_toggle_hides_section_again clicked twice back-to-back while the
  chart widget below the button was still resizing (its responsive fill
  layout takes a couple seconds, and settles again after the query
  section expands), so the second click could land on stale
  coordinates. Wait for the button's position to stabilize before
  interacting.
- The QueryChat.R snapshot for `enable_bookmarking` deprecation still
  had the pre-migration deparse of the mocked testServer() call; R CMD
  check consistently renders it with explicit braces now.

@gadenbuie gadenbuie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two quick things jump out at me from the reading the PR description:

  1. Chat greetings now use shinychat's greeting API — schema-aware, generated on demand, preserved across bookmark/restore.

    I thought we had this already with #261, so I'm guessing this is a bit of Claude missing the nuance in what happened here in the PR. Would you mind clarifying what specific changes we made around greetings?

  2. There's a bunch of bookmarking changes in #266 that also touch enable_bookmarking and bookmark_store and are going to be equally complicated by the new history feature in shinychat. That region of the API is sufficiently complex that I think it's worth us talking through the API design in person to make sure we're coordinating between bookmarking and history in the best way possible.

    The core proposal in #266 was to deprecate enable_bookmarking in favor of bookmark_enable and bookmark_store. That's both a prefix hygiene change and a functional change: with the additional cards, bookmark_enable would accept both TRUE or specific content types that should be included in the bookmark (e.g. "cards" or "chat").

    I'm very open to other options for the API, but if there's a chance we'll take #266 (which I do think we should), I think it's worth our time to make sure we're including the cards feature in the bookmarking arguments. The core request there is for it to be possible to restore the cards independently of or together with the chat history.

@cpsievert

cpsievert commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Would you mind clarifying what specific changes we made around greetings?

These changes don't undo the work of #261 -- they just lean into the fact that shinychat can manage the event handling, restoration, etc.

I think it's worth us talking through the API design in person

I agree, but I'm hoping we can not let that block a Python release, which I would love to have for SciPy (this Monday).

If it helps, this PR does the following:

  1. Deprecates enable_bookmark since we now always register the bookmark save/restore callbacks; and history=history_options(restore_mode="bookmark") is the new way to have chat events (response, new chat, etc) trigger a bookmark.
  2. Removes bookmark_store from $app() since bookmarking chat history is (at least currently) only supported with bookmark_store="server". Also, note that bookmark-based restoration will continue to be the default experience for this method.

I anticipate if we're going to disagree on anything in the public API surface, it will be in the realm of whether bookmark_enable should exist. I would argue that it probably shouldn't because:

  1. We can now save and restore the chat (and other state) without bookmarking.
    • As a result, specifying which portions of app state to save/restore via bookmark_enable feels wrong. If we still feel it's important to save and restore only certain portions of state, that should come as a separate parameter -- something like restore = "cards"?
  2. It's really just a shorthand for history_options(restore_mode="bookmark")

@gadenbuie gadenbuie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration looks great and I agree, we don't need to block a Python release on fully figuring out the bookmarking/history API issues I called out. In fact, I'm confident that we can ship this and then I can work out how to best roll the dynamic cards feature into either history or bookmarking.

I ran the PR by Claude for a review, which flagged a few minor things below. Feel free to resolve and merge!

Comment thread pkg-r/R/QueryChat.R
Comment thread pkg-py/src/querychat/_shiny.py
Comment thread pkg-r/R/querychat_module.R
Comment thread pkg-py/src/querychat/_shiny_module.py
Comment thread pkg-r/DESCRIPTION
S7,
shiny,
shinychat (>= 0.4.0),
shinychat (> 0.4.0),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor housekeeping (not a blocker): > 0.4.0 does not guarantee the API this PR calls. A dev build (0.4.0.9000) satisfies > 0.4.0 yet lacks chat_server() / history_options(). Enough has moved upstream in dev shinychat that I think we should bump the dev-version pin here just to be explicit, and leave a NEWS/CHANGELOG note that this floor will need to move to the released shinychat version once that release lands.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked into pinning Remotes: to a commit, but tested remotes's actual resolution logic first (remotes::dev_package_deps()): it diffs the installed package's SHA against the live SHA of whatever ref Remotes: points to, not the Version: string, and defaults to auto-upgrading on non-interactive installs (interactive sessions get prompted with that diff). So a stale local shinychat gets caught and updated on a normal install_deps()/pak run regardless of whether we pin — pinning would mostly add manual upkeep (remembering to bump the SHA) without closing a real gap. Left DESCRIPTION as it was.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh the suggestion here wasn't about pinning to SHA but instead about bumping the dev version of shinychat on the shinychat side, e.g. 0.4.0.9001, as a signal that we've introduced a coordinated API change within the dev cycles of both packages.

Comment thread pkg-r/NEWS.md Outdated
Comment thread pkg-py/CHANGELOG.md Outdated
Comment thread pkg-py/tests/test_shiny_module.py
Comment thread pkg-py/tests/playwright/test_11_viz_footer.py
Comment thread pkg-r/R/querychat_module.R Outdated
cpsievert and others added 9 commits July 10, 2026 10:32
…istory

QueryChat$server(enable_bookmarking = TRUE) warned but the value was then
discarded -- resolved_history was computed from history %||% self$history
%||% TRUE, so it silently degraded to history = TRUE (localStorage) instead
of shareable bookmark state.

Map enable_bookmarking = TRUE to
history_options(restore_mode = "bookmark") when neither this call's nor the
constructor's history was set, preserving the old bookmark-based behavior
through the deprecation window.
…history

Same issue as the R side: .server(enable_bookmarking=True) and
QueryChatExpress(enable_bookmarking=True) warned but the value was then
discarded, silently resolving to history=True instead of bookmark-based
restore.

Map enable_bookmarking=True to HistoryOptions(restore_mode="bookmark")
when history wasn't otherwise set, on both QueryChat.server() and
QueryChatExpress's lazily-started server. Also aligns the two deprecation
warning messages, which previously said different things ("for the
equivalent behavior" on the R side vs. "no longer has any effect" here).
chat_server() registers chat_enable_history() internally whenever `history`
isn't FALSE, and shinychat's own docs call chat_enable_history() and
chat_restore() mutually exclusive -- both independently try to restore the
client's turns from a Shiny bookmark. Registering chat_restore() too, as
querychat did unconditionally, meant doubled per-turn storage on every save
and a visible flicker on restore whenever history's restore_mode was
"bookmark" (chat_restore's onRestore fires first from the raw bookmark
payload, then chat_enable_history's own restore clears and re-renders from
its own conversation-store copy).

Skip chat_restore() in that case; it's still registered otherwise, so the
chat client's own state continues to round-trip through Shiny bookmarks the
host app might trigger for unrelated reasons.
…rk mode

Mirrors the R fix: shinychat.Chat(history=) already enables self.history
(the Python equivalent of chat_enable_history()) whenever history isn't
False, and shinychat treats it and enable_bookmarking() as mutually
exclusive -- both independently try to restore the client's turns from a
Shiny bookmark. Registering enable_bookmarking() too, as querychat did
unconditionally, meant doubled per-turn storage on every save and a visible
flicker on restore whenever history's restore_mode was "bookmark".

Skip enable_bookmarking() in that case; it's still registered otherwise, so
the chat client's own state continues to round-trip through Shiny bookmarks
the host app might trigger for unrelated reasons.
…at usage

Every existing mod_server() test patches shinychat.Chat with a fake
constructor, so an upstream rename/removal of
Chat(client=, greeting=, history=), .history.on_save()/.history.on_restore(),
or .enable_bookmarking(client, bookmark_on=) wouldn't turn any of them red --
only the playwright e2e tests and the manual bookmark/restore test-plan step
would catch it.

Add one thin test that constructs the real shinychat.Chat and calls the
real methods with the same arguments mod_server() uses, so a signature
drift in that surface fails loudly here too.
…te jitter

_wait_for_stable_position() silently returned once max_wait_s elapsed even
if the locator's bounding box never settled, so a genuine layout regression
could masquerade as "settled" and pass. It also compared bounding boxes for
exact float equality, which is jitter-sensitive.

Raise a clear pytest.fail() with the last observed bounding box on deadline
exhaustion, and compare boxes within a small pixel tolerance instead of
exact equality. (playwright-python 1.58 doesn't expose expect.poll() for a
custom condition like this, so the hand-rolled polling loop stays, just
made stricter.)
The existing NEWS/CHANGELOG entry described the bookmark_store removal as a
behavior-preserving rename ("existing callers keep working without
changes"), which is true functionally but glosses over a real deployment
difference: the old default (bookmark_store="url") encoded the whole
bookmark state in the URL with no server storage, while the new default
requires server-side bookmark storage. Spell that out so anyone relying on
the old stateless behavior isn't surprised.
QueryChat now defaults to history=True (shinychat's browser-localStorage
history) instead of no persistence, so this example doesn't need to opt
into Shiny-level URL bookmarking on top of that. Also drops the now-unused
app_opts import.
@cpsievert

Copy link
Copy Markdown
Contributor Author

E2E test failures were introduced by a new pyarrow 25.0.0 release. Will fix that separately

@cpsievert cpsievert merged commit e2e8969 into main Jul 10, 2026
17 of 18 checks passed
@cpsievert cpsievert deleted the feat/shinychat-api-migration branch July 10, 2026 19:45
@github-actions github-actions Bot restored the feat/shinychat-api-migration branch July 10, 2026 19:46
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.

3 participants