The Interface

There is no built-in UI compiled into thurbox. The binary boots a kernel, reads a directory of Lua files, and draws whatever it finds — so every pane, the session list included, is a plugin under ui/. You can edit one, replace one with a file of your own, turn one off, or delete it.

The interface is a directory #

Not a setting, and not a theme: the panes themselves are files. thurbox ships a small set and reads whatever is in the directory, so adding one is adding a file and removing one is deleting it. Your sessions, worktrees, tasks and automations are untouched by any of it — they live in the database, not the interface.

You can just ask the agent #

You do not have to learn Lua to change the interface. From any session, in whichever coding CLI you run, ask in English:

  • Add my project logs within a dedicated pane on the right in the thurbox UI.
  • Add a pane on the left with CPU and RAM usage. There is a top example plugin — install it and give it a slot in the layout.
  • Move the search strip to the bottom, and make the session column 30% wide.
  • The session list is too noisy — drop the repo group headers and show the branch instead of the agent name.
  • Install the info panel plugin.

That works because thurbox ships a built-in ui-skill extension : a thurbox-ui skill installed into each coding CLI's own skill directory, which loads only when the request is about the interface. It tells the agent that thurbox-cli plugin dir finds the directory and thurbox-cli plugin check is how it verifies its own work, and warns it off the mistakes that are invisible until runtime — building under a recursively watched directory, or writing inside a cloned plugin's own working copy. Press F10 and the change is on your screen.

Want the agent looking at the files directly instead? Point a session at the directory — it ships an AGENTS.md that any coding CLI loads as context without being asked:

bash
thurbox-cli plugin dir          # prints the directory
thurbox-cli session create --name ui --repo-path ~/.config/thurbox/ui

What a prompt like “add a CPU/RAM pane on the left” actually does #

Worth seeing once, because every interface change has this shape — a pane, and a slot for it.

1. The pane. thurbox-cli plugin install top delivers plugins/85_top.lua and records the entry in plugins.toml. It declares slot = "top" and the run capability.

2. The slot. ui/layout.lua decides where a slot goes, so putting it on the left is putting it first in the column list:

local columns = {}
if filled(ctx, "top") then
  columns[#columns + 1] = { slot = "top", pct = 20, min = 24 }
end
if panels.shown("sessions") and filled(ctx, "sessions") then
  columns[#columns + 1] = { slot = "sessions", pct = 25, min = 20 }
end
columns[#columns + 1] = { slot = "center" }

Then F10 — and trust it (settings → Interface → t), because it shells out to top and an untrusted plugin does not get run at all. Until you do, it draws the reason rather than a blank pane.

Two things follow from that split and are easy to miss. The plugin manager never writes layout.lua — placement stays yours, which is why step 2 is a separate edit. And a pane that loads but which no arrangement places draws nothing while looking perfectly healthy: thurbox-cli plugin check is what fails on that, and prints the line to add.

Every pane is a plugin #

The kernel owns no pane. What you see is Lua, in files you can open:

the interface directory
ui/
├── layout.lua              the arrangement: which slot goes where
├── lib/                    widgets, theme roles, fuzzy match, text input
└── plugins/
    ├── 10_sessions.lua     the session list
    ├── 20_agent.lua        the agent terminal
    ├── 65_search.lua       global search
    ├── 60_confirm.lua      the confirm float
    ├── 70_new_session.lua  the new-session flow
    └── 80_restore.lua      the restore list

Drop a file in and it is a pane. Delete a bundled one and it is gone for good — delivery records the removal and never writes it back, so a pane can be replaced by a differently named file of your own on equal terms. A file can also simply be turned off: present on disk, intact, and not loaded.

The one thing a plugin cannot do is surprise you. It gets no filesystem, no network, and no process — io, os, debug and the loaders are not in its world at all, and a lint enforces their absence. Reads are served from a snapshot and return instantly; writes are commands the kernel performs. So no plugin, including one nobody has written yet, can freeze the interface on a slow git fetch or an unreachable host.

The Interface tab (Ctrl+, then ]): every file, where it came from, and whether it is on screen. Here plugins/10_sessions.lua is turned off with space — the file stays on disk and the session list stops loading, so it declares no keys, is granted no capability, and its column is not reserved. The agent pane takes the space rather than a hole being left behind.

Running programs, and trust #

Two capabilities reach outside thurbox, and they are two separate decisions.

Reading a program's output. A plugin can run git status, docker compose ps or npm outdated in a session's working directory — and on that session's own host for a remote session. It is bounded: output is capped, a run times out, four run at once.

Running a program you interact with. A pane can hold a real terminal for a program it names — htop, a REPL, or a log you page through. Keystrokes reach it, it is resized to the pane's rect, and it keeps running while you work elsewhere. The pane belongs to the plugin rather than to a session, and survives reloading the interface.

The second is not covered by trusting the first, and that is deliberate: none of the bounds that make reading output safe apply to a program held open on your keystrokes. Trusting a pane to poll top every few seconds is a different decision, so it is asked separately — and the Interface tab says which of the two a file wants before you decide.

It is granted per plugin, and only after you trust that plugin: settings (Ctrl+,) → ] → select it → t. Trust is keyed to the file's absolute path with a digest recorded, so a trusted file that changes afterwards is shown as trusted · modified. Revoking takes effect on the next frame.

This is not a sandbox and does not pretend to be one. A program thurbox runs for you has your authority, and no gate here changes that. What trust buys is that nothing runs unasked. Treat a plugin you did not write the way you would treat a shell profile someone sent you.

Writing one #

Four commands, no terminal required — useful from a script or an agent as much as by hand:

Command What it answers
thurbox-cli plugin dir Which interface directory is live, and which rule chose it
thurbox-cli plugin new <name> Writes a starter that already loads
thurbox-cli plugin check Loads the interface the way thurbox does; non-zero on a failure — including on a pane that loaded but which no arrangement places
thurbox-cli plugin list The same inventory the settings Interface tab shows, with each file's origin
thurbox-cli plugin events Every event a plugin may subscribe to (events = { … } + on_event), with its payload
thurbox-cli plugin install <src> Fetches a pane and records it in plugins.toml
thurbox-cli plugin sync Brings the directory into agreement with plugins.toml
thurbox-cli plugin update / remove Advances a pin when you ask; removes a pane, its entry and its record

A pane does not have to be one you wrote. plugins.toml in your interface directory lists what it is composed of — a source, a destination file, an optional pin — and plugins.lock beside it records what each entry resolved to, so the same spec reproduces the same interface on another machine. A plugin that carries a program or a data file is itself a repositoryplugin install git+<url> clones it, payload and all, keeping its .git so updates are a fetch and your edits are protected by git rather than by us. The lock records the commit, so the same spec reproduces the same bytes. Installing one puts that repository's files on your disk, executables included; nothing runs without the capability you grant.

A bare name resolves to the repository's examples at this binary's release tag , exactly as an extension name does; URLs and filesystem paths work too. The rules delivery already follows carry over unchanged: a managed file you edited is preserved and reported, and one you deleted stays deleted.

Two deliberate limits. The manager never writes your arrangement — adding a pane is two edits, and plugin check fails loudly on the missing second one instead of guessing at it. And installing grants nothing: a pane that asks to run programs still draws how to trust it until you do. Trust for an installed pane is recorded against the source and version it was granted for, so an ordinary release asks you again rather than reading as tampering — while contents that change within one version still show up, because from outside a local edit and an upstream substitution are the same thing.

A plugin can react as well as draw. Declare events = { "session.status", … } and an on_event(name, payload) handler, and the kernel calls it once per change, off the render path — a session appearing, leaving or changing status, the selection moving, a command finishing, the interface reloading. The events are derived by diffing the snapshot, so a session made by thurbox-cli or a cron tick fires the same one the creation flow does; a subscription to a name nothing emits refuses to load, and thurbox-cli plugin events lists them all. Plugins reach each other with command("emit", …). And an action need not spend a chord to exist: commands = { { action, desc } } puts it in the Ctrl+P command palette beside every declared key, run through the same handler a key press takes.

A broken plugin does not cost you the interface: the error is reported in place, and the bundled copy stands in until the file loads again. The full author's guide, including the mistakes that are invisible until runtime, is docs/PLUGINS.md; the kernel's own shape and constraints are in docs/V2-KERNEL.md.

When a pane breaks the interface #

Editing the thing you are looking at means it can break, so the way back is chrome rather than a pane: Ctrl+, then ] is the settings Interface tab, and nothing in the directory can edit it away. A file that failed to load sorts to the top of that list with its error underneath.

The file The way back
a pane thurbox ships, edited or deleted r — the shipped copy comes out of the binary
a pane you wrote space — off, untouched on disk, and the interface loads without it
an installed pane (from <src>) thurbox-cli plugin sync
the whole directory it never loaded, so the embedded copies are running — fix the file from inside them

r cannot restore a file thurbox never shipped, and says so instead of guessing. space is also the key for bisecting which of several files is at fault. Both reload immediately. With no terminal to trust, thurbox-cli plugin check reports the same failure and exits non-zero. Full detail: docs/PLUGINS.md → When something goes wrong.

Panes you install #

Two surfaces thurbox does not ship in the binary are maintained as panes, each its own repository:

Pane What it gives you What it asks for
thurbox-code-review A diff reviewer — the branch, a single commit or the uncommitted changes, a changed-files tree, notes you send back to the agent. Takes the centre slot beside the agent and claims Ctrl+X / F7, with no change to your arrangement run, and only for the two targets the kernel does not compute; untrusted it still draws the branch diff
thurbox-info-panel An info panel — session, git, agent, usage and system readouts in a column beside the terminal. Claims F2; being a column, it wants one line in your layout.lua Nothing — every readout is already in the snapshot

Both carry more than a single Lua file, so both install by cloning:

bash
thurbox-cli plugin install git+https://github.com/Thurbeen/thurbox-code-review
thurbox-cli plugin install git+https://github.com/Thurbeen/thurbox-info-panel

Neither is bundled and neither is vendored into thurbox, which is the point: they are ordinary panes on the same terms as one you write, recorded in your plugins.toml and pinned by your plugins.lock.