Skip to content

Support shinychat v0.5.0 - #215

Merged
gadenbuie merged 33 commits into
mainfrom
fix/shinychat-v0.5.0
Sep 9, 2026
Merged

gadenbuie merged 33 commits into
mainfrom
fix/shinychat-v0.5.0

Conversation

@gadenbuie

Copy link
Copy Markdown
Collaborator

Prepares {btw} for the upcoming shinychat v0.5.0 release.

Summary

  • btw_app() uses the new page_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.
  • Chat history persists in a duckdb database in btw's user cache when duckdb is installed, keyed by project directory, with shinychat's default storage as the fallback.
  • Tool result values are collapsed to a single string, data frame tool results return JSON, and run-R tool results are serializable so all tool results survive shinychat history serialization.
  • Subagent reports and tool display follow the shinychat 0.5.0 display protocol via contents_html(), with styled disclosure of the full subagent conversation.
  • New /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.
  • Skills are registered as slash commands under their own names, submitting the skill's SKILL.md instructions plus the user's message to the model.
  • /new and /clear start 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.
  • New exported helpers btw_skill_prompt() (renders a skill's <skill> block: name, description, location, optional compatibility and allowed tools) and btw_skills_register_slash_commands() (registers skill slash commands on any chat_server() handle, gated on shinychat 0.4.0.9000) make both features reusable outside btw_client() and btw_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.
  • With shinychat 0.4.0.9000 installed, launch 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-creator submits the skill text with your message. /new or /clear saves the current conversation to the history drawer and zeroes the token and cost counters; loading the conversation back from the drawer restores them.

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.
@gadenbuie
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
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.
@gadenbuie
gadenbuie added this pull request to stack #217 September 9, 2026 14:49
@gadenbuie
gadenbuie merged commit 2124120 into main Sep 9, 2026
9 checks passed
@gadenbuie
gadenbuie deleted the fix/shinychat-v0.5.0 branch September 9, 2026 15:10
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.

1 participant