DocsVis

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-form vis html block draws there only when the session allows it, and vis svg shows 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"
```
How a reply reaches your browser
One processprompt over WebSocketrequeststreamed tokenseventsyesOne processOne processBrowser tabBrowser tabSova serverSova serverAgent sessionAgent session1Model providerModel providerRun settled?Run settled?
One processprompt overWebSocketrequeststreamedtokenseventsyesOne processOne processBrowser tabBrowser tabSova serverSova serverAgent sessionAgent session1Model providerModel providerRun settled?Run settled?
  1. 1one writer per session file
The server owns the session; the browser only streams what it sends.

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"
```
A worker's life
slot freeasks a questionanswerederrorretry oncesettleds0QueuedQueuedRunningRunningNeeds youNeeds you1FailedFaileddone
slot freeasks aquestionanswerederrorretry oncesettleds0QueuedQueuedRunningRunningNeeds youNeeds you1FailedFaileddone
  1. 1shows in Needs you
A worker that fails is retried once, then reported to its parent.

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"
```
Delegating a fix to a worker
YouChatWorkerFix the flaky login testbrief: reproduce, fix, run the suiteWorker runsruns the test 20 timesfails 3 of 20 on a timer racereport: fixed, suite green1summary and the diff
YouChatWorkerFix the flaky logintestbrief: reproduce,fix, run the suiteWorker runsruns the test20 timesfails 3 of 20 ona timer racereport: fixed, suitegreen1summary and thediff
  1. 1the worker checks its own fix
The parent waits on the worker's report, not on its transcript.

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"
```
Where a chat lives
  1. Browser
    • Chat view
    • composer
    • service worker
  2. 1Sova server
    • REST API
    • WebSocket
    • session registry
    one process per checkout
  3. Agent
    • Tools
    • model calls
    • minor modes
  4. Disk
    • Session files
    • settings
  1. 1holds every live session
Everything above the disk can restart without losing a word.

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"
```
What the refactor touched
  • app/
    • auth/
      • 1login.tstoken check moved here
      • session.ts
    • routes/
      • index.ts
  • tests/
    • auth/
      • login.test.tsnew: expired token
    • …
  1. 1the only behaviour change
Two files changed; the tests moved with them.

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"
```
Test suite time by package
s
050100150200250e2ee2eserverserverwebwebsharedsharede2e: 214 sserver: 96 sweb: 58 sshared: 12 s2149658121
s
0100200300e2ee2eserverserverwebwebsharedsharede2e: 214 sserver: 96 sweb: 58 sshared: 12 s2149658121
  1. 1run it last, in its own job
The e2e package takes over half the run.

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"
```
How the outage was found
  1. 14:02First 502 on loginfrom the edge logs
  2. 14:09Alert fireserror rate over 5%
  3. 14:181Cause foundan expired TLS certificate on the auth service
  4. 14:31Certificate renewed
  5. 14:43Error rate back to normal
  1. 1the renewal job had been off since March
41 minutes from the first error to the fix landing.

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"
```
What the login change must handle
Signed in
Valid token
  1. Request
  2. check token
  3. 200 OK
Expired token
  1. Request
  2. check token
  3. 401
  4. sign-in page
1Refresh mid-request
  1. Request
  2. token expires
  3. refresh
  4. request lost
Signed out
No token
  1. Request
  2. sign-in page
  1. 1needs a retry after refresh
One scenario still fails: the token refresh during a request.

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"
```
The settings screen, before and after the change
1Before
Settings
Themefollows the system
Notificationswhen a chat needs you, toggle, on
Default modelused by new chats
Save
2After1
Settings
Default modelused by new chats
Themefollows the system
Notificationswhen a chat needs you, toggle, on
Changes save as you make them
  1. 1no Save button: each row saves itself
The change moves the default model to the top and drops the Save button.

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"
```
Where to run the migration
Main checkout1WorktreeRemote machine
Keeps your branch untouchedNoYesYes
Shares installed packagesYesPartlyafter installNo
Runs the full test suiteYesYesslow (warning)
Easy to throw awayNoYesYes
Main checkout1WorktreeRemote machine
Keeps your branch untouchedNoYesYes
Shares installed packagesYesPartlyafter installNo
Runs the full test suiteYesYesslow (warning)
Easy to throw awayNoYesYes
  1. 1the default for a worker
A worktree keeps your checkout untouched until you merge.

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));
  }
}
```
Why the retry never stops
while (!done) {
try {
431 let attempts = 0;
done = await send(request);
} catch (err) {
462 if (++attempts > MAX_RETRIES) throw err;
await sleep(backoff(attempts));
}
}
  1. 1attempts is reset inside the loop
  2. 2the check that should end it
The counter resets on every pass, so the limit is never reached.

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>
```
Context window, before and after compaction
Before Earlier turns, 92% full After Summary Latest
Compaction keeps a summary and the latest turns.

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.