Minor modes
Vis
With vis on, the agent can draw small diagrams and charts in its replies. Sova draws them with its own code, and never puts the model's HTML or SVG into the page.
What vis does
Vis is a minor mode: turn it on in the chat’s mode menu (see Modes), in either major mode. While it’s on, the agent knows it can draw, and when a picture explains faster than prose, it adds one to its reply: at most 1 or 2 per reply, small, captioned, next to text that says what to notice.
The agent writes a drawing as a fenced code block labelled vis and a kind, such as vis flow.
Sova draws it with its own code instead of showing the block’s text.
The kinds
| Kind | Draws |
|---|---|
vis flow |
Boxes and arrows, with optional frames around groups of nodes, or side-by-side panels |
vis state |
A state machine, with start and end dots |
vis sequence |
Actors, lifelines and numbered messages, which you can step through |
vis layers |
A stack of labelled layers |
vis tree |
An indented hierarchy |
vis chart |
Bars (grouped or stacked), lines or scatter, on a linear or log axis |
vis timeline |
Dated events in order |
vis steps |
Scenario chains, with a status per row |
vis wireframe |
Low-fidelity phone or desktop screens, with arrows between them |
vis matrix |
A comparison grid with yes, no, partial or text cells |
vis code |
An annotated snippet: highlighted, numbered lines with notes |
vis html, vis svg |
Free-form, when no other kind fits (see below) |
Any drawing can mark up to 8 items, each with a tone and a short numbered note listed under the drawing. Colour is never the only signal: a marked item is also heavier, and carries its note’s number.
There is an example of every kind at the end of this page, under Examples.
Reading a drawing
Each drawing has a title (or its kind’s name), the drawing itself, its numbered notes and its caption. Two buttons sit in its head:
- Source shows the block as the agent wrote it.
- Copy copies the whole block.
A sequence starts complete. Step Through walks it one message at a time with Previous and Next (“Step 3 of 8”), dimming the later steps, and Show All ends the walk. Nothing plays by itself.
A drawing never widens the chat: a wide one shrinks to fit, then scrolls sideways inside its own box, at phone widths too. While the agent is still writing a block, its place holds a box reading “Drawing {kind}…”, so the reply doesn’t jump when it finishes.
When a block can’t be drawn
- With warnings. If the intent is clear but something is off, such as a label over 200 characters or a mark that names nothing, the drawing still draws. A muted line under it says what was changed: “Drawn with warnings: …”.
- Broken. A block that can’t be read shows as an ordinary code block, with a line saying why: “Couldn’t draw this vis {kind} block (line N: …), so here is its source.”
When a reply has a broken block, Sova tells the agent with a hidden note, once, and the agent gets one more chance in the same run to re-send only the fixed blocks. The broken block stays visible above the fix.
Free-form drawings, and safety
vis html and vis svg are the fallback when no kind fits. The model’s markup never enters
Sova’s own page:
- It runs in a sandboxed frame, cut off from Sova’s page, whose rules block scripts, styles, images, fonts and connections from anywhere outside the block itself.
- Most animation waits for your first click or key press in the frame.
- A block is meant to stay under 8K characters. Up to 16K it still draws, with a warning; over 16K it doesn’t draw.
Where drawings show
- In the chat, in the agent’s replies.
- On shared pages, where Sova draws every kind but
vis code. A free-formvis htmlblock draws there only when the session allows it, andvis svgshows as a static image. Shared pages show no warning lines, and a block they can’t draw becomes one quiet line.
What the agent gets
With vis on, the agent has a short entry in its instructions: when to draw, and one line per kind.
Each kind’s exact syntax comes from a vis_guide tool, which the agent calls before the first
drawing of a kind in a chat, so a reply that draws nothing carries no drawing grammar. A
vis_check tool lets it test a free-form block before sending it. Both tools come and go with the
mode.
Turning vis on in the middle of a chat reaches the agent from its next message. Workers don’t get vis: drawings are for you, and a worker’s replies are read by its parent session.
Where vis isn’t available
The Overseer is always in normal mode with no minor modes, so it doesn’t draw.
Examples
One block of each kind, as the agent writes it, followed by the drawing Sova makes of it. On this page the drawings are still pictures: in the chat, a sequence’s Step Through works.
vis flow
```vis flow
title: How a reply reaches your browser
caption: The server owns the session; the browser only streams what it sends.
web "Browser tab" -> srv "Sova server" "prompt over WebSocket" -> agent "Agent session" -> model "Model provider" "request"
model --> agent "streamed tokens"
agent --> srv "events"
srv -> done "Run settled?" decision
done -> web "yes"
group "One process" srv agent
mark agent "one writer per session file"
```
- 1one writer per session file
vis state
```vis state
title: A worker's life
caption: A worker that fails is retried once, then reported to its parent.
node s0 start
node queued "Queued"
node running "Running"
node blocked "Needs you"
node failed "Failed"
node done end
s0 -> queued
queued -> running "slot free"
running -> blocked "asks a question"
blocked -> running "answered"
running -> failed "error"
failed -> running "retry once"
running -> done "settled"
mark blocked warn "shows in Needs you"
```
- 1shows in Needs you
vis sequence
```vis sequence
title: Delegating a fix to a worker
caption: The parent waits on the worker's report, not on its transcript.
actor you "You"
actor parent "Chat"
actor worker "Worker"
you -> parent "Fix the flaky login test"
parent -> worker "brief: reproduce, fix, run the suite"
== Worker runs ==
worker -> worker "runs the test 20 times"
note worker "fails 3 of 20 on a timer race"
worker --> parent "report: fixed, suite green"
parent --> you "summary and the diff"
mark 4 "the worker checks its own fix"
```
- 1the worker checks its own fix
vis layers
```vis layers
title: Where a chat lives
caption: Everything above the disk can restart without losing a word.
Browser | Chat view, composer, service worker | accent
Sova server | REST API, WebSocket, session registry | one process per checkout
Agent | Tools, model calls, minor modes
Disk | Session files, settings | muted
mark "Sova server" "holds every live session"
```
- Browser
- Chat view
- composer
- service worker
- 1Sova server
- REST API
- WebSocket
- session registry
- Agent
- Tools
- model calls
- minor modes
- Disk
- Session files
- settings
- 1holds every live session
vis tree
```vis tree
title: What the refactor touched
caption: Two files changed; the tests moved with them.
app/
auth/
login.ts "token check moved here"
session.ts
routes/
index.ts
tests/
auth/
login.test.ts "new: expired token"
…
mark login.ts "the only behaviour change"
```
- app/
- auth/
- 1login.tstoken check moved here
- session.ts
- routes/
- index.ts
- auth/
- tests/
- auth/
- login.test.tsnew: expired token
- …
- auth/
- 1the only behaviour change
vis chart
```vis chart
title: Test suite time by package
caption: The e2e package takes over half the run.
type: bar
unit: s
"e2e" 214 warn
"server" 96
"web" 58
"shared" 12
mark "e2e" "run it last, in its own job"
```
- 1run it last, in its own job
vis timeline
```vis timeline
title: How the outage was found
caption: 41 minutes from the first error to the fix landing.
== Detection ==
14:02 | First 502 on login | from the edge logs | error
14:09 | Alert fires | error rate over 5%
== Fix ==
14:18 | Cause found | an expired TLS certificate on the auth service | warn
14:31 | Certificate renewed
14:43 | Error rate back to normal | | ok
mark "Cause found" "the renewal job had been off since March"
```
- Detection
- 14:02First 502 on loginfrom the edge logs
- 14:09Alert fireserror rate over 5%
- Fix
- 14:181Cause foundan expired TLS certificate on the auth service
- 14:31Certificate renewed
- 14:43Error rate back to normal
- 1the renewal job had been off since March
vis steps
```vis steps
title: What the login change must handle
caption: One scenario still fails: the token refresh during a request.
== Signed in ==
"Valid token" ok | Request -> "check token" -> "200 OK"
"Expired token" ok | Request -> "check token" -> "401" -> "sign-in page"
"Refresh mid-request" error | Request -> "token expires" -> "refresh" -> "request lost"
== Signed out ==
"No token" ok | Request -> "sign-in page"
mark "Refresh mid-request" "needs a retry after refresh"
```
- Request
- check token
- 200 OK
- Request
- check token
- 401
- sign-in page
- Request
- token expires
- refresh
- request lost
- Request
- sign-in page
- 1needs a retry after refresh
vis wireframe
```vis wireframe
title: The settings screen, before and after the change
caption: The change moves the default model to the top and drops the Save button.
screen "Before"
header "Settings"
icon "back"
list
item "Theme" "follows the system"
item "Notifications" "when a chat needs you"
toggle on
item "Default model" "used by new chats"
button "Save" accent
screen "After"
header "Settings"
icon "back"
list
item "Default model" "used by new chats"
item "Theme" "follows the system"
item "Notifications" "when a chat needs you"
toggle on
text "Changes save as you make them"
mark "After" "no Save button: each row saves itself"
```
- 1no Save button: each row saves itself
vis matrix
```vis matrix
title: Where to run the migration
caption: A worktree keeps your checkout untouched until you merge.
columns: Main checkout, Worktree, Remote machine
Keeps your branch untouched | no | yes | yes
Shares installed packages | yes | partial "after install" | no
Runs the full test suite | yes | yes | slow warn
Easy to throw away | no | yes | yes
mark Worktree "the default for a worker"
```
| Main checkout | 1Worktree | Remote machine | |
|---|---|---|---|
| Keeps your branch untouched | No | Yes | Yes |
| Shares installed packages | Yes | Partlyafter install | No |
| Runs the full test suite | Yes | Yes | slow (warning) |
| Easy to throw away | No | Yes | Yes |
| Main checkout | 1Worktree | Remote machine | |
|---|---|---|---|
| Keeps your branch untouched | No | Yes | Yes |
| Shares installed packages | Yes | Partlyafter install | No |
| Runs the full test suite | Yes | Yes | slow (warning) |
| Easy to throw away | No | Yes | Yes |
- 1the default for a worker
vis code
```vis code
title: Why the retry never stops
caption: The counter resets on every pass, so the limit is never reached.
lang: ts
start: 41
mark 43 error "attempts is reset inside the loop"
mark 46 "the check that should end it"
---
while (!done) {
try {
let attempts = 0;
done = await send(request);
} catch (err) {
if (++attempts > MAX_RETRIES) throw err;
await sleep(backoff(attempts));
}
}
```
while (!done) { try { let attempts = 0; done = await send(request); } catch (err) { if (++attempts > MAX_RETRIES) throw err; await sleep(backoff(attempts)); }}- 1attempts is reset inside the loop
- 2the check that should end it
vis svg
```vis svg
title: Context window, before and after compaction
caption: Compaction keeps a summary and the latest turns.
<svg viewBox="0 0 360 120" xmlns="http://www.w3.org/2000/svg" font-family="Inter, system-ui, sans-serif" font-size="12">
<text x="0" y="14" fill="var(--color-ink-2)">Before</text>
<rect x="0" y="22" width="360" height="24" rx="6" fill="var(--color-sunken)" stroke="var(--color-border-strong)"/>
<rect x="0" y="22" width="330" height="24" rx="6" fill="var(--status-warn-bg)" stroke="var(--status-warn)"/>
<text x="10" y="38" fill="var(--color-ink)">Earlier turns, 92% full</text>
<text x="0" y="74" fill="var(--color-ink-2)">After</text>
<rect x="0" y="82" width="360" height="24" rx="6" fill="var(--color-sunken)" stroke="var(--color-border-strong)"/>
<rect x="0" y="82" width="60" height="24" rx="6" fill="var(--color-accent-tint)" stroke="var(--color-accent)"/>
<rect x="64" y="82" width="70" height="24" rx="6" fill="var(--status-info-bg)" stroke="var(--status-info)"/>
<text x="8" y="98" fill="var(--color-ink)">Summary</text>
<text x="72" y="98" fill="var(--color-ink)">Latest</text>
</svg>
```
vis html
```vis html
title: Binary search, one step at a time
caption: Press Step: each step halves the range still in play.
<style>#r{display:flex;gap:4px;flex-wrap:wrap}#r i{font-style:normal;min-width:32px;padding:4px;text-align:center;border:1.5px solid var(--color-border);border-radius:6px}#r .in{border-color:var(--color-accent)}#r .hit{background:var(--status-success-bg);border-color:var(--status-success)}</style>
<p>Looking for 23</p>
<div id="r"></div><button id="s">Step</button>
<script>
var v=[2,5,8,12,16,23,38,56,72,91],lo=0,hi=v.length-1,found=-1,r=document.getElementById("r");
function draw(){r.innerHTML=v.map(function(x,k){return '<i class="'+(k===found?"hit":k>=lo&&k<=hi?"in":"")+'">'+x+'</i>'}).join("")}
document.getElementById("s").onclick=function(){if(found>=0||lo>hi)return;var m=(lo+hi)>>1;if(v[m]===23)found=m;else if(v[m]<23)lo=m+1;else hi=m-1;draw()};
draw();
</script>
```
In the chat this block runs in a sandboxed frame, with its own Step button. This page runs no script, so here is its source only.