--- name: blockodile-modules description: Author a blockodile module in ~/.config/blockodile/modules/ and diagnose a broken blockodile + herdr setup. Use when a module does not appear or render, when a pane shows no agent status, when the terminal block looks wrong, or when a setup check in Settings > General fails. --- # blockodile modules You are helping someone **use** blockodile, not build it. Everything below happens in the user's home directory. (Editing blockodile's own source is a different job — that repo carries `.cursor/skills/blockodile-commands/SKILL.md`.) blockodile is a desktop shell for [herdr](https://herdr.dev): a sidebar of workspaces and panes on the left, a bento grid of blocks per pane in the middle. herdr stays the runtime — sessions, panes, agent detection — and blockodile draws it. Two paths belong to the user: `~/.config/blockodile/modules/` (custom blocks) and `~/.config/blockodile/settings.json`. Working on another machine over SSH is a separate skill, **blockodile-connections** (`~/.config/blockodile/connections.json`). Hand anything about a VPS, a second computer or a failing SSH connection to that one. ## Module folder A module is one folder. The folder name **is** the id; a `module.json` whose `id` disagrees with its folder loads nothing. ``` ~/.config/blockodile/modules/ btop/ module.json # kind: command hello/ module.json # kind: web index.html ``` Install = copy the folder there. Uninstall = delete it. There is no package manager. ## Manifest `module.json`, read by blockodile at startup and on every file change: | Field | Required | Default | Notes | |---|---|---|---| | `id` | yes | — | Must equal the folder name | | `name` | yes | — | Shown in the command palette as `Add ` | | `kind` | yes | — | `"command"` or `"web"` | | `command` | for `command` | `""` | Shell line | | `entry` | for `web` | `"index.html"` | HTML file inside the folder | | `w` | no | `4` | Grid width, 1–12 | | `h` | no | `3` | Grid height, 1–6 | ```json { "id": "btop", "name": "System monitor", "kind": "command", "command": "btop", "w": 6, "h": 4 } { "id": "hello", "name": "Hello", "kind": "web", "entry": "index.html", "w": 4, "h": 3 } ``` A manifest missing `id` or `name`, or with any other `kind`, is **dropped silently** — no error, no log, the block simply never appears in the palette. When a module does not show up, suspect the manifest first. ## Kind command The command runs through the login shell (`$SHELL -lc ""`) in the focused pane's folder. Leave `command` empty (or omit it) for a plain interactive shell instead. Each block owns its process. Closing the block kills the whole process group, so a dev server started inside it dies with it rather than leaking as an orphan. ## Kind web One HTML file, rendered in an iframe with `sandbox="allow-scripts"` and **no** `allow-same-origin`, loaded through `srcDoc`. The origin is opaque, which decides everything else: - **No relative assets.** `` never loads. Inline SVG, or a `data:` URL. - Inline all CSS and JS into the one file. Third-party libraries too. - Network `fetch()` works. `localStorage` does not survive meaningfully — keep state on disk via `fs.write`, or in the host. - The only channel to blockodile is the injected `window.blockodile` object below. - Use `blockodile.theme` for colours so the block matches the app. ## The blockodile API `window.blockodile` is injected before your code runs; no import, no bundling, no network. ```js const { cwd, home } = await blockodile.call("ctx"); const items = await blockodile.call("fs.list", { path: cwd }); const { code, stdout, stderr } = await blockodile.call("shell.run", { command: "git status -s" }); blockodile.onContext(({ cwd, paneId, agentStatus }) => render(cwd)); blockodile.onCommand((id, extra) => { /* palette / hotkey invocation for this block */ }); blockodile.run("terminal.focus"); ``` | Method | Params | Returns | |---|---|---| | `ctx` | — | `{ cwd, home }` | | `fs.list` | `path` | `{ path, items: [{ name, dir, size }] }` — folders first, then case-insensitive | | `fs.read` | `path` | file contents as a string | | `fs.write` | `path`, `content` | `null` | | `shell.run` | `command` | `{ code, stdout, stderr }` | Anything else is an error. Relative paths resolve against the pane's `cwd`; absolute paths are used as given. There is no sandbox on those paths — a module already runs arbitrary shell commands, so it has the user's full access. Treat that as the trust model and do not be clever with it. Two shape traps: `call("ctx")` resolves to `{ cwd, home, paneId, agentStatus }`, but `onContext` fires with only `{ cwd, paneId, agentStatus }` — **no `home`**. And `shell.run` blocks until the process exits, so anything long-running belongs in a `command` module, not a `shell.run`. `window.crocofile` is a back-compat alias from the app's old name. Do not use it in new modules. ## Keys and commands A focused iframe swallows its own keydowns, so the module forwards **only** chords with `alt` + (`ctrl` or `meta`) to the host — blockodile's block-tiling family. Everything else stays yours: an editor keeps Ctrl+S, the terminal keeps mod+V. Every user-facing action goes through `blockodile.run(id, extra)` rather than a private path, and `blockodile.onCommand(fn)` receives palette and hotkey invocations aimed at this block (`extra.blockId` targets one specific block). ## Traps | Symptom | Cause | Fix | |---|---|---| | Block never appears under `Add …` | Invalid manifest, dropped silently | Check `id` = folder name, `name` present, `kind` is `command` or `web` | | Syntax colours or regex-looking text mangled after inlining a library | `String.replace(tag, bundle)` interprets `$&`, `` $` `` and `$1` inside the bundle | Concatenate the strings, or pass a replacer function. Inline the library's CSS too | | Works on Linux, breaks on macOS (or the reverse) | The module branched on the OS | Chain the two commands, see below. Never read `navigator.platform` | | An image or file 404s | Relative asset under an opaque origin | Inline it as a `data:` URL | A module cannot ask which OS it is on, and must not guess from `navigator.platform`. Run the Linux command first and let the macOS one take over when it is missing — a missing binary exits 127, so the second branch runs: ```sh xdg-open "$1" || open "$1" wl-copy < "$1" || pbcopy < "$1" ``` ## Publish Sharing privately is a zip of the folder — the receiver unzips it into `~/.config/blockodile/modules/`. For the public library at `blockodile.dev/modules`, open a PR on `AlekDob/blockodile-modules` adding `modules//` in exactly the layout the app already installs. CI validates it. The gallery card needs a few extra manifest fields the app itself ignores: | Field | Required | Notes | |---|---|---| | `description` | yes | One line | | `author` | yes | | | `details` | no | Array of strings, the longer pitch | | `glyph` | no | 5×5 array of `#`/`.` strings | | `requires` | no | Binaries the module shells out to | | `tags`, `version` | no | | ## Setup checks blockodile checks its runtime at startup; the list is Settings > General, and every failing row names the command that fixes it. Run that command — do not hand-fix around it. | Check | Fails when | Fix command | |---|---|---| | `herdr.installed` | herdr is not on PATH | `Install herdr` | | `herdr.version` | herdr is older than 0.9.0 | `Install herdr` | | `herdr.server` | The running server is a different build from the installed binary | `Restart herdr server` — closes every pane and its agents | | `herdr.config` | herdr's `[ui]` chrome keys are missing | `Apply herdr display settings` | | `deps` (Linux) | No zenity/kdialog, xdg-utils or wl-clipboard | Install the named package; there is no in-app fix | Re-run them all with `Re-run setup checks` from the palette (`Cmd/Ctrl+K`). blockodile runs its **own** herdr server, on its own socket, and starts it itself — a standalone `herdr` in a terminal attaches to the user's session on herdr's default socket, which blockodile never reads, so running one is not a fix for anything here. A `herdr.server` row saying "Not running" is therefore either the moment before the first poll or a start that failed, and `~/.config/blockodile/herdr/server.log` is the only place that says why. ## A pane shows no agent status Start by knowing what "no dot" means: blockodile draws an indicator **only** for `working`, `blocked` and `done`. `idle` and `unknown` both render nothing, so a missing dot is ambiguous between "the agent is idle" and "herdr cannot tell". Disambiguate with herdr, never by guessing: ``` $ herdr agent explain # pane ids come from `herdr api snapshot` ``` A working detection names the rule that fired and the text it matched: ``` agent: claude state: working rule: osc_title_working (region=osc_title priority=1100) evidence: "◑ Blockodile status di pi" ``` A missing one falls back, and says so: ``` agent: pi state: idle rule: none fallback_reason: default_known_agent_idle_fallback ``` `rule: none` + `fallback_reason` means herdr recognised the agent but has no way to read its state. Install the agent-state integration: ``` $ herdr integration status # every supported agent and its hook path $ herdr integration install pi # e.g. ~/.pi/agent/extensions/herdr-agent-state.ts ``` Then **restart the agent in that pane.** The extension loads at agent startup, so panes started before the install keep reporting `rule: none` and look like the install failed. Claude Code needs no integration — herdr reads its state from the terminal title (`osc_title_*` rules). pi, codex, opencode and most others do need one. That asymmetry is why "status works for Claude but not for my other agent" is the usual shape of this report. ## Display and terminal problems | Symptom | Cause | Fix | |---|---|---| | herdr's own tab bar, sidebar, borders or scrollbars inside the terminal block | herdr's chrome keys are unset | `Apply herdr display settings` | | A header with workspace / tab / agent counts, on Linux but not macOS | The block is ~43 columns, under herdr's default `mobile_width_threshold` of 64, so herdr draws its mobile layout | Same command — it pins the threshold to 20 | | Edited `~/.config/herdr/config.toml`, nothing changed | herdr reads config on the **server**, not per client | `herdr server reload-config` | | `nested herdr is disabled by default` | blockodile was launched from inside a herdr pane | `HERDR_ENV` is the sentinel; launch it from a plain terminal or the app launcher | | Every pane squeezed side by side | herdr cleared the tab's zoom on a split, close or agent exit | Expected; blockodile re-zooms on the next poll. Wait a tick | | Stale frame right after switching pane | Known repaint lag | Expected; it clears itself. Resize the window if it persists | ## Defer to herdr This skill owns the blockodile side of the bridge: the module format, the settings blockodile needs from herdr, and what this UI's indicators mean. herdr owns the rest. | Task | Use | |---|---| | Control panes, tabs, workspaces, agents | `herdr --skill` (skip if a herdr skill is already in context) | | Debug herdr itself | https://herdr.dev/llms.txt | | Set herdr up for the first time | https://herdr.dev/agent-guide.md |