snyvi
snyvi.com·Widgets·v1.35.0

Widgets and the sidebars

snyvi's two sidebars are made of sections, and you arrange them on /sidebars (⌘K → Arrange sidebars, or right-click any section's head). Widgets are small sections that anything can fill: an agent, a script, or a widget file snyvi runs on a timer.

The left is global. The right is the desk you are on. A section's scope decides its side, so nothing changes sides, and the sidebars never redraw because you clicked somewhere.

Arranging

Side Sections, in today's order
Left, on every page Inbox · Desks · Folders · Widgets (the global ones)
Right, on a desk Panels · Points · Documents · Notes · Widgets (this desk's)

Three ways to fill a widget

Way Who Lives
MCP set_widget an agent in a desk's panel, after your yes on Your turn on that desk until cleared, or for as long as you said on the card; dims after stale_after (30 min), and says "panel 2 closed" when its panel closes
snyvi widget set scripts, git hooks, cron, CI until cleared; on a desk with --desk ID, global otherwise (or the panel's desk, run inside one)
a widget file you, or an agent through propose_widget (a card, Add) while it is switched on, on its desk (or the desks you shared it with), for as long as you chose; run while it is in view

What an agent may make one for (1.32): only when you asked, and only for a figure that changes by itself and that you'd otherwise go and check (CI 3/5 green, a deploy running, a dev server up), as a count and one short line. Not a to-do of yours (that's hand_over, on Your turn), not how to do something (the agent's reply), not news (a document), and not what changes only when you act.

A box is its name, its count and its body. Who writes it, and how often, is the name's tip.

All three send the same thing: Markdown, or one JSON object:

{ "body": "**deploy** 3/5 green", "tone": "ok", "count": "3/5", "lines": 2, "stale_after": 600 }
Field
body Markdown, 1,500 characters at most text, bold, code, links, lists. No HTML, pictures (their alt text instead), tables or diagrams. Empty clears the widget
tone ok, warn, bad, none colours the count
count 8 characters at most beside the name, kept while folded: 3, !2, 3/5
lines 1–6 (3) the body's room, 20 px a line. A longer body is cut and opens on a click; a new one never moves what's below
stale_after seconds (1800); 0 is never after it the widget dims

Names are lowercase letters, digits and dashes, 32 at most. A desk has six widgets at most and the left eight. A name a widget file has is that file's: a push to it is refused.

snyvi widget set backups --tone ok --count "03:00" "Nightly backup **done**"
snyvi widget set ci --desk 3 < ci-summary.json
snyvi widget clear backups

A widget file

~/.config/snyvi/widgets/
  git/            shared: on the desks you tick, or every desk
    widget.json
    run.sh
  disk-free/      global: on the left
  @7/             desk 7's own
    arena/          runs on desk 7 alone
      widget.json
      arena-status.sh

A widget file is one of three kinds:

Kind Who makes it Folder Runs
A desk's own an agent's card you Add, or snyvi widget new inside a panel widgets/@<desk id>/<name>/ on that desk alone, in its folder
Shared you (snyvi widget new --shared, or Share with other desks…) widgets/<name>/ on the desks you tick on /sidebars; none ticked is every desk
Global you (snyvi widget new --global) widgets/<name>/, "scope": "global" on the left, in its own folder

The desk is named by its id, so renaming a desk keeps its widgets. A name is shared or a desk's own, never both: two desks may each have their own status, but then no shared widget can be called status.

{
  "name": "git",
  "title": "Git",
  "scope": "desk",
  "run": { "command": "run.sh", "every": 30, "timeout": 5 },
  "lines": 2,
  "settings": {
    "base": { "type": "string", "label": "Compare against", "default": "main" }
  }
}
#!/bin/sh
# stdin: {"desk":{"id":3,"name":"snyvi","folder":"/home/you/snyvi"},"settings":{"base":"main"},"snyvi":"1.27.0"}
# cwd: the desk's folder (a desk widget), or this folder (a global one);
# this folder is $SNYVI_WIDGET_DIR, and first on PATH
base=$(sed -n 's/.*"base":"\([^"]*\)".*/\1/p')
ahead=$(git rev-list --count "${base:-main}"..HEAD 2>/dev/null || echo 0)
dirty=$(git status --porcelain | wc -l | tr -d ' ')
tone=ok; [ "$dirty" -gt 0 ] && tone=warn
printf '{"body":"**%s** · %s ahead of %s","tone":"%s","count":"%s"}\n' \
  "$(git branch --show-current)" "$ahead" "${base:-main}" "$tone" "$dirty"

How snyvi runs it

Removing an agent's widget

Turn off keeps a widget on /sidebars, ready to switch back on. An agent's widget you're done with can be removed instead: a desk's own widget file, or a box a panel pushed. Remove is last in its box's menu, and on its row on /sidebars.

flowchart LR
  A[an agent's box] -->|Remove| B["'Removed · Undo' where it was, 4 s"]
  B -->|Undo| A
  B -->|4 s pass| C["widgets/.removed/7-arena-…<br/>nothing runs"]
  C -->|moved back by hand| A

Allow, honestly

Nothing in a widget file runs until you Allow it. The seat asks ("Git wants to run run.sh every 30 s · Allow"), and so does /sidebars. Allow takes the whole folder as it is now; a change to any file in it asks again ("changed"), unless you switch Rerun my edits on for that widget, which is for one you are writing yourself.

Only the snyvi window can allow. The token an agent holds can't, and neither can a browser tab.

This is consent, not a sandbox. A widget runs as you, with your PATH, like anything in your shell. Agents in a desk's panels have a shell as you too, so they could write into the widgets folder. That is why every widget file needs Allow whoever wrote it, and why a changed one asks again.

An agent asks first

An agent never puts anything in your sidebars without a yes on Your turn.

propose_widget is checked first (a command that can't reach its script goes back to the agent with the fix), then written where proposals wait (widgets/.proposed/, where nothing runs), and a card on Your turn shows where it runs, how often, the command and the script. Try once runs it here, as it would run, and shows what it printed in the card. You pick how long it lasts (the agent may suggest one). Add moves it into this desk's own folder, widgets/@<desk id>/, and allows it as it is. Not now leaves it be. An agent can't propose a widget for every desk, or a global one: sharing is yours.

set_widget asks the same way, once per desk and name: the panel's first push is a card, "A box on this desk: deploy" with what it would show, Allow or Not now. Its updates wait behind the card; after Allow they go straight in. After Not now, a push of that name on that desk is refused, and the agent is told why. snyvi widget set run inside a panel asks the same; from a plain shell, a hook or cron it doesn't -- those are yours.

sequenceDiagram
  participant U as You (window)
  participant A as Agent in panel 2
  participant D as snyvi
  U->>A: make me a widget for the worker's error rate
  A->>D: propose_widget (widget.json, the script)
  D-->>U: Your turn card · errors · this desk · every 1 min · the script · Try once · Add or Not now
  U->>D: Add (the window)
  D->>D: moves it into widgets/@7/errors/, allows it as it is
  D-->>U: Errors appears on the desk's rail at the next run

From 1.31

1.32 made an agent's widget its desk's own. On the first start of 1.32, a widget file that was on exactly one desk moves into that desk's folder, allowed as it was; one on every desk, on several, or global stays where it is. A widget that can't move (its desk closed, the name taken there) stays shared, as it was, and the daemon's log says why.

flowchart LR
  A["widgets/arena/ · desks: [7]"] -->|first start of 1.32| B["widgets/@7/arena/ · desk 7's own"]
  C["widgets/git/ · desks: every"] --> C2[stays shared]
  B -->|Share with other desks| D["widgets/arena/ · desks: [7]"]
  D -->|Make it this desk's own| B
↑ ↓ move↵ openesc close