Skip to content

feat(mod): Add set_client() and status to chat_mod_server() return value#227

Merged
gadenbuie merged 16 commits into
mainfrom
feat/mod-set-client
May 26, 2026
Merged

feat(mod): Add set_client() and status to chat_mod_server() return value#227
gadenbuie merged 16 commits into
mainfrom
feat/mod-set-client

Conversation

@gadenbuie
Copy link
Copy Markdown
Collaborator

Summary

  • Adds set_client(new_client, sync = TRUE) to the environment returned by chat_mod_server(), allowing the parent server to hot-swap the chat client (e.g. when a user switches models). When sync = 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.
  • Adds a status reactive ("idle" / "streaming") so the parent can observe the chat interaction state.
  • Changes client from a plain list element to an active binding that always reflects the current client, and changes the return type from list() to a locked environment.
  • Adds a restore_ui parameter to chat_restore() to support re-registering bookmark callbacks after a client swap without re-rendering the full conversation UI.

Verification

library(shiny)
library(shinychat)

ui <- page_fillable(
  selectInput("model", "Model", choices = c("openai", "anthropic")),
  textOutput("chat_status"),
  chat_mod_ui("chat")
)

server <- function(input, output, session) {
  chat <- chat_mod_server("chat", ellmer::chat_openai())

  output$chat_status <- renderText(paste("Status:", chat$status()))

  observeEvent(input$model, {
    new_client <- switch(input$model,
      openai = ellmer::chat_openai(),
      anthropic = ellmer::chat_anthropic()
    )
    chat$set_client(new_client)
  })
}

shinyApp(ui, server)

gadenbuie and others added 5 commits May 18, 2026 12:23
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.
@gadenbuie gadenbuie marked this pull request as ready for review May 18, 2026 17:33
@gadenbuie gadenbuie requested a review from cpsievert May 18, 2026 17:33
@cpsievert cpsievert requested a review from Copilot May 18, 2026 20:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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) and status exposed by chat_mod_server(), with mid-stream swaps deferred via a pending_swap reactive.
  • chat_mod_server() now returns a locked environment; client is exposed via makeActiveBinding() so callers always see the current client.
  • chat_restore() gains a restore_ui argument 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 from set_client() or via the deferred path in this observer), it sets client <<- new_client and changes pending_swap() to NULL. Because this observer takes a reactive dependency on pending_swap(), it will re-execute. At that point, append_stream_task$status() is still "success", so last_turn(client$last_turn()) runs again — this time against the new client. When sync = FALSE, the new client has no turns, so last_turn will be silently overwritten with NULL, losing the last assistant turn that was just published to consumers. Consider gating the last_turn update 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.

Comment thread pkg-r/R/chat_app.R
Comment thread pkg-r/R/chat_app.R
Comment thread pkg-r/R/chat_app.R
Comment thread pkg-r/R/chat_app.R Outdated
gadenbuie added 8 commits May 19, 2026 10:34
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.
@schloerke schloerke mentioned this pull request May 21, 2026
1 task
@gadenbuie gadenbuie merged commit fb6ee67 into main May 26, 2026
12 checks passed
@gadenbuie gadenbuie deleted the feat/mod-set-client branch May 26, 2026 16:31
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>
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