Add an inline SVG presentation tool with durable chat replay #80
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
Let the local model deliberately present a static SVG inline at the point of a chat response. This is a chat content type, not an A2UI surface: it has no controls, client events, separate tab, surface lifecycle, or A2UI state.
The SVG must remain visible after switching sessions and after restarting DS4Server.
Research
svgfeature.iced::widget::svg::Handle::from_memoryrenders SVG bytes directly, so no browser/WebView or A2UI renderer is needed: https://docs.rs/iced/0.14.0/iced/widget/svg/struct.Handle.htmlusvg/resvg.resvgintentionally renders only static SVG and does not implement scripts, events, links, or animation: https://github.com/linebender/resvg/blob/main/README.mdCurrent DS4Server path
The implementation should reuse the existing durable tool path:
src/agent.rsdeclares tools inTOOLS, parses model tool calls, executes them, and reconstructsToolCards withstored_tool_cards.messages.contentbysrc/app/generation.rs/src/database.rs.tool = truemessage.ChatMessage, andsrc/app/view/chat.rsrebuilds stored tool cards from the assistant message and following tool result.Therefore the SVG source should have one durable source of truth: the
present_svgtool argument already stored in the assistant message. Do not add an SVG table, duplicate the SVG into the tool result, or write an artifact file for this bounded text payload.Proposed implementation
1. Add a model tool
Add a non-mutating
present_svgtool tosrc/agent.rs:altdescribes the information conveyed by the graphic.SVG presented inline: <alt>so the model can continue its response.Add
ToolHandler::PresentSvgand route it through the existing schema validation andexecute_validatedswitch. Do not create a second tool execution mechanism.2. Validate at the tool boundary
Treat model-produced SVG as untrusted input even though it is rendered by
resvgrather than a browser.Tool error:feedback.usvggeneration already present through Iced; declaring the already-resolvedusvg 0.45dependency directly is preferable to adding a second XML/SVG stack.foreignObject, HTML, or remote URLs.resvgalready omits active browser behavior; keep validation explicit so a later renderer change cannot silently broaden the trust boundary.Validation must happen before the tool reports success. Invalid SVG stays a normal failed tool card and is not rendered.
3. Render the successful call inline
In
src/app/view/chat.rs, render successfulpresent_svgcards as a quiet inline figure in call order instead of the generic tool-card row:iced::widget::svg::Handle::from_memoryfrom the storedsvgargument.altas a muted visible caption so the information has a text equivalent and remains understandable if rendering fails.Completed.present_svgis emitted in the same batch as ordinary tools.Keep extraction of
svgandaltfrom aToolCardin one small pure helper so live and rehydrated rendering use the same rules.4. Rehydrate without a migration
No database migration should be needed. The complete tool call is already persisted in
messages.content; the following tool result records whether validation completed or failed. On reload,stored_tool_cardsmust reconstruct the call and the chat view must render the same successful SVG from its stored arguments.This must also work for conversations that later compact: compaction may change model context, but it must not remove the persisted visible chat record.
Do not store a second base64 or SVG copy. This avoids the history bloat and duplication problems seen when agents persist large image payloads in multiple event/message records.
5. Preserve exports
Update Markdown chat export so a successful
present_svgcall is represented by its alt text and a fencedsvgsource block, rather than a generic tool-result dump. Failed calls remain ordinary tool failures. This keeps the durable chat portable without relying on unsafe raw HTML rendering in Markdown readers.Likely files
Cargo.toml/Cargo.lockonly ifusvgmust be named directly for validation; reuse the version already selected by Iced.src/agent.rsfor the tool schema, handler, limits, validation, and stored-card extraction.src/app/view/chat.rsfor inline rendering.src/app.rsfor Markdown export behavior.src/database.rsonly for a reopen regression test; no schema or migration change is expected.Acceptance criteria
present_svg(svg, alt)and a valid static SVG appears inline in Chat, not in the A2UI tab.foreignObject, HTML, filesystem reads, and network access cannot execute through presented SVG.present_svgcalls preserve their original order and lifecycle states.Minimum verification
present_svgcalls map to inline figure vs. ordinary failed card, including a mixed tool batch.present_svgcall and its tool result, reloads the session, rebuilds stored cards, and recovers byte-identical SVG plus alt text.AGENTS.mdbefore committing the implementation.Non-goals
Implemented and pushed as
8adac26.Verification: both DSML and GLM transports recover the SVG byte-for-byte; validation tests cover empty, oversized, malformed/non-SVG, excessive dimensions/complexity, external references, raster content, and active/foreign content; mixed lifecycle, export, schema, and database reopen regressions pass. Manual local-model verification rendered a two-step diagram inline, preserved it across a session switch, and replayed it after restarting/reopening DS4Server. This is a new DS4Server presentation feature; DS4 has no corresponding SVG functionality, and the existing DS4 transport/A2UI regression suite remains green.
Commit gates passed: cargo fmt --all -- --check; cargo clippy --all-targets --all-features -- -D warnings; make bundle; cargo test --all-features (214 passed, 16 ignored).