--- name: luvus-module description: >- Write a luvus module (an extension for luvus, mission control for your AI coding agents). Use when the user is building or debugging a luvus module: authoring luvus-module.toml, adding a sidebar dock, Luvus Bar widget, right-click action, event hook, module pane, module settings, or calling luvus back over its UHP. --- # Writing a luvus module A **luvus module** is a plugin for luvus, mission control for your AI coding agents. There is **no SDK and no scripting engine**: a module is a directory with a `luvus-module.toml` manifest that declares **argv commands** (any executable: `sh`, `python`, `node`, a compiled binary). luvus runs those commands as subprocesses with `LUVUS_*` context in the environment, and the command calls luvus back through the same UHP the `luvus` CLI uses. So a module in bash/python/node can do what a built-in feature does. Reference material in this repo (read for depth): `MODULE-GUIDE.md`, `docs/13-modules.md`, and the worked modules in `examples/modules/` (`branch-dock` = a sidebar dock, `agent-ping` = an event hook, `scratch-pane` = a pane). ## The manifest: `luvus-module.toml` Top-level keys (all required unless noted): ```toml id = "yourname.my-module" # unique, reverse-dns style name = "My Module" version = "0.1.0" min_luvus_version = "0.8.3" # oldest luvus this needs description = "One line." # optional platforms = ["macos", "linux"] # optional; omit = all. Also per-item. ``` Then any of these tables, each declaring an argv `command` (a list, run as-is, cwd = the module dir): - **`[[docks]]`** `id`, `title`, `placement` (`sidebar.left` | `sidebar.right`) — reserve a sidebar dock. luvus renders it; you fill it with `ui.dock.push` (see below). - **`[[bars]]`** `id`, `title`, `region` (`top-right` | `bottom-right`), optional `priority` — reserve a bounded one-row Luvus Bar widget. Publish structured segments with `luvus bar push` (`ui.bar.push` on the UHP); live content is not persisted. - **`[[startup]]`** `command` — run once when the session is up and the socket is listening (and on enable). Dock rows and bar content are **not** persisted, so this is how you repaint them after a restart. - **`[[events]]`** `on`, `command` — run when a luvus event fires. Valid `on` values include `workspace.created`/`closed`, `tab.created`/`closed`/`moved`, `pane.created`/`closed`/`forked`/`moved`, `pane.agent_status_changed`, `agent.hook`, and the `task.*`/`lease.*` events (see `KNOWN_EVENTS` in `src/module/manifest.rs` for the full set — an unknown `on` is a hard manifest error). - **`[[actions]]`** `id`, `title`, `command`, optional `contexts` — a runnable action. With `contexts = ["pane"|"workspace"|"node"|"agent"|"tab"]` it also appears in that right-click menu, acting on **what was clicked**. Without `contexts` it is CLI-only (`luvus module run `). Dock rows also invoke an action on click. - **`[[panes]]`** `id`, `title`, `command`, `placement` (`split` | `overlay` | `tab`) — a real pane running your command (`luvus module pane open `). - **`[[settings]]`** `key`, `title`, `type` (`bool` | `string` | `number` | `enum`), plus `default`, `options` (enum), `min`/`max`/`step` (number), `secret` (mask + hide the value). Rendered in Settings → Modules; values reach every command as env (below). - **`[worktree_provider]`** one optional fixed creation `command`, optional fixed `remove_command`, and optional `platforms`. The user selects the module id in `config.json` at `worktree.provider`; config cannot replace either command. A worktree provider reads one versioned JSON request from stdin. Creation gets `version`, `operation`, `repository`, `branch`, and `branch_exists`, and must print only `{"path":"/absolute/worktree/path"}` on stdout. Removal gets `version`, `operation`, `repository`, `path`, `branch`, and `force`; it must print nothing, exit successfully, and unregister and remove the target. If a provider violates its exit or stdout contract after deletion already completed, Luvus reconciles the proven Git/filesystem state instead of retaining a stale workspace; stdout remains unsupported. Human logs go to stderr. This command is synchronous and must not call back into the Luvus CLI/API while the server waits. Luvus verifies the returned path is a registered worktree of the source repository on the exact requested branch before opening a workspace; explicit removal is verified before and after the command. Internal rollback and task merge remain Git-backed. ## What your command receives (no JSON parsing needed) luvus puts context in the environment, flat, so a bash module never parses JSON: - `LUVUS_MODULE_ID`, `LUVUS_MODULE_ROOT` (the module dir), `LUVUS_MODULE_VERSION` - `LUVUS_MODULE_TOKEN` — UI-publisher credential for this server; the Luvus CLI forwards it automatically, so do not persist or override it - `LUVUS_MODULE_CONFIG_DIR`, `LUVUS_MODULE_STATE_DIR` — writable per-module dirs - `LUVUS_WORKSPACE_ID`, `LUVUS_WORKSPACE_CWD`, `LUVUS_TAB_INDEX` - `LUVUS_PANE_ID`, `LUVUS_PANE_CWD`, `LUVUS_PANE_AGENT`, `LUVUS_PANE_STATUS` (the clicked/target pane) - `LUVUS_SETTING_` for each declared setting (uppercased key), plus the whole set as JSON - `[worktree_provider]` commands read their versioned operation request as JSON from stdin - Dock-row clicks add `LUVUS_MODULE_DOCK_ID`, `LUVUS_MODULE_ROW_ACTION`, `LUVUS_MODULE_ROW_VALUE`, `LUVUS_MODULE_ROW_TEXT`, `LUVUS_MODULE_ROW_INDEX` - Bar-segment clicks add `LUVUS_MODULE_BAR_ID`, `LUVUS_MODULE_BAR_SEGMENT`, and optional `LUVUS_MODULE_BAR_VALUE` - `LUVUS_MODULE_CONTEXT_JSON` — the full snapshot, if you want structured data - `LUVUS_SOCKET_PATH`, `LUVUS_BIN_PATH` — to call luvus back (below) ## What your command can do: call luvus back Run the `luvus` CLI from inside the command; it talks to the running server over `$LUVUS_SOCKET_PATH`. Use `"$LUVUS_BIN_PATH"` to guarantee the same binary as the session. Module-facing methods: - `luvus ui dock push --id --rows ` (or pipe the JSON on stdin) — fill your dock. Rows are `{text, dot?, tone?, spans?, action?, value?, menu?}`; a row's `action` invokes one of your `[[actions]]` on click, with the row's `value` in `LUVUS_MODULE_ROW_VALUE`. `tone` is a Luvus Bar tone name (`normal`, `muted`, `accent`, `success`, `warning`, `error`) for the text, and `spans` is `[{text, tone?}]` to color segments of one row with the same tone names (a span without a tone takes the row's). Both are optional and theme-bound, raw colors and ANSI are not accepted. Omitting `tone` gives the default row color, which is not the same as `normal`. - `luvus bar push --id --content ` — atomically publish structured `text`, `symbol`, `state`, `badge`, `progress`, `spacer`, and `separator` segments. Add `--compact-content`; an `action` must name one of your `[[actions]]`. - `luvus ui notification push --text --level info|success|warning|error` — publish bounded transient status; use `--dedupe-key` for replacement. - `luvus ui toast ""` — flash a one-line confirmation. - `luvus ui agent-title push --titles ` — publish AGENTS sidebar titles for live panes (`pane`) and resumable sessions (built-in `agent` + `session_id`). OSC still wins; never send a Luvus `=alias` as the title. `luvus ui agent-title clear` drops one key or every title owned by your module. The CLI authenticates ownership automatically. - `luvus ui sidebar` / `ui dock list` / `ui dock move` — sidebar/dock control. - `luvus tab rename ` / `tab list` — tabs. - `luvus module settings ` / `module settings ` — read your settings exactly (values are masked in `settings list` when `secret`, but exact in `settings get`). - Plus the whole CLI: `luvus pane split/run/send`, `luvus agent ...`, `luvus git ...`, etc. ## Recipes **A sidebar dock** (like `examples/modules/branch-dock`): declare `[[docks]]`, a `[[startup]]` that runs a script which builds a JSON rows array and calls `ui dock push`, and `[[events]]` so it refreshes on `workspace.created`/`tab.created`. Give rows an `action` matching a `[[actions]]` id to make them clickable. **A Luvus Bar widget** (like `examples/modules/ci-bar`): declare `[[bars]]`, publish full and compact structured content from a one-shot startup/event action, and keep expensive work outside the render path. Use a pane for arbitrary terminal UI and a dock for multi-row content. **An event hook** (like `agent-ping`): a `[[events]]` with `on = "pane.agent_status_changed"` and a command that reads `LUVUS_PANE_*` and reacts (notify, log, `ui toast`). **A right-click action**: an `[[actions]]` with `contexts = ["pane"]` (or workspace/node/agent/tab). It appears in that menu and runs against the clicked target's `LUVUS_*` env. **A pane**: a `[[panes]]` entry; open it with `luvus module pane open `. ## Test loop ``` luvus module link # register a local module (dev) luvus module list # confirm it's runnable luvus module log [] # tail command output/errors while iterating luvus module run # invoke an action directly luvus module enable | disable ``` Iterate: edit the script, re-run the action or trigger the event, watch `module log`. The manifest is validated on link; a broken manifest keeps the entry visible but not runnable, with the reason in `module info `. ## Conventions - One module, one job. Keep commands fast and quiet — luvus caps a command's output (64 KiB) and runs at most 32 at a time. - Identity env (`LUVUS_MODULE_ID`, `LUVUS_MODULE_TOKEN`, socket path) is injected and cannot be overridden. Never log or persist the token. - Use `platforms` on the manifest or any item to skip a build step / hook / pane / action where it does not apply. - To share it: `luvus module install /` clones + builds + registers from GitHub; tag the repo with the `luvus-module` topic so `luvus module search` finds it.