plugins/
Lifecycle code. Every .ts file in here loads on launch — no import, no export, no
wrapper.
// plugins/worklog.ts
const sent: string[] = []
tui.hook("message:sent", ({ content, instance }) => {
sent.push(`${instance.name}: ${content}`)
})
tui.hook("tui:shutdown", async () => {
if (sent.length) await Bun.write(`${process.env.HOME}/worklog.md`, sent.join("\n"))
})
Files load alphabetically, after main.ts. Each is independent: one that throws disables
itself and nothing else.
A convention, not a capability
tui.hook() works from main.ts too, and a plugin can register commands and keys. The
folder exists so lifecycle code has somewhere obvious to live, and so it loads without
you importing it.
Just don't do both:
// main.ts
import "./plugins/worklog" // ✗ auto-loaded AND imported — registers twice
One file per concern
plugins/
├── worklog.ts # message:sent → disk
├── resume.ts # tui:shutdown / tui:boot → last agent
└── notify.ts # message:received → desktop notification
Nothing enforces this, but a throw contains itself to one file — so splitting means a broken plugin costs you one behaviour instead of all of them.
See tui.hook() for every event and what each carries.