Build Skribbl
KINDAreplaces $8.25/mosaves $99/yrback to the verdict
A desktop canvas of live terminals where the canvas is the easy part, and the difficult parts are keeping a dozen child processes alive across restarts, learning an agent's state from the agent rather than from its output, and metering spend accurately enough to act on.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the desktop application and its build.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why The pseudo-terminal bindings compile against your desktop runtime's version, and this is the first failure on every fresh machine.
Get it Your platform's compiler and build tools, then rebuild the bindings for the runtime version you ship.
Verify
A shell spawns in a node after a clean install - installfree
Why Session survival across crashes and restarts comes entirely from it, and without it every relaunch kills every agent.
Get it Install it from your package manager and confirm a session survives closing the terminal that started it. open ↗
Verify
A detached session is still there after its terminal closes - accountthe agent's own subscription or per-token billing
Why Status and usage come from the agent's own channels, and each agent exposes them differently or not at all, which decides what the canvas can honestly show.
Get it Install the agent, authenticate it, and read its hooks and status line documentation before writing phase three. open ↗
Verify
A hook fires and writes an event on a test prompt - decidefree
Why The four counters are priced differently and the rates change, so the table needs a checked-on date shown beside every total.
Get it Copy the current per-token rates for each model you run, including the cache read and cache write multipliers, and record the date.
Verify
The table has four rates per model and a date
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- nodes(id, title, cwd, agent_kind, x, y, w, h, mux_session, created_at) - links(id, from_node_id, to_node_id, direction, label) · recorded relationships, no message passing - sessions(node_id, mux_session, attached_at, alive, last_checked_at) · alive is observed, never assumed - status_events(id, node_id, source, kind, at, payload_json) · source is hook or none; kind is waiting_for_permission, waiting_for_input, working, finished or unknown - usage(id, node_id, at, input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, model, source) - price_table(model, input_minor_per_million, cache_write_minor_per_million, cache_read_minor_per_million, output_minor_per_million, checked_on) - A status event records its source, so the canvas renders unknown honestly where no hook has fired rather than inferring a state from when the terminal last printed something.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
MUX_BIN | required | tmux | The multiplexer binary. Sessions are named by node id. |
HOOK_SOCKET_PATH | required | ~/.local/share/canvas/hooks.sock | Where installed hooks write events. The canvas reads, never guesses. |
STATUS_FILE_DIR | required | ~/.local/share/canvas/status | Per-session usage written by the agent's status line command. |
PRICE_TABLE_PATH | required | ./config/prices.json | Four rates per model and a checked-on date, shown beside totals. |
LAYOUT_PATH | required | ~/.local/share/canvas/layout.json | Node positions and links, written atomically. |
SCROLLBACK_LINES | required | 5000 | Per node. Twelve unbounded buffers is how the canvas dies. |
MAX_NODES | required | 16 | Above this the canvas refuses with the reason rather than degrading quietly. |
INFER_STATUS | required | never | Present as a constant. A guess is worse than showing nothing. |
The build, in order
A real shell in a node
Each node is a real process that resizes, scrolls and dies cleanly.
Full screen programs draw for the size the pty reports, not the size you see.
Files
src/main/pty.tsAn invisible agent is still a billed agent.
Files
src/main/pty.tstests/no-orphans.test.tsTwelve unbounded buffers is how the canvas dies at four in the afternoon.
Files
src/renderer/terminal.tsx
done when · tick each as it passeswatch out- Forgetting to tell the pty about a resize means every full screen program draws for the wrong size and the agent's own interface wraps into garbage, and closing a node without killing its process group leaves agents running invisibly and spending money. Resize the pty on every node resize, and kill the group rather than the shell.
Sessions that outlive the window
A crash or a restart reattaches to running agents rather than killing them.
The session name is the node id, so a relaunch finds it without a lookup table.
Files
src/main/mux.tsA fresh shell in an old node's place is a lie with a cursor.
Files
src/main/mux.tstests/dead-session.test.tsA crash during a save should cost nothing.
Files
src/main/layout.ts
done when · tick each as it passeswatch out- Restoring the layout from a file and spawning fresh shells looks exactly like persistence until you notice the agent that was halfway through a refactor is gone, and you notice an hour later. Attach to the named session, and if it no longer exists say so on the node.
Status from the agent, or unknown
A node shows waiting, working, finished or unknown, and never a guess.
The agent knows when it is waiting. Nothing else does.
Files
src/main/hooks.tsconfig/hooks.jsonNot by which terminal printed last, which is wrong as soon as two are busy.
Files
src/main/status.tsUnknown is a true statement. Idle is a guess.
Files
src/renderer/status-badge.tsx
done when · tick each as it passeswatch out- Inferring 'blocked' from a terminal that has been quiet for thirty seconds flags every long test run as needing you and misses a permission prompt that printed once and is waiting, and the second is the expensive one. Read the hook events, map them by identifier, and render unknown where no hook has ever fired for a node.
Counting tokens without lying
Per node and per canvas spend is four counters from the agent's own accounting, priced from a dated table.
Its own accounting, written to a file the canvas tails.
Files
src/main/usage.tsconfig/statusline.shFour rates, one date, and the date shown beside the total.
Files
src/main/pricing.tsconfig/prices.jsonA missing model or a missing channel produces a blank, not a zero.
Files
src/renderer/meter.tsxtests/pricing.test.ts
done when · tick each as it passeswatch out- Summing the four counters into one number and pricing it at the input rate overstates spend when the agent is mostly reading its cache and understates it during long outputs, and either way the figure cannot be acted on before the bill arrives. Keep them apart, price each, and show the date of the price table beside the total.
Twelve terminals without falling overproduct builder
A dozen live agents run for a day without the canvas becoming the bottleneck.
A canvas crash must cost a window, never an agent.
Files
src/main/host.tstests/renderer-crash.test.tsWith twelve streaming. One node proves nothing.
Files
src/renderer/scheduler.tsSo a day's work can be read somewhere other than this canvas.
Files
src/main/export.ts
done when · tick each as it passeswatch out- Rendering twelve streaming terminals at full rate in one window saturates the interface thread, and the first symptom is keystrokes arriving late in the node you are typing in. Throttle rendering for nodes outside the viewport and measure input latency with all twelve streaming, rather than with one.
That is the whole plan for Skribbl. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Multiplayer and cloud sync.
- Code signing and notarisation.
- Message passing between agents.
- Status inferred from terminal output.
- hook-driven agent status, so a blocked agent looks the same as an idle one
- session survival across restarts and crashes
- an accurate token meter, cache reads price nothing like input tokens
- a signed and notarized Mac build, so Gatekeeper blocks what you ship to anyone else
- A per-node spend ceiling that pauses the agent through its own channel, once metering is trusted
- Git worktree creation per node, so parallel agents stop fighting over one checkout
- A queue node that hands the next task to whichever agent finished, once hooks are reliable
Need the files? The project pack on the verdict page hands your agent the whole brief · more dev tools.