Repository navigation
Support shinychat v0.5.0 - #215
Merged
Merged
Conversation
ellmer (>= 0.5.0) no longer auto-serializes arbitrary R objects in tool results. Convert data frames passed as tool result values to JSON via as_json_rowwise(). Data frames are also kept in the tool result extra field for programmatic access.
OpenAI's Responses API rejects tool results whose output is an array of bare strings: each element must be a typed object. Character vectors of length > 1 are serialized by ellmer as an array of strings, which produces HTTP 400 'expected an object, but got a string'. Normalize tool result values once in as_tool_result_value(), used by btw_tool_result() and the sites that build result objects directly.
expect_btw_tool_result() now checks that character values have
length 1, catching multi-element vectors that OpenAI's Responses API
rejects ('expected an object, but got a string').
shinychat 0.5.0 validates `display` fields and serializes tool cards as wire blocks, breaking btw's card post-processing. Express card customization through supported `display$html`/`footer` fields instead, which both shinychat 0.4.0 and 0.5.0 render on btw's behalf: - run-r: move `copy_code` out of `display` (now `extra$copy_code`), build card body, footer, and title pre-`super()`; keep only the `status` post-mutation (a plain field on both protocols) - file diffs: pass the diffviewer widget via `display$html` - subagent report: render as trusted `display$html` via commonmark (GFM extensions for pipe tables) instead of `display$markdown`, which 0.5.0's client sanitizer would render as plain text; render nested tool cards with `format()` (`as.tags()` is gone in 0.5.0) - `md_code_block()` now returns a single string (0.5.0 rejects multi-element `display$markdown`) Add commonmark to Imports; no shinychat version floor bump needed.
The subagent report embedded shinychat tool cards via format(), but
those are inert <shiny-tool-*> custom elements outside shinychat's
React runtime, so the Full Conversation disclosure rendered empty.
Render the subagent client's turns with ellmer's contents_html()
instead, supplying HTML for tool requests and results (ellmer returns
NULL for both): run results reuse the run-output renderer, other
results prefer display$markdown, and values fall back to an escaped
pre block. Collapse format(show = "call") output, which line-wraps
long calls into multiple elements (fixes an nzchar() length error).
Tool calls are paired with their results in a collapsible
<details> block (summary: Tool Call: {fn}), empty ContentThinking
objects are dropped, and the disclosure is omitted entirely when
there's nothing to show. A blank line separates the final response so
it renders as markdown instead of joining the raw HTML block.
Put the tool name in a <code> element in the tool-call summary, give the full-conversation details a btw-subagent-conversation class, and add simple Bootstrap-variable styling (border, radius, cursor, subtle background on tool-call blocks) in btw_app.css.
shinychat (>= 0.4.0) saves turns via ellmer::contents_record() followed by jsonlite::serializeJSON(), and a tool result's extra field is passed through as-is. Storing raw S7 objects in extra$contents broke history and bookmark saving with jsonlite 2.0.0, which cannot pack S7 objects. - Record extra$contents with ellmer::contents_record() and replay them when rendering the result card, so live and restored turns render identically. - Drop extra$data when the evaluated code's last value is not serializeJSON()-safe.
Extend expect_btw_tool_result() to record each tool result into a Turn via ellmer::contents_record() and require jsonlite::serializeJSON() to succeed, since shinychat passes tool result extras through as-is when saving chat history and bookmarks.
Split btw_app() into a shared core and two version-specific UI shells that can be deleted independently when btw adopts the shinychat 0.5.0 floor: - btw_client_app_core.R: version-agnostic server factory (chat handle interface), status bar, tool selection, greeting, shortcuts, bookmarking - btw_client_app_v05.R: page_chat() shell with tool selection and the system prompt editor in bslib offcanvases triggered from the page toolbar, status bar in toolbar_input, and chat_server() wiring - btw_client_app_legacy.R: previous page_sidebar() shell using chat_mod_ui()/chat_mod_server(), deleted when shinychat >= 0.5.0 is required The v0.5 shell replays `messages` via chat_append() (messages is deprecated in chat_ui()), gates the close button on interactive sessions, and drops the toolbar divider and clear-chat button. The tools offcanvas uses a flex column layout so the checkbox list scrolls inside a full-height panel. Toolbar icons switch to Material Symbols wght200 variants to match bslib's icon weight. Requires bslib >= 0.12.0 at runtime when page_chat() is available.
Wire shinychat >= 0.5.0's conversation history into btw_app() using a
custom ConversationStore backed by duckdb. Conversations are stored in
a single database at path_btw_cache("chat_history.duckdb"), keyed by
project directory (the closest DESCRIPTION/.git root, or the working
directory otherwise).
The store connects per operation with retry and backoff under a 10s
deadline (duckdb allows one writer process at a time), closes the
connection via withr::defer() even on failure, and degrades gracefully
with a warning if the database can't be reached. Without duckdb,
shinychat's default history is used and a one-time notice suggests
installing duckdb.
Mirror the @<command> context commands as /btw-* slash commands that stage their output as a text attachment on the chat input, and register discovered skills as slash commands that combine the skill's SKILL.md text with the user's message. Failed commands restore the original slash text to the chat input and show an error toast; /btw-* commands show a persistent running toast while they work.
Add btw_skill_prompt(), which renders one skill's <skill> block (name, description, location, and optional compatibility and allowed tools), and btw_skills_register_slash_commands(), which registers a slash command for every discovered skill on a shinychat chat_server() handle. Both share internals with existing code: the skill tool and the skill slash command handler resolve skills through one btw_skill_resolve(), and the btw_client() system prompt renders its <skill> blocks through the same format_skill_prompt(). btw_app() now uses the exported registration function instead of its app-local copy.
…counters /new and /clear start a new chat: with the chat history enabled, the current conversation is saved and the HistoryController starts a fresh one; otherwise the conversation is cleared without saving. Both refuse while a response is streaming. The status bar counters now follow the active conversation: loading an old chat from the history restores its token and cost totals from the client's turns, and starting a new chat zeroes them. The status bar module returns its token and cost reactives for use in tests. btw_skills_register_slash_commands() gained a reserved argument so callers can pass the names of slash commands they registered themselves; btw_app() passes /new and /clear.
The duckdb history store, the page_chat shell, the slash command tests, and the chat_attachment staging all need shinychat >= 0.4.0.9000, which CI does not have yet. Skip those tests when the dev shinychat is missing, run only the legacy shell assertions in the page_chat shell test, and mark the chat_server() example \dontrun so --run-donttest can't execute it. Reference shinychat::chat_server() as plain code in the docs so Rd cross-references check against 0.4.0 without warnings.
Replaces the \dontrun block; the example runs once shinychat 0.5.0 is installed, matching the function's own version requirement.
Bump the shinychat version requirement to 0.4.0.9000 and add a Remotes field so CI installs the development version with the 0.5.0 features (page_chat, chat_server, chat_attachment, history_options). This runs the version-gated tests on CI and clears the missing-objects warning from checking dependencies in R code. Remove the Remotes field when shinychat 0.5.0 is on CRAN.
R CMD check flags shinychat:: references to chat_server(), page_chat(), chat_attachment(), history_options(), and the ConversationStore class because the installed CRAN shinychat 0.4.0 does not export them. Route all five through R/utils-shinychat.R proxies that check the installed shinychat version, fail with a clear message on 0.4.0, and resolve the object at runtime with asNamespace(), which static analysis cannot follow. The 0.4.0-safe functions (chat_mod_ui(), chat_mod_server(), chat_ui(), chat_greeting(), contents_shinychat()) keep their direct shinychat:: calls.
This reverts commit e35219f.
gadenbuie
marked this pull request as ready for review
September 8, 2026 00:13
shinychat renders display$html without sanitizing and commonmark lets raw HTML pass through, so model-, user-, and tool-sourced markdown embedded in the subagent report could inject markup into the app. Escape raw HTML outside inline code spans and fenced code blocks, where commonmark escapes code content itself, and escape deparsed tool calls whose string literals could carry markup. Also render tool results without a paired request as "unknown tool" instead of erroring on the NULL request.
shinychat invokes slash command handlers with no streaming guard and the chat input stays live while a response streams, so a skill command could start a second concurrent stream on the same client. Skill commands now refuse to run while streaming and fail like other commands, restoring the input and showing an error toast. Also point the btw_skills_register_slash_commands() seealso at btw_tool_skill() for a skill's instructions, since btw_skill_prompt() returns only the skill's metadata block.
Error messages can contain markup from user or tool input; escape them before interpolating into the toast/notification HTML. The action stays raw because callers pass intentional HTML there. Also fix the inverted no-bslib fallback: error notifications now show the action plus the error message instead of dropping it, and success notifications no longer announce "Failed to update system prompt".
The MutationObserver fired on every DOM mutation batch for the app's lifetime, each time running a whole-document querySelectorAll whose result was discarded once the counters were initialized. Disconnect the observer as soon as the status counters exist.
run_r_extra_data() serialized the entire last value to JSON on every run tool call just to test serializability. Environments, functions, and formulas are now rejected up front, and values over 1 MB are dropped without paying for the test: nothing reads the run tool's extra$data, and large values would bloat every chat history save that includes the result.
shinychat#399 makes chat$clear() on the chat_server() handle coordinate with the history controller: the current conversation is saved, turns and UI are cleared, and the active conversation resets. The /new and /clear slash commands now call chat$clear() directly instead of duplicating the controller coordination, and the now-unused btw_app_history_controller() helper is removed. The status bar Clear chat button passes the default client_history implicitly.
shinychat#399 makes chat$clear() error when conversation history is enabled and moves the save-and-reset behavior to the new chat$new_chat() method. The status bar Clear chat button and the /new and /clear slash commands now call chat$new_chat() when the handle has it, falling back to chat$clear(client_history = "clear") for handles without history support, such as the shinychat 0.4.x app shell.
Move shinychat from Suggests to Imports and install it from the posit-dev/shinychat@rc-v0.5.0 remote, which includes the history-aware chat$new_chat() API from shinychat#399.
shinychat's rc-v0.5.0 chat_server() enables conversation history by default, which requires a Shiny session, so the example for btw_skills_register_slash_commands() failed R CMD check examples. The example now runs only when a Shiny app is running. Also qualify utils::object.size to clear the "no visible global function definition" NOTE.
A runnable example can never be evaluated for btw_skills_register_slash_commands(): shinychat's chat_server() requires a running Shiny app, so the example would always be skipped and hidden in any viewer. Move the snippet into the description as a static code block instead.
gadenbuie
requested
a lite review from Copilot
and removed request for
Copilot
September 9, 2026 11:35
Group the PR's user-facing changes under a single 'shinychat >= 0.5.0 compatibility' entry with subpoints per behavior, and lead with the user-facing benefit over internal implementation details. Refs #215
shinychat 0.5.0 escapes HTML in result titles shown in grouped tool
call lists, so raw HTML() titles rendered as literal tags. Rework all
tool results to use the fields the shinychat tool UI vignette describes:
- Result titles are plain, generic past-tense phrases ("Read file",
"Searched code"); definition titles in annotations are present tense
("Reading file") since the "Running {title}" template is gone.
- Per-call identifiers (filenames, refs, branches, packages, URLs)
moved to the display label field so grouped calls are distinguishable.
- Static icons moved from result displays to tool annotations; each
file-modifying tool gets a distinct icon (new edit-document and
find-replace Material Symbols SVGs; existing post-add and difference).
Also drops the now-unused file_result_title() helper and its test.
btw_app() now emits a warning suggesting shinychat 0.5.0 when it falls back to the legacy sidebar layout. In-development 0.4.0.9000 builds are treated as 0.5.0 and don't trigger the warning.
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.
Prepares {btw} for the upcoming shinychat v0.5.0 release.
Summary
btw_app()uses the newpage_chat()layout when shinychat >= 0.5.0 is installed, with the tool selection menu and system prompt editor in bslib offcanvases, and keeps the legacy sidebar shell for shinychat 0.4.x.contents_html(), with styled disclosure of the full subagent conversation./btw-*slash commands mirror the@command syntax, staging context as a text attachment on the chat input (nothing submitted or echoed) with a running toast while they work; failed commands restore the input text and show an error toast.SKILL.mdinstructions plus the user's message to the model./newand/clearstart a new chat: the current conversation is saved to the chat history (when enabled) and a fresh one begins; the token and cost counters follow the active conversation, restoring their totals when an old chat is loaded from the history.btw_skill_prompt()(renders a skill's<skill>block: name, description, location, optional compatibility and allowed tools) andbtw_skills_register_slash_commands()(registers skill slash commands on anychat_server()handle, gated on shinychat 0.4.0.9000) make both features reusable outsidebtw_client()andbtw_app().Verification
devtools::test()passes locally: 2508 tests, including new suites for the slash commands, the skill helpers, the new-chat counter behavior, and the duckdb history store.btw_app()and try/btw-news dplyr: the running toast appears, the context lands as an attachment chip on the chat input, and submitting a message includes it as a<file-attachment>block. A skill command such as/skill-creatorsubmits the skill text with your message./newor/clearsaves the current conversation to the history drawer and zeroes the token and cost counters; loading the conversation back from the drawer restores them.