Added chat_server() as the new primary way to wire
up server-side chat logic. Pair it with chat_ui() by
matching id, e.g. chat_ui("chat") with
chat_server("chat", client). It does the same job as
chat_mod_server() but runs directly in the caller’s session
scope rather than creating its own module scope, and it enables many of
the features under New features and improvements below
automatically. chat_mod_server() and
chat_mod_ui() are now soft-deprecated in favor of
chat_server() and chat_ui(). (#264)
Added page_chat() for full-window chat pages with
persistent chat navigation, responsive sidebars, and drawers. Register
secondary pages with chat_nav_panel(), configure sidebars
with chat_sidebar(), and add a drawer with
chat_drawer(), controllable from the server via
chat_drawer_show(), chat_drawer_update(),
chat_drawer_hide(), and chat_drawer_toggle().
Related additions: chat_ui_history() for mounting
conversation history outside the chat, a page_chat_theme()
baseline theme, and drawer/show_history
options on chat_ui(). (#329)
bslib::nav_select()), with the active page readable
server-side as input$<id>_page.Added slash commands: a typeahead command palette that lets users
trigger named shortcuts directly from the chat input (type
/ to open it). Commands can expand into LLM prompts,
trigger server-side side effects (clear chat, open a modal, export
transcript), or be handled entirely client-side via the cancelable
shiny:chat-slash-command DOM event. Register commands with
the slash_command() method of the object returned by
chat_server(); its echo parameter controls
whether an invocation is recorded as a user message and triggers a
loading state. (#239)
Added chat_get_greeting() for reading the current
greeting (and whether the user dismissed it) from the server, along with
a new input$<id>_greeting_dismissed event. Server-set
greetings now also survive Shiny bookmarking round-trips.
(#254)
chat_server() gets multi-conversation history
automatically: a drawer for starting new chats and returning to previous
ones, with LLM-generated titles, search, rename, and delete.
Conversations are persisted per-user (or a custom scope) via a pluggable
store (the default FileConversationStore works out of the
box, including on Posit Connect). Customize with
history = history_options(...) — e.g. how the active
conversation is restored across reloads (browser storage, URL, or Shiny
bookmarking) and callbacks to keep app state synced to it — or opt out
with history = FALSE. The history object also offers
programmatic control, including a reactive conversation ID and an
explicit save() method. For apps that can’t use
chat_server(), wire it up manually with
chat_enable_history(). (#266, #307, #328)
You can now edit and resend a message after sending it. Editing
forks the conversation from that point — the original branch is kept as
a sibling, and ‹ 1 / 2 › controls let you switch between
versions at any time, including after reloading the page or returning
from the history drawer. Requires history to be enabled (the default
with chat_server()). (#269)
Cancelling an in-progress response is now automatic with
chat_server(): the stop button appears by default and
cancellation is handled for you. (#264)
Added file attachment support: users can upload images, PDFs, and
text files alongside their messages. chat_server() enables
attachments by default and automatically converts uploads into ellmer
Content objects for the model. For
non-chat_server() usage, enable with
allow_attachments = TRUE (or a MIME allow-list). The
maximum combined attachment size defaults to approximately 30 MB and can
be configured via the SHINYCHAT_MAX_ATTACHMENT_SIZE
environment variable. (#250)
input$<id>_user_input is a list of ellmer
Content objects (typed text, if present, followed by one
object per attachment) rather than a plain string — forward it to a chat
method by splicing with !!!,
e.g. chat$stream_async(!!!input$<id>_user_input). The
last_input reactive returned by chat_server()
mirrors this shape.Web search and web fetch responses from ellmer now show their
activity and citations directly in the chat. Readers can open a citation
beside its claim or use the message-wide Sources pill.
ContentCitation@grounded_span links each citation to the
answer text it supports. (#280)
<shiny-aside>
markup convention that any assistant message can use to attach source
details to specific claims — useful for custom RAG workflows. See the
Asides section in ?chat_append. (#278)Tool call displays have been reworked to be more concise and to intelligently group multiple calls together. By default, calls render as a condensed activity row; expand a group row to see each call, then drill into a call for its full request/result card. (#283)
tool_result_display(), a constructor for the
display object passed as
extra = list(display = ...) on an
ellmer::ContentToolResult (a bare named list with the same
fields still works). New label and
value_preview fields (e.g. a filename, and “1,204 rows”)
are shown in the activity row.title (from its annotations) and
its result title (from tool_result_display())
are now shown as-is, without client-side tense conjugation — the old
"Running {title}" / "{title} failed" templates
are gone. The definition title shows while the call is running; for a
single-call row, the result title (if provided) replaces it when the
result arrives, and failures are shown via a separate status cue. If a
title now reads oddly while running, write it in the present tense
(e.g. “Running R code”).tool_grouping parameter of
chat_ui(): "tool" (default) groups calls to
the same tool within a tool-calling loop, "all" groups
every call in the loop together, and "none" shows one
activity row per call (thinking or prose starts a new loop). Individual
tools can override the chat-level setting with a grouping
tool annotation,
e.g. tool(..., annotations = tool_annotations(grouping = "all")).open_style = "framed" in
tool_result_display() to draw a border around an open tool
result’s header and contents — a better fit for results with a footer or
fullscreen toggle. (#331)contents_shinychat() method is now paired with its tool
request: while the tool runs it appears in the activity row, and once
the result arrives the custom UI renders as standalone output. Custom
results are also preserved when preloading or restoring
conversations.Added submit_key parameter to
chat_ui(): "enter" (default, Enter submits) or
"enter+modifier" (Ctrl/Cmd+Enter submits, plain Enter
inserts a line break). The input remains editable while a response is
streaming — only submission is blocked, not typing. (#251)
The send button is easier to customize: a new
icon_send parameter on chat_ui() swaps the
submit icon, and a data-state attribute exposes the
button’s current state for styling. (#350)
Styling is more customizable via CSS custom properties: the set
of public --shiny-chat-* variables has grown to cover the
send button, history drawer, page layout, thinking display, and more,
and all of them can now be overridden from :root
(previously, element-level defaults always won). (#350, #355)
Single tildes no longer trigger strikethrough in markdown. Text
like (~$1.50) and ~/Documents now renders as
literal text; only ~~text~~ produces strikethrough. (#349,
#353)
chat_app() now configures its full-window page
through page_chat(). Arguments in ... are
passed to page_chat() instead of
shiny::shinyApp(): use app_options instead of
options, bookmark_store instead of
enableBookmarking, and compose page_chat()
with chat_server() when you need onStart or
uiPattern. chat_app() now owns the page
layout, so its title, icon, and
id configure the page-chat shell. Existing layouts that
embed chat should use chat_ui() and
chat_server() directly. (#329)
The messages parameter is deprecated in favor of the
conversation-history feature: chat_ui(messages = ...) now
warns, and chat_app(messages = ...) errors when history is
enabled (the default). Use greeting for a startup message,
chat_append() to replay messages, or
history = FALSE if you manage state yourself.
(#381)
CSS classes and custom properties used by the external-link
dialog, thinking display, and tool-result images/PDFs now use the
.shiny-chat-* prefix instead of .shinychat-*.
Update any custom CSS that targets these identifiers. (#285,
#286)
chat_ui() and page_chat() no longer
show an assistant icon by default. Pass
icon_assistant = TRUE to restore the built-in robot icon,
or supply your own icon as before. (#345)
chat_ui() now uses a wider default content width on
large displays while preserving its existing width on smaller windows.
(#364)
The dismissible parameter of
chat_greeting() has been renamed to persistent
with an inverted value. dismissible = FALSE (greeting stays
visible) is now persistent = TRUE. The old
dismissible argument still works but warns. (#260)
Fixed a security issue where model-authored Markdown could create
or escape into shinychat’s raw-HTML islands
(<shiny-chat-raw-html>), potentially injecting
arbitrary HTML into the page. As part of this,
output_markdown_stream()/markdown_stream() now
track which parts of a mixed value are trusted server-rendered HTML
versus untrusted text. (#287, #360)
A response that fails before it streams anything (e.g. an
exhausted quota, an over-long context, or a dropped connection) is now
reported in the chat instead of leaving a loading indicator that never
resolves and a locked composer. The chat_server() return
also gained last_error, a reactive holding the condition
from the most recent failed response, since both a finished and a failed
response report "idle" in status. (#304,
#314)
Fixed output_markdown_stream() permanently stopping
following new content after the user scrolled back to the bottom.
(#282)
Fixed expanding or collapsing a tool result yanking the chat’s scroll position away from what you were reading. (#348)
chat_app() no longer renders a close button or
registers a stopApp() observer when deployed to a server.
Both are now gated on rlang::is_interactive(), preventing
session crashes in multi-user deployments. (#265)
Fixed control-only inputs (the close button, cancel button, and greeting-requested signal) leaking into bookmarked state. (#258, #259)
Fixed set_client() failing when the conversation
contains an htmlwidget or tagList(). (#362, #369)
Fixed suggestion cards and the greeting overflowing the chat container in narrow spaces such as sidebars. (#255)
Fixed the copy button on code blocks not working in some embedded contexts. (@thisisnic, #247)
<script> tags are generally not supported inside a
React runtime). If you encounter issues, please let us
know.The chat UI now displays model reasoning/thinking content as
collapsible panels above assistant responses. Thinking content streams
in real-time with animated topic labels. This works with providers that
support structured thinking (e.g., Claude’s extended thinking via
ellmer) and with local models that wrap reasoning in
<thinking> tags. (#208)
Added enable_cancel parameter to
chat_ui() to show a stop button that lets users cancel an
in-progress AI response. Press the stop button or hit Escape to cancel.
chat_mod_ui() enables cancellation by default, and
chat_mod_server() handles the cancellation wiring
automatically, using the stream cancellation features introduced in
ellmer v0.4.1. (#221)
Markdown lists where every item is a
<span class="suggestion"> are now rendered as a grid
of clickable suggestion cards. Each suggestion’s text content becomes
both the card label and the value sent on click. To add a short heading
above the body text, set the title attribute on the span —
e.g. <span class="suggestion" title="Heading">Body text shown on the card.</span>.
Only the body text (not the title) is submitted when the card is
clicked. Cards stream in with staggered animations and support keyboard
navigation (arrow keys, Home/End) with roving tabindex. (#219)
Added chat_greeting() for creating welcome messages
that appear when the chat is empty. Greetings can be set statically via
chat_ui(greeting=) or dynamically from the server with
chat_set_greeting(). They are automatically dismissed when
the user sends their first message. A new
greeting_requested input fires when the chat is visible,
empty, and has no greeting, enabling LLM-generated welcome messages.
chat_mod_server(greeting=) accepts a function for
auto-generated greetings. (#217)
Tool result cards now render images and PDFs returned by ellmer
tools. When a tool returns content_image_file(),
content_image_url(), or content_pdf_file(),
the result is displayed as an inline image or a PDF filename badge.
Mixed content lists (e.g.,
list(ContentText("summary"), content_image_file("plot.png")))
are rendered with items interleaved in order. (#225)
Added footer parameter to chat_ui() for
displaying arbitrary HTML content below the chat input. Useful for
disclaimers, attribution, or interactive toolbars. Styled with sensible
defaults and customizable via --shiny-chat-footer-font-size
and --shiny-chat-footer-color CSS custom properties.
(#224)
Tool result cards now support a fullscreen toggle. Set
full_screen = TRUE in the display list (or set
res$full_screen <- NA in a custom
contents_shinychat() method) to add a button that expands
the card to fill the viewport. Press Escape, click the
backdrop, or use the close button to exit fullscreen.
Added footer field to ToolResultDisplay
for displaying custom HTML content below the tool result card body.
(#178)
chat_mod_server() now returns a
set_client(new_client, sync = TRUE) function for swapping
the chat client used by the module at runtime. When
sync = TRUE (the default), the new client inherits the
current conversation’s turns, system prompt, and tools so the
conversation continues seamlessly. If a response is currently streaming,
the swap is deferred until the stream completes. (#227)
chat_mod_server() now returns a status
reactive that reports the current interaction state: "idle"
when no response is in progress, or "streaming" while a
response is actively being received. (#227)
chat_restore() now invisibly returns a cancel
function that tears down all bookmark registrations made by that call.
This is useful when swapping the chat client via
set_client(), which handles the re-registration
automatically. (#227)
Fixed the external link confirmation dialog not rendering in
Safari. The backdrop overlay appeared but the dialog content was
invisible due to a Bootstrap/<dialog> CSS
interaction. (#201, #238)
Fixed pressing Escape to dismiss the external link dialog leaving it in a broken state where subsequent link clicks no longer worked. (#238)
Fixed an issue where user chat messages would display the default assistant icon. (#162)
chat_mod_server() now returns a list of reactives for
last_input and last_turn, as well functions to
update_user_input(), append() and
clear() the chat. (#130, #143, #145)Added chat_restore() which adds Shiny bookmarking
hooks to save and restore the {ellmer} chat client. (#28,
#82)
Added update_chat_user_input() for programmatically
updating the user input of a chat UI element. (#78)
shinychat now shows tool call request and results in the UI, and
the feature is enabled by default in chat_app() and the
chat module (chat_mod_server()). When using
chat_append() with chat_ui(), set
stream = "content" when you call the
$stream_async() method on the ellmer::Chat
client to ensure tool calls are included in the chat stream output.
Learn more in the tool
calling UI article. (#52)
Added chat_append(icon=...) and
chat_ui(icon_assistant=...) for customizing the icon that
appears next to assistant responses. (#88)
chat_app() now correctly restores the chat client
state when refreshing the app, e.g. by reloading the page.
(#71)
External links in chat messages in chat_ui() now
open in a new tab by default, with a confirmation dialog.
(#120)
We consolidated the <shiny-chat-message> and
<shiny-user-message> components into a single
<shiny-chat-message> component with a
data-role attribute to indicate whether it’s an “assistant”
or “user” message. This likely has minimal impact on your apps, other
than custom styles. You should update any
shiny-user-message rules to use
shiny-chat-message[data-role="user"]. (#101)
The chat UI’s send input button is now identified by the class
.shiny-chat-btn-send. (@DeepanshKhurana, #138)
Added new output_markdown_stream() and
markdown_stream() functions to allow for streaming markdown
content to the client. This is useful for showing Generative AI
responses in real-time in a Shiny app, outside of a chat interface.
(#23)
Both chat_ui() and
output_markdown_stream() now support arbitrary Shiny UI
elements inside of messages. This allows for gathering input from the
user (e.g., selectInput()), displaying of rich output
(e.g., {htmlwidgets} like {plotly}), and more.
(#29)
Added a new chat_clear() function to clear the chat
of all messages. (#25)
Added chat_app(), chat_mod_ui() and
chat_mod_server(). chat_app() takes an
{ellmer} chat client and launches a simple Shiny app
interface with the chat. chat_mod_ui() and
chat_mod_server() replicate the interface as a Shiny
module, for easily adding a simple chat interface connected to a
specific {ellmer} chat client. (#36)
The promise returned by chat_append() now resolves
to the content streamed into the chat. (#49)
chat_append(), chat_append_message()
and chat_clear() now all work in Shiny modules without
needing to namespace the id of the Chat component.
(#37)
chat_append() now logs and throws a silent error if
the stream errors for any reason. This prevents the app from crashing if
the stream is interrupted. You can still use
promises::catch() to handle the error in your app code if
desired. (#46)