Repository navigation
Native visuals: agents show interactive Luau visuals rendered by GPUI - #900
Merged
Merged
Conversation
show_visual now takes a Luau script instead of an HTML page. The script builds its data and state once and returns a view function whose ui.* tree (layout, text, stats, tables, charts, tabs, toggles, sliders, buttons, links, 2D canvases, 3D scenes) Zeron renders with its own GPUI elements, so a visual scrolls, clips, blurs under the glass composer and takes input like any transcript row on every platform. zeron-visual is the GPUI-free runtime: a Luau sandbox with no I/O, a memory cap and per-call time budgets, seeded randomness, strict props whose style rules are the API (theme color tokens, the transcript's type scale, no fills or borders), canvas drawing commands, and 3D scene projection. The engine runs every script through check() before showing it and returns line-numbered errors to the agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Scenes rasterize with a depth buffer (no more painter's-algorithm occlusion errors), clip at the near plane, shade curved solids smoothly, and paint as one image; the software path renders in parallel row bands at full resolution. - With a GPU (wgpu, new crate zeron-visual-gpu), scenes render with PBR materials (roughness, metal, glow, glass with ior and dispersion), soft shadows, MSAA, and bloom; the software renderer is the fallback. - ui.shader runs Shadertoy-style GLSL on the GPU: iTime, iResolution, iMouse, iCamera (orbit), named inputs bound to state, iNoise, and up to four buffer passes read as iChannel0-3 with feedback. Composited by blend (light by default, so black is clear; alpha; opaque) with optional bloom. Each pass is validated with naga so errors reach the agent with their line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assets: show_visual takes files (glTF models, PNG/JPEG/WebP/HDR images) from the agent's working directory. The engine checks and stores them beside the script; scenes gain s:model, textures, environment maps and sprites, and shaders get iTexture0-3. Limits (16 files, 8 MB each, 10 MB together) fit the MCP server's one WebSocket message to the engine. Rendering: GPU scenes and shaders fade out toward their edges; plain translucent surfaces blend unbent (only glass refracts) and, with sprites, draw before glass so it refracts them; bloom composites correctly in alpha mode; shader images sample upright as on Shadertoy. iOS: each visual gets a Rust worker thread in zeron-mobile that runs the script, lays the tree out, draws canvases and charts, and renders scenes and shaders with wgpu; the transcript takes each visual's measured height, and VisualView only paints what arrives and forwards touches. The demo gains an "Agent visuals" chat (ZERON_DEMO_VISUALS can point it at a desktop's visuals). Hardening (security and code review): - glTF: images stay encoded and are size-checked when drawn; accessor shapes are validated; node walks are bounded; parsing can't panic. - Images: a pixel budget besides the side limit; textures larger than the GPU allows are refused. - wgpu errors are caught or logged, never a panic; the texture cache is keyed by content, bounded, and decodes outside its lock. - The engine checks for a running turn first and removes partial writes; asset names may not differ only by case; the MCP server reads only regular files, bounded. - Desktop: released visuals free their GPU images; iTime and iMouse follow the docs; no second render when a drag ends. - iOS: fonts are shared, not leaked per visual; offscreen and backgrounded visuals pause; commands see the tree the last one left; failed loads retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Desktop scenes and shaders rendered and read back inside GPUI's paint, blocking the main thread on the GPU every animated frame, so a heavy agent shader could stall the app. Paint now does only the cheap parts (input, uniforms, the scene's budgeted Lua draw) and hands the render to a background thread, one frame in flight per visual; the view shows the newest finished frame and repaints when the next lands. Scenes also allocated ~70 MB of render targets every frame. They now live in a SceneState kept per visual (desktop and iOS) and are remade only when the size changes; scenes and shaders keep their readback staging buffer too. The test room's steady frame went from ~8 ms to ~3 ms at 1440x720. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bigx333
added a commit
to bigx333/zeron
that referenced
this pull request
Oct 15, 2026
main's native visuals (zeronsh#900) added WidgetKind.Visual to the shared core, which made the transcript's widget `when` non-exhaustive. Android has no visual renderer yet, so the row keeps its space and draws nothing, as iOS does without a session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bigx333
pushed a commit
to bigx333/zeron
that referenced
this pull request
Oct 15, 2026
…zeronsh#900) * Render agent visuals natively from sandboxed Luau scripts show_visual now takes a Luau script instead of an HTML page. The script builds its data and state once and returns a view function whose ui.* tree (layout, text, stats, tables, charts, tabs, toggles, sliders, buttons, links, 2D canvases, 3D scenes) Zeron renders with its own GPUI elements, so a visual scrolls, clips, blurs under the glass composer and takes input like any transcript row on every platform. zeron-visual is the GPUI-free runtime: a Luau sandbox with no I/O, a memory cap and per-call time budgets, seeded randomness, strict props whose style rules are the API (theme color tokens, the transcript's type scale, no fills or borders), canvas drawing commands, and 3D scene projection. The engine runs every script through check() before showing it and returns line-numbered errors to the agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Render visual scenes and shaders on the GPU - Scenes rasterize with a depth buffer (no more painter's-algorithm occlusion errors), clip at the near plane, shade curved solids smoothly, and paint as one image; the software path renders in parallel row bands at full resolution. - With a GPU (wgpu, new crate zeron-visual-gpu), scenes render with PBR materials (roughness, metal, glow, glass with ior and dispersion), soft shadows, MSAA, and bloom; the software renderer is the fallback. - ui.shader runs Shadertoy-style GLSL on the GPU: iTime, iResolution, iMouse, iCamera (orbit), named inputs bound to state, iNoise, and up to four buffer passes read as iChannel0-3 with feedback. Composited by blend (light by default, so black is clear; alpha; opaque) with optional bloom. Each pass is validated with naga so errors reach the agent with their line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add visual assets and iOS visuals, and harden untrusted visual input Assets: show_visual takes files (glTF models, PNG/JPEG/WebP/HDR images) from the agent's working directory. The engine checks and stores them beside the script; scenes gain s:model, textures, environment maps and sprites, and shaders get iTexture0-3. Limits (16 files, 8 MB each, 10 MB together) fit the MCP server's one WebSocket message to the engine. Rendering: GPU scenes and shaders fade out toward their edges; plain translucent surfaces blend unbent (only glass refracts) and, with sprites, draw before glass so it refracts them; bloom composites correctly in alpha mode; shader images sample upright as on Shadertoy. iOS: each visual gets a Rust worker thread in zeron-mobile that runs the script, lays the tree out, draws canvases and charts, and renders scenes and shaders with wgpu; the transcript takes each visual's measured height, and VisualView only paints what arrives and forwards touches. The demo gains an "Agent visuals" chat (ZERON_DEMO_VISUALS can point it at a desktop's visuals). Hardening (security and code review): - glTF: images stay encoded and are size-checked when drawn; accessor shapes are validated; node walks are bounded; parsing can't panic. - Images: a pixel budget besides the side limit; textures larger than the GPU allows are refused. - wgpu errors are caught or logged, never a panic; the texture cache is keyed by content, bounded, and decodes outside its lock. - The engine checks for a running turn first and removes partial writes; asset names may not differ only by case; the MCP server reads only regular files, bounded. - Desktop: released visuals free their GPU images; iTime and iMouse follow the docs; no second render when a drag ends. - iOS: fonts are shared, not leaked per visual; offscreen and backgrounded visuals pause; commands see the tree the last one left; failed loads retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Render desktop visual frames off the main thread and reuse GPU targets Desktop scenes and shaders rendered and read back inside GPUI's paint, blocking the main thread on the GPU every animated frame, so a heavy agent shader could stall the app. Paint now does only the cheap parts (input, uniforms, the scene's budgeted Lua draw) and hands the render to a background thread, one frame in flight per visual; the view shows the newest finished frame and repaints when the next lands. Scenes also allocated ~70 MB of render targets every frame. They now live in a SceneState kept per visual (desktop and iOS) and are remade only when the size changes; scenes and shaders keep their readback staging buffer too. The test room's steady frame went from ~8 ms to ~3 ms at 1440x720. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: wing-anara <wing@anara.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bigx333
added a commit
to bigx333/zeron
that referenced
this pull request
Oct 15, 2026
main's native visuals (zeronsh#900) added WidgetKind.Visual to the shared core, which made the transcript's widget `when` non-exhaustive. Android has no visual renderer yet, so the row keeps its space and draws nothing, as iOS does without a session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 of 6 tasks
bigx333
added a commit
to bigx333/zeron
that referenced
this pull request
Oct 19, 2026
main's native visuals (zeronsh#900) added WidgetKind.Visual to the shared core, which made the transcript's widget `when` non-exhaustive. Android has no visual renderer yet, so the row keeps its space and draws nothing, as iOS does without a session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Agents can show interactive visuals (charts, tables, dashboards, stepping explainers, simulations, real 3D) inline in their replies. A visual is a sandboxed Luau script that Zeron renders with its own GPUI elements, not a web page. This replaces the WebKit approach in #898.
Hosting a browser engine inline kept producing workarounds: offscreen compositing on Linux, snapshotting an invisible
WKWebViewon macOS, pointer arming and disarming to route clicks, focus handoffs, a loopback server, height reporting, injected font and zoom fixes, and a card fallback on Windows. All of that is gone. A visual is now ordinary transcript content, so it scrolls, clips, blurs under the glass composer, takes clicks and drags, and follows theme and text-size changes the same way on Linux, macOS, and Windows.Why Luau instead of a JSON component tree
A JSON tree of preset components can't express a visual we didn't predict, and its state handling turns into a small expression language. A script can draw anything and owns its state, so I went with Luau, but kept the output as a tree of Zeron nodes so the style rules still hold:
ui.*nodes:column,row,grid,text/span,stat,table,chart(bar, line, area, scatter, with axes, legend and hover readout built in),tabs,toggle,slider,button,link,canvas(2D vector drawing), andscene(3D). Controls call handlers, the handlers assign to state, and Zeron renders the view again.text,muted,accent,chart1…chart6, and so on), text sizes are steps on the transcript's type scale, the font is always the interface font, and layout nodes have no fill, border, shadow, padding, or font props. Using a hex color,background, orfontfails with an error that explains the rule, so agents can't build cards and every visual reads on glass or opaque, light or dark.ui.scenetakes boxes, spheres, cylinders, height-field surfaces, meshes, lines, points and labels. The runtime frames an orbit camera on the geometry (with a fit that doesn't change size while it spins), lights faces with one soft light, culls and depth-sorts them, and hands the desktop 2D polygons to paint as GPUI paths. The reader drags to orbit, andspinadds a turntable.string,table,math,utf8andbit32libraries are loaded (no I/O, network orrequire), with a 64 MB memory cap and a time budget per call: 250 ms for setup, renders and handlers, 60 ms per animated frame. An endless loop fails with a message instead of freezing the app.math.randomis reseeded from the visual's id before every call, so generated data is identical on every render and device.PublishVisualruns the script the way the desktop will: setup, the first render, every canvas and scene draw, then each tab, toggle, slider end and button once. The first error goes back to the agent with its line number. In testing, an agent used a pixel size for canvas text, gotvisual script failed: in a canvas draw: visual:133: unknown text size '13': use xs, sm, body, lg, xl, or 2xl (the transcript's type scale), fixed it and resent.Agent-authored examples
These were written by Claude from the tool description alone (orbit speeds and inclinations are Claude's figures):
A height-field surface whose frequency slider and wireframe toggle re-run the scene (light glass):
What changed
crates/visual(zeron-visual, new, GPUI-free): the Luau runtime (mluawith Luau), theuiprelude, node parsing with strict props, canvas drawing commands, 3D scene building and projection, andcheck(). 17 tests cover the sandbox, time and memory limits, style enforcement, canvas, 3D culling and depth order, and the publish-time check.crates/ui/src/visuals.rs(rewritten): renders the tree with existing idioms: markdown's table treatment, the settings tab, switch and button widgets, the transcript's rule, and canvases and scenes painted as GPUI paths and shaped text in theme colors. Canvas draws are cached per render unless animated, and only visible rows paint, so offscreen animation costs nothing. When the tree changes shape (a tab switch, for example), the visual emits an event and the transcript remeasures that row.show_visual {title, script}on the Zeron MCP server (only inside a chat), thePublishVisualRPC (now runscheckon a blocking thread), uploads store{id}-visual.luau, and anAgentEvent::Visual/visualdoc part carrying only the path and title (theheightfield is gone, since native layout measures itself).check, and another asserts the description names every color the runtime accepts.transparent/zoomcommands, the inlineBrowserSurfacemode, andinline_macos.rs.Since the first version: GPU rendering, assets, iOS, hardening
GPU rendering (
crates/visual-gpu, new).ui.scenerenders through wgpu, with physically based materials, soft shadows, glass that refracts with dispersion, bloom and 4× multisampling. The software rasterizer is now the fallback where there's no GPU. A newui.shaderruns Shadertoy-style GLSL withiMouse, an orbit camera, named inputs bound to state, up to four feedback buffers,iNoiseand blend modes, and naga validates it at publish time. Visuals fade out toward their edges instead of stopping at a hard line.Assets.
show_visualtakes files from the agent's working directory:s:model);s:environment) and sprites (s:sprite), or as shader images (iTexture0-3).The engine checks the files and stores them beside the script, and they travel to other devices like attachments. The limits are 16 files, 8 MB each and 10 MB together, sized so the request fits in one WebSocket message to the engine.
iOS (
crates/mobile/src/visual,VisualView.swift). Each visual gets a Rust worker thread that:Swift only paints what arrives and forwards touches, so the main thread stays nearly idle: 1.7% busy while a canvas, a scene and a shader all animate. Transcript rows take each visual's measured height. Visuals pause when they're offscreen or the app is in the background. The demo has an "Agent visuals" chat (
-demo -route chat:chat-visuals).Hardening. A security audit and code review of the whole change led to these fixes:
Testing
cargo test -p zeron-visual -p zeron-mcp -p zeron-doc, the engine e2epublish_visual_lands_in_the_live_turn(now also checks that a broken script is refused and leaves no part), and the UIvisualstests.ZERON_DEMO_VISUALS. A UI test (VisualFlowTests) drives a dashboard's toggle, slider and tabs.cargo testforzeron-visual,zeron-visual-gpu,zeron-mobile,zeron-client,zeron-mcpandzeron-proto. New tests cover the glTF attacks (a node cycle, short attributes, a mis-shaped accessor, an oversized embedded image), shader image orientation, translucency, the edge fade, the iOS layout and worker, and load retries.Follow-ups
Supersedes #898.
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.