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
topexample 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:
thurbox-cli plugin dir # prints the directory
thurbox-cli session create --name ui --repo-path ~/.config/thurbox/uiWhat 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:
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.
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
repository
—
plugin 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:
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.