feat: support images and PDFs in tool results#225
Merged
Conversation
Add content_extra valueType to ToolResult component that parses a JSON array of content items and renders <img> elements for images and a PdfBadge component for PDFs.
Add content_extra valueType for ContentImage and ContentPDF tool results. Refactors tool_string() into tool_default_display() which detects image/PDF content objects and serializes them as structured JSON for the React frontend.
Handle both streaming and turn-replay paths for content images/PDFs. Add content_extra valueType and helpers for structured JSON rendering. Register standalone ContentImageInline/ContentImageRemote/ContentPDF handlers for the turn-replay path. Suppress tool-content XML wrappers.
Extend content_extra to handle interleaved lists where images/PDFs are mixed with other value types. Non-content-extra items serialize as text items with value and value_type fields, rendered by React using the appropriate renderer (code, markdown, html, or plain text).
Use Content base class check (not just images/PDFs) to detect mixed content lists. ContentText items extract the text slot directly; other Content types fall back to string coercion.
Markdown degrades gracefully for plain text and gives tool authors formatting support without needing a per-item API to control it.
34a064a to
aee90a4
Compare
cpsievert
reviewed
May 18, 2026
cpsievert
reviewed
May 18, 2026
cpsievert
reviewed
May 18, 2026
cpsievert
reviewed
May 18, 2026
Revert unrelated formatting changes in _chat.py and _chat_provider_types.py. Drop underscore prefixes from helper functions in _chat_normalize_chatlas.py (module is already private). Import types from chatlas.types where available, keeping chatlas._content only for ContentPDF which isn't yet publicly exported.
cpsievert
reviewed
May 18, 2026
ContentPDF is not exported from chatlas.types until chatlas > 0.18.0. Add _chatlas_compat.py that tries chatlas.types first and falls back to chatlas._content, centralizing the compat logic for all 3 import sites. Add test_chatlas_compat_cleanup_reminder that fails when the minimum chatlas version exceeds 0.18.0, prompting removal of the shim.
ContentImageInline and ContentImageRemote have been in chatlas.types since v0.15.0 — the try/except ImportError guard is unnecessary given our minimum of chatlas >= 0.17.0.
Addresses review feedback from cpsievert on PR #225.
The explicit re-export aliases are needed for pyright to recognize the symbol as public.
gadenbuie
added a commit
that referenced
this pull request
May 19, 2026
* feat(js): render images and PDFs in tool result cards Add content_extra valueType to ToolResult component that parses a JSON array of content items and renders <img> elements for images and a PdfBadge component for PDFs. * feat(r): support images and PDFs in tool result display Add content_extra valueType for ContentImage and ContentPDF tool results. Refactors tool_string() into tool_default_display() which detects image/PDF content objects and serializes them as structured JSON for the React frontend. * feat(py): support images and PDFs in tool results Handle both streaming and turn-replay paths for content images/PDFs. Add content_extra valueType and helpers for structured JSON rendering. Register standalone ContentImageInline/ContentImageRemote/ContentPDF handlers for the turn-replay path. Suppress tool-content XML wrappers. * `air format` (GitHub Actions) * feat: support mixed content in tool result lists Extend content_extra to handle interleaved lists where images/PDFs are mixed with other value types. Non-content-extra items serialize as text items with value and value_type fields, rendered by React using the appropriate renderer (code, markdown, html, or plain text). * feat: handle ContentText in mixed content tool result lists Use Content base class check (not just images/PDFs) to detect mixed content lists. ContentText items extract the text slot directly; other Content types fall back to string coercion. * feat: default text items in mixed content to markdown rendering Markdown degrades gracefully for plain text and gives tool authors formatting support without needing a per-item API to control it. * chore: rebuild dist * ci: run * chore(py): format * docs: add changelog entries for tool result image/PDF rendering * refactor(py): address PR review feedback Revert unrelated formatting changes in _chat.py and _chat_provider_types.py. Drop underscore prefixes from helper functions in _chat_normalize_chatlas.py (module is already private). Import types from chatlas.types where available, keeping chatlas._content only for ContentPDF which isn't yet publicly exported. * chore: restore _chat.py * refactor(py): add compat shim for ContentPDF import ContentPDF is not exported from chatlas.types until chatlas > 0.18.0. Add _chatlas_compat.py that tries chatlas.types first and falls back to chatlas._content, centralizing the compat logic for all 3 import sites. Add test_chatlas_compat_cleanup_reminder that fails when the minimum chatlas version exceeds 0.18.0, prompting removal of the shim. * refactor(py): remove unnecessary try/except for content type imports ContentImageInline and ContentImageRemote have been in chatlas.types since v0.15.0 — the try/except ImportError guard is unnecessary given our minimum of chatlas >= 0.17.0. * docs(py): explain chatlas tool-content XML wrapper suppression Addresses review feedback from cpsievert. * style(py): suppress ruff PLC0414 in _chatlas_compat.py The explicit re-export aliases are needed for pyright to recognize the symbol as public.
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.
Closes #106 (rewritten)
Closes #161
Summary
When ellmer/chatlas tools return
ContentImageorContentPDFobjects, shinychat now renders them visually inside tool result cards: images as<img>elements, PDFs as a filename badge with a PDF icon.Tools can also return mixed content lists — e.g.
list(ContentText("summary"), content_image_file("plot.png"), ContentText("stats"))— and each item renders in order with appropriate formatting. Text items in mixed lists default to markdown rendering.How it works
The server sends a new
valueType = "content_extra"with a JSON array of typed items:[ {"type": "text", "value": "Generated 50 samples", "value_type": "markdown"}, {"type": "image", "src": "data:image/png;base64,..."}, {"type": "text", "value": "Mean: 0.123, SD: 0.987", "value_type": "markdown"} ]Changes by layer
content_extravalue type inToolResultwith renderers forimage,pdf, andtextitem types.ContentExtraTextdispatches onvalue_type(markdown, html, code, plain text). NewPdfBadgecomponent.tool_default_display()detectsContentobjects in tool result values and serializes them viaas_content_extra_item_or_text().ContentTextextracts@text; images/PDFs serialize to structured data.ContentToolResultwith image/PDF values) and turn-replay path (standalone content items after chatlas expansion). Suppresses<tool-content>XML wrapper tags from chatlas expansion.Design notes
value_type: "markdown". There's currently no per-item API to override this — a future enhancement could add that via the display options.Contentsubclasses (e.g.ContentText()for text, not bare strings).displayoptions (html/markdown/text) still take full precedence over automatic content-extra rendering.R example app
Python example app
Test plan
content_image_file()renders inline image in tool result cardcontent_image_url()renders remote imagecontent_pdf_file()renders PDF badgeContentText+ image +ContentText) renders interleaved<tool-content>XML wrapper text visible in UIdisplay(html/markdown/text) still takes precedence over content-extra