Vectrum
Sign In

Tell agents about inline UI, with a setting to turn it off

Claude and Pi get how to write mermaid, svg, html and openui blocks in
their system prompt, joined with the browser's into one
--append-system-prompt; jcode gets it with the first message. Settings →
General → Inline diagrams and UI (on by default) turns off both the
instructions and the rendering, which leaves the blocks as code. A test
keeps the examples agents are shown parsing. The README describes the
feature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JjohannSocommitted 1 parent 4a52bd0commit5dfe90d

Showing 9 changed files with 97 additions and 12 deletions.

+1−1Cargo.lockView File
@@ -7480,8 +7480,8 @@ dependencies = [
74807480 "gpui-kit",
74817481 "image",
74827482 "libc",
7483- "openh264",
74847483 "mermaid-rs-renderer",
7484+ "openh264",
74857485 "serde",
74867486 "serde_json",
74877487 "smol",
+7−0README.mdView File
@@ -79,6 +79,13 @@ Gruvbox, Rosé Pine, Solarized and Dracula.
7979 switch Claude into plan mode and back.
8080 - Finished turns fold their tool calls and interim messages into one **Worked for 12s** row
8181 above the answer. Click it to unfold.
82+ - Answers can carry live diagrams and UI, written as plain fenced code blocks. ` ```mermaid `
83+ draws a diagram in the theme's colors. ` ```openui ` ([OpenUI Lang](https://www.openui.com/))
84+ draws native cards, stats, tables, charts, tabs and forms; their state stays put while
85+ you scroll, and buttons and form submissions reach the agent as `[UI]` messages.
86+ ` ```svg ` draws the image. ` ```html ` gets a **Preview** button that opens the page in the
87+ thread's browser panel. Every block keeps **Copy** and **Show source**. Agents are told
88+ how to write them; Settings → General → *Inline diagrams and UI* turns it all off.
8289 - Under each turn that touched files, **N changed files +a −r** with a folder tree and
8390 per-file counts. Click a file, or **Open diff**, to read the diff in the app. The app
8491 snapshots the working tree through a private git index, so your staging area is never
+9−6src/claude.rsView File
@@ -198,12 +198,15 @@ impl AgentProcess {
198198 if config.tools.devices {
199199 crate::device::agent::prepare_command(&mut command);
200200 }
201- // Claude and Pi take extra system prompt with this flag.
202- if config.instance.kind != Provider::Jcode
203- && let Some(session) = &config.browser_session
204- && let Some(prompt) = browser::prepare_agent(&mut command, session)
205- {
206- command.args(["--append-system-prompt", &prompt]);
201+ // Claude and Pi take extra system prompt with this flag, once: the
202+ // browser's, if the thread has one, and how to write inline UI.
203+ if config.instance.kind != Provider::Jcode {
204+ let browser = config.browser_session.as_ref().and_then(|session| browser::prepare_agent(&mut command, session));
205+ let inline = crate::settings::get(|settings| settings.inline_ui).then(|| crate::inline_ui::INSTRUCTIONS.to_string());
206+ let prompts: Vec<String> = browser.into_iter().chain(inline).collect();
207+ if !prompts.is_empty() {
208+ command.args(["--append-system-prompt", &prompts.join("\n\n")]);
209+ }
207210 }
208211 Self::spawn_command(&binary, command, events)
209212 }
+51−0src/inline_ui/mod.rsView File
@@ -4,3 +4,54 @@
44
55 pub mod eval;
66 pub mod lang;
7+
8+ /// What agents are told about rich blocks, when the setting is on: in the
9+ /// system prompt for Claude and Pi, and with jcode's first message.
10+ pub const INSTRUCTIONS: &str = r##"## Inline UI in Vectrum Code
11+
12+ Vectrum Code shows your Markdown answers and renders some fenced code blocks live. Plain Markdown comes first; use these only when a picture, table or control says it better than prose: architecture and flows, comparisons, results data, choices for the user to make.
13+
14+ - ```mermaid: a diagram (flowchart, sequenceDiagram, classDiagram, stateDiagram-v2, erDiagram, gantt, pie, mindmap).
15+ - ```svg: a small self-contained SVG image, with width and height set.
16+ - ```html: a self-contained page with inline CSS and JS. It shows as code; the user can open it with Preview.
17+ - ```openui: native UI written in OpenUI Lang, below.
18+
19+ OpenUI Lang: one statement per line, `name = Expression`, `root = ...` first; forward references are fine. Arguments are positional, and optional ones can be left off the end. Values: strings, numbers, true/false/null, [arrays], {key: value} objects, references to other statements, `a.b` (on an array it takes `b` from every element), `cond ? a : b`, arithmetic, comparisons, && || !. State: declare `$name = default` and read `$name`; an input given `$name` as its first argument edits it, and everything reading it updates.
20+ Built-ins: @Count @Sum @Avg @Min @Max @First @Last @Round(n, decimals) @Abs @Floor @Ceil @Filter(array, field, op, value) @Sort(array, field, "asc"|"desc") @Each(array, "item", template).
21+ Actions, a Button's second argument: Action([steps]) with @Set($x, value), @Reset($x), @ToAssistant("message"), @OpenUrl("https://..."); or "submit:formName" to send a form's values to you, or "action:name".
22+ Components:
23+ - Layout: Stack(children, direction "column"|"row", gap "s"|"m"|"l", align, justify, wrap), Card(children, variant "raised"|"well"|"plain"), CardHeader(title, subtitle), Tabs([TabItem(value, label, content)]), Separator().
24+ - Content: TextContent(text, size "small"|"default"|"large"|"large-heavy"), Markdown(text), Callout(variant "info"|"success"|"warning"|"error", title, description), Tag(text, variant), Stat(label, value, delta), Progress(value 0..1 or 0..100, label), Image(url, alt), CodeBlock(language, code).
25+ - Data: Table([Col(label, values, type "text"|"number"|"tag")]), BarChart(labels, [Series(name, values)], "grouped"|"stacked", xLabel, yLabel), LineChart and AreaChart(labels, [Series(name, values)], xLabel, yLabel), PieChart(labels, values).
26+ - Input: Form(name, [fields], Buttons([...])), FormControl(label, input, hint), Input(name, placeholder, type "text"|"email"|"number"|"password", rules ["required", "email"]), TextArea(name, placeholder, rows), Select(name, options, placeholder), Checkbox(name, label), Switch(name, label), Slider(name, min, max, step), Buttons([buttons], direction), Button(label, action, variant "primary"|"secondary"|"danger"|"ghost").
27+ Query, Mutation and @Run aren't available. When the user acts on your UI you get a message starting with [UI], such as `[UI] Submitted "deploy": {"env":"staging"}`. Answer it in a new message; earlier blocks keep their state.
28+
29+ Examples:
30+ ```openui
31+ root = Card([header, form])
32+ header = CardHeader("Deploy", "Pick a target")
33+ form = Form("deploy", [FormControl("Environment", Select("env", ["staging", "production"]))], Buttons([Button("Deploy", "submit:deploy", "primary")]))
34+ ```
35+ ```openui
36+ $count = 0
37+ root = Stack([TextContent("Count: " + $count, "large"), Buttons([Button("-", Action([@Set($count, $count - 1)])), Button("+", Action([@Set($count, $count + 1)]), "primary")], "row")])
38+ ```"##;
39+
40+ #[cfg(test)]
41+ mod tests {
42+ use super::{INSTRUCTIONS, eval, lang};
43+
44+ /// The examples agents are shown must parse and render.
45+ #[::core::prelude::v1::test]
46+ fn instruction_examples_parse() {
47+ let blocks: Vec<&str> = INSTRUCTIONS.split("```openui\n").skip(1).map(|block| block.split("```").next().unwrap_or_default()).collect();
48+ assert_eq!(blocks.len(), 2);
49+ for block in blocks {
50+ let program = lang::parse(block);
51+ assert!(program.errors.is_empty(), "{block}: {:?}", program.errors);
52+ assert!(program.undefined().is_empty(), "{block}");
53+ assert!(!eval::evaluate(&program, &eval::State::new()).is_pending());
54+ }
55+ assert!(INSTRUCTIONS.lines().count() < 60);
56+ }
57+ }
+7−3src/session.rsView File
@@ -1607,11 +1607,15 @@ impl Session {
16071607 let mut request = json!({"req": "send_message", "content": prompt, "images": images});
16081608 // jcode's system prompt can't be added to, so the app's
16091609 // instructions go along with the first message.
1610- let instructions = (!self.jcode.instructions_sent)
1610+ let first = !self.jcode.instructions_sent;
1611+ self.jcode.instructions_sent = true;
1612+ let instructions = first
16111613 .then(|| orchestrator::instructions(self.tool_caps()))
16121614 .filter(|instructions| !instructions.is_empty());
1613- self.jcode.instructions_sent = true;
1614- let reminder: Vec<String> = instructions.into_iter().chain(self.jcode.browser_note.take()).collect();
1615+ let inline = (first && settings::get(|settings| settings.inline_ui))
1616+ .then(|| crate::inline_ui::INSTRUCTIONS.to_string());
1617+ let reminder: Vec<String> =
1618+ instructions.into_iter().chain(inline).chain(self.jcode.browser_note.take()).collect();
16151619 if !reminder.is_empty() {
16161620 request["system_reminder"] = reminder.join("\n\n").into();
16171621 }
+5−0src/settings.rsView File
@@ -449,6 +449,9 @@ pub struct Settings {
449449 pub slash_skills: bool,
450450 /// Highlight Markdown in the composer as you type.
451451 pub rich_composer: bool,
452+ /// Draw diagrams, charts and small UIs from agents' answers, and tell
453+ /// agents they can write them.
454+ pub inline_ui: bool,
452455 /// Rest the composer to one line while scrolling a conversation.
453456 pub collapse_composer: bool,
454457 pub time_format: TimeFormat,
@@ -600,6 +603,7 @@ impl Default for Settings {
600603 streaming: Streaming::Paragraphs,
601604 slash_skills: true,
602605 rich_composer: true,
606+ inline_ui: true,
603607 collapse_composer: true,
604608 time_format: TimeFormat::System,
605609 proactive_panels: false,
@@ -709,6 +713,7 @@ impl Settings {
709713 ("Show skills in slash menu", self.slash_skills != d.slash_skills),
710714 ("Collapse composer on scroll", self.collapse_composer != d.collapse_composer),
711715 ("Rich text composer", self.rich_composer != d.rich_composer),
716+ ("Inline diagrams and UI", self.inline_ui != d.inline_ui),
712717 ("Send shortcut", self.send_shortcut != d.send_shortcut),
713718 ("Follow-up behavior", self.follow_up != d.follow_up),
714719 ("Context window indicator", self.context_meter != d.context_meter),
+7−2src/ui/inline/mod.rsView File
@@ -98,9 +98,9 @@ struct Fence {
9898 program: Option<Arc<Program>>,
9999 }
100100
101- /// Whether answers render rich blocks at all.
101+ /// Whether answers render rich blocks at all (Settings → General).
102102 pub fn enabled() -> bool {
103- true
103+ crate::settings::get(|settings| settings.inline_ui)
104104 }
105105
106106 /// The workspace, so a block's buttons can open previews in its panels.
@@ -131,6 +131,11 @@ pub fn preview_html(code: &str, window: &mut Window, cx: &mut App) {
131131
132132 /// Adds the rich block hooks to an answer's Markdown view.
133133 pub fn extend(view: TextView, icx: InlineCx) -> TextView {
134+ // With the setting off the view has no hooks, and the change in their
135+ // number makes it parse the answer again as plain code blocks.
136+ if !enabled() {
137+ return view;
138+ }
134139 view.markdown_block_parser(parse).markdown_block_renderer(NODE, move |node, window, cx| render(node, &icx, window, cx))
135140 }
136141
+9−0src/ui/settings/general.rsView File
@@ -388,6 +388,15 @@ impl SettingsView {
388388 .reset(reset(&view, cx, |s| s.rich_composer, |s, on| s.rich_composer = on))
389389 .control(switch("rich-composer", saved.rich_composer, &view, p, |settings, on| settings.rich_composer = on)),
390390 )
391+ .row(
392+ row("Inline diagrams and UI")
393+ .id("inline-ui")
394+ .description(
395+ "Let agents draw diagrams, charts and small interactive UIs in their answers. Applies to the agent instructions when a thread's agent next starts.",
396+ )
397+ .reset(reset(&view, cx, |s| s.inline_ui, |s, on| s.inline_ui = on))
398+ .control(switch("inline-ui", saved.inline_ui, &view, p, |settings, on| settings.inline_ui = on)),
399+ )
391400 .row(
392401 row("Collapse composer on scroll")
393402 .id("composer-collapse")
+1−0src/ui/settings/search.rsView File
@@ -81,6 +81,7 @@ pub const ITEMS: &[Item] = &[
8181 item("proactive-panels", "Proactive panels", Page::General, "automatically open diff right panel agent completion"),
8282 item("skills-in-slash-menu", "Show skills in slash menu", Page::General, "command menu dollar $ slash /"),
8383 item("composer-rich-text", "Rich text composer", Page::General, "composer rich text bold italic markdown styled wysiwyg"),
84+ item("inline-ui", "Inline diagrams and UI", Page::General, "mermaid openui charts tables forms svg html preview artifacts render answers"),
8485 item("composer-collapse", "Collapse composer on scroll", Page::General, "composer rest resting scroll wheel conversation shrink minimize"),
8586 item("send-shortcut", "Send shortcut", Page::General, "enter return ctrl multiline prompt new line composer"),
8687 item("follow-up-behavior", "Follow-up behavior", Page::General, "queue steer running turn send default behavior composer"),