feat(mod): Add set_client() and status to chat_mod_server() return value#227
Merged
Conversation
Allow replacing the chat client after module initialization. The new `set_client(new_client, sync = TRUE)` function swaps the internal client reference used by all module closures. When `sync` is TRUE, turns, system prompt, and tools are copied from the old client to the new one. If called while a stream is in progress, the swap is deferred until the stream completes. Bookmarking is re-registered with the new client. The return value is now a locked environment with an active binding for `client`, so `chat$client` always reflects the current client.
Add `restore_ui` param to `chat_restore()` to control whether existing client turns are rendered into the chat UI on registration. `do_swap()` passes `restore_ui = FALSE` to avoid duplicating the already-displayed conversation when re-registering bookmark callbacks. Rename the `"update_last_turn"` observer to `"on_stream_complete"` to reflect that it now also handles deferred client swaps.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds the ability to hot-swap the ellmer chat client in chat_mod_server() and exposes a status reactive describing whether a stream is in progress. The module's return value also changes from a list to a locked environment with client as an active binding, and chat_restore() gains a restore_ui flag so re-registration after a swap can skip rendering the existing turns.
Changes:
- New
set_client(new_client, sync = TRUE)andstatusexposed bychat_mod_server(), with mid-stream swaps deferred via apending_swapreactive. chat_mod_server()now returns a locked environment;clientis exposed viamakeActiveBinding()so callers always see the current client.chat_restore()gains arestore_uiargument used by the swap path to re-register bookmark callbacks without re-rendering UI.
Reviewed changes
Copilot reviewed 2 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| pkg-r/R/chat_app.R | Implements set_client, swap_client, status reactive, deferred-swap observer, and switches return type to a locked environment with client as active binding. |
| pkg-r/R/chat_restore.R | Adds restore_ui parameter to allow skipping initial UI rendering when re-registering after a client swap. |
| pkg-r/man/chat_app.Rd | Regenerated docs reflecting the new environment return value, status, and set_client(). |
| pkg-r/man/chat_restore.Rd | Regenerated docs for the new restore_ui parameter. |
Files not reviewed (2)
- pkg-r/man/chat_app.Rd: Language not supported
- pkg-r/man/chat_restore.Rd: Language not supported
Comments suppressed due to low confidence (1)
pkg-r/R/chat_app.R:267
- After
swap_client()runs (either inline fromset_client()or via the deferred path in this observer), it setsclient <<- new_clientand changespending_swap()toNULL. Because this observer takes a reactive dependency onpending_swap(), it will re-execute. At that point,append_stream_task$status()is still"success", solast_turn(client$last_turn())runs again — this time against the new client. Whensync = FALSE, the new client has no turns, solast_turnwill be silently overwritten withNULL, losing the last assistant turn that was just published to consumers. Consider gating thelast_turnupdate so it only fires when the task status actually transitions to success (e.g. track the previously-seen status), or only run the swap branch when status is not "success" / when there is no fresh result to publish.
shiny::observe(label = "on_stream_complete", {
status <- append_stream_task$status()
swap <- pending_swap()
if (status == "success") {
last_turn(client$last_turn())
}
if (!is.null(swap) && status != "running") {
pending_swap(NULL)
swap_client(swap$client, swap$sync)
}
})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Remove the callback cancellation tracking and re-registration pattern from chat_restore(). This machinery existed to support calling chat_restore() multiple times for the same id (e.g. after a client swap), tearing down previous onBookmark/onRestore/observer registrations before installing new ones. Also removes the restore_ui parameter, which only existed to skip initial UI rendering during re-registration after a client swap. In chat_mod_server(), swap_client() no longer re-calls chat_restore().
chat_restore() now accepts a function (called without arguments) that returns the ellmer Chat client, in addition to a Chat object directly. When a function is supplied, the bookmark callbacks (onBookmark, onRestore) call it at fire time to resolve the current client. This means swapping the client no longer requires re-registering bookmarks — the closures naturally see the updated client. In chat_mod_server(), pass `function() client` to chat_restore() so swap_client() works by simply reassigning the local variable.
Clarifies that when set_client() is called multiple times while a response is streaming, only the most recent new client is used.
enable_bookmarking() now accepts a callable (called without arguments) that returns the client instance, in addition to a client object directly. When a callable is supplied, get_state/set_state resolution is deferred to callback time so the bookmark hooks always see the current client. This means swapping the client no longer requires re-registering bookmarks. Both ClientWithState and chatlas.Chat paths support the lazy pattern.
… argument" This reverts commit fb061338c123835d6599119e8bf6b06d7adb6011.
This reverts commit 1af80980dd2eb89a7b778a1d5690713f3c6d7b16.
…structure" This reverts commit aa63a838c34b01205de32dc720a7993f5f39b981.
chat_restore() now invisibly returns a function that cancels all bookmark registrations it created. This replaces the internal session-keyed bookkeeping that previously managed teardown on re-registration. chat_mod_server() uses the returned cancel callback in swap_client() for clean teardown-and-re-register when the client changes.
cpsievert
approved these changes
May 20, 2026
cpsievert
pushed a commit
that referenced
this pull request
May 26, 2026
…urn value (#227) * feat: add `set_client()` to `chat_mod_server()` return value Allow replacing the chat client after module initialization. The new `set_client(new_client, sync = TRUE)` function swaps the internal client reference used by all module closures. When `sync` is TRUE, turns, system prompt, and tools are copied from the old client to the new one. If called while a stream is in progress, the swap is deferred until the stream completes. Bookmarking is re-registered with the new client. The return value is now a locked environment with an active binding for `client`, so `chat$client` always reflects the current client. * fix: prevent UI duplication on client swap and rename observer Add `restore_ui` param to `chat_restore()` to control whether existing client turns are rendered into the chat UI on registration. `do_swap()` passes `restore_ui = FALSE` to avoid duplicating the already-displayed conversation when re-registering bookmark callbacks. Rename the `"update_last_turn"` observer to `"on_stream_complete"` to reflect that it now also handles deferred client swaps. * chore: rename `swap_client()` * feat(mod): Return `status` reactive var * `air format` (GitHub Actions) * refactor(chat_restore): remove teardown/re-registration infrastructure Remove the callback cancellation tracking and re-registration pattern from chat_restore(). This machinery existed to support calling chat_restore() multiple times for the same id (e.g. after a client swap), tearing down previous onBookmark/onRestore/observer registrations before installing new ones. Also removes the restore_ui parameter, which only existed to skip initial UI rendering during re-registration after a client swap. In chat_mod_server(), swap_client() no longer re-calls chat_restore(). * feat(chat_restore): accept a function for the client argument chat_restore() now accepts a function (called without arguments) that returns the ellmer Chat client, in addition to a Chat object directly. When a function is supplied, the bookmark callbacks (onBookmark, onRestore) call it at fire time to resolve the current client. This means swapping the client no longer requires re-registering bookmarks — the closures naturally see the updated client. In chat_mod_server(), pass `function() client` to chat_restore() so swap_client() works by simply reassigning the local variable. * docs(set_client): note that only the most recent swap is used Clarifies that when set_client() is called multiple times while a response is streaming, only the most recent new client is used. * feat(py/enable_bookmarking): accept a function for the client argument enable_bookmarking() now accepts a callable (called without arguments) that returns the client instance, in addition to a client object directly. When a callable is supplied, get_state/set_state resolution is deferred to callback time so the bookmark hooks always see the current client. This means swapping the client no longer requires re-registering bookmarks. Both ClientWithState and chatlas.Chat paths support the lazy pattern. * Revert "feat(py/enable_bookmarking): accept a function for the client argument" This reverts commit fb061338c123835d6599119e8bf6b06d7adb6011. * Revert "feat(chat_restore): accept a function for the client argument" This reverts commit 1af80980dd2eb89a7b778a1d5690713f3c6d7b16. * Revert "refactor(chat_restore): remove teardown/re-registration infrastructure" This reverts commit aa63a838c34b01205de32dc720a7993f5f39b981. * refactor(chat_restore): return bundled cancel callback chat_restore() now invisibly returns a function that cancels all bookmark registrations it created. This replaces the internal session-keyed bookkeeping that previously managed teardown on re-registration. chat_mod_server() uses the returned cancel callback in swap_client() for clean teardown-and-re-register when the client changes. * docs(r): add NEWS entries for set_client() and status reactive (#227) --------- Co-authored-by: gadenbuie <gadenbuie@users.noreply.github.com>
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.
Summary
set_client(new_client, sync = TRUE)to the environment returned bychat_mod_server(), allowing the parent server to hot-swap the chat client (e.g. when a user switches models). Whensync = TRUE, conversation turns, system prompt, and tools are copied from the old client to the new one. Mid-stream swaps are safely deferred until the stream completes.statusreactive ("idle"/"streaming") so the parent can observe the chat interaction state.clientfrom a plain list element to an active binding that always reflects the current client, and changes the return type fromlist()to a lockedenvironment.restore_uiparameter tochat_restore()to support re-registering bookmark callbacks after a client swap without re-rendering the full conversation UI.Verification