| name | tbm-plugin |
| description | Author a Tabame launcher plugin — an external Python/Node/Bun script that extends the app launcher, talking newline-delimited JSON over stdin/stdout. Use when the user wants to build, scaffold, debug, or install a Tabame launcher plugin, add a new launcher keyword backed by a script, or asks about the plugin render-frame protocol. |
Tabame Launcher Plugin Authoring
Tabame's launcher is extensible with plugins: standalone scripts (Python, Node.js, or Bun) that Tabame runs as a long-lived child process when the user types the plugin's keyword. Communication is newline-delimited JSON over stdin/stdout — no SDK, no dependencies.
- Tabame → script (stdin): UI events (
init, query, select, action, close).
- Script → Tabame (stdout): render frames — JSON objects that fully describe what the launcher shows right now.
The script is the source of truth for the UI: every time you want the launcher to display something different, you print a new render frame.
Authoritative references (read these)
The complete, authoritative protocol spec lives in the repo. Read it before writing non-trivial plugins — do not invent fields or message types not documented there:
- Full spec: skills/TABAME_PLUGIN_SKILL.md — every message type, render-frame field, item field, view type, icon name, and pattern.
- Working examples to copy from:
plugins/echo/ — Python demo exercising list/grid/detail/preview, actions (shortcuts/confirm), forms v2, streaming chat, pagination, storage, and background notify.
plugins/linear/ — Node.js plugin with an HTTP API and config.json secrets.
plugins/caniuse/ — Node.js plugin with fetched data and detail views.
When a detail isn't covered below, consult the full spec rather than guessing.
Workflow for building a plugin
- Clarify the shape (only if unspecified): keyword, runtime, data source, what each item shows, what Enter does, what Ctrl+K actions to offer, and which view (list / grid / detail / preview pane).
- Pick a runtime the user has on
PATH: python, node (18+), or bun. Tabame bundles none.
- Scaffold the folder with
plugin.json + entry script. Start from the matching template in §15 of the full spec, or copy the closest plugins/ plugin.
- Implement the event loop (see contract below).
- Install & test: drop the folder in
%localappdata%\Tabame\plugins\<id>\, reopen the launcher (it rescans on every open — no restart), and type the keyword.
plugin.json (manifest)
{
"keyword": "weather",
"runtime": "python",
"entry": "main.py",
"id": "weather",
"name": "Weather",
"description": "",
"icon": "cloud",
"args": [],
"enabled": true,
"dev":
Launch is effectively <runtime> <args...> <entry>, started without a shell, with the working directory set to the plugin folder (so config.json/assets resolve relatively). Activation: the query equals the keyword or starts with keyword + " "; the keyword prefix is stripped before your script sees the text.
The protocol contract (essentials)
One JSON object per line, both directions. Flush stdout after every frame.
Messages you receive on stdin:
| Message | When | Key fields |
|---|
init | Once at startup | query, protocol (3), theme ({accent,text,background,dark} hex + flag), locale |
query | Every keystroke (not in inputMode:"submit") | text, rev (generation counter) |
submitQuery | Enter on an inputMode:"submit" frame — the whole line at once (chat) | text, rev |
select | Highlighted item changed | id, rev |
action | Enter (action:"default"), a Ctrl+K pick, an action shortcut, or the empty-state CTA | id ("" for frame-level/empty-state), action (no rev) |
submit | User submits a form view | values ({fieldId: value}), button (pressed form.buttons id) |
change | A watch: true form field changed | id, values |
loadMore | Scrolled near the end of a hasMore frame | rev — answer with a longer full list |
storage / clipboard | Replies to storage get/keys and clipboardRead commands | requestId (echoed), value/keys/text |
back |
Minimal render frame you print to stdout:
{
"type": "render",
"rev": 1,
"view": "list",
"items": [
{ "id": "1", "title": "Hello", "subtitle": "world", "icon": "star" }
]
}
Full frame/item fields are documented in spec §6–§9: views list/grid/detail/form; frame loading (bool or {progress}), emptyText, empty ({icon,title,hint,action} — action is a clickable CTA), placeholder, canGoBack, grid, detail ({markdown, metadata, wide, append} — a markdown-only document view; append streams chunks onto the current document, wide: true widens the window; text is selectable, code blocks get a copy button, images open in a lightbox), form, preview, frame-level actions (Ctrl+K entries independent of the highlighted item), selectId (move the highlight), hasMore (pagination via loadMore), inputMode: "submit" (Enter delivers the whole query — chat-style); item accessories (tintable via color, own icon), actions, preview ({markdown, metadata}), section headers (lists and grids), lines (subtitle wrap), progress bar, grid tileColor, and **bold**/`code` spans in title/subtitle. Actions (item- or frame-level) support shortcut ("ctrl+shift+c" — must include ctrl/alt), destructive (danger tint), and confirm (host-shown are-you-sure gate). Forms (spec §8) support text/password/textarea/dropdown/checkbox/number(+min/max)/date/filepicker/folderpicker/tags fields with required, description, plugin-set , (change events), and multi- submits. entries (spec §7.1) are aligned key-value rows supporting dots, , clickable , arrays, and dividers. Icons accept a §11 name, a color swatch, or a raster URL.
You can also print commands — side effects the host executes for you (spec §5.2). No rev, fire-and-forget; combine by printing several lines (e.g. copy then hide):
{"type":"command","command":"copy","text":"..."}
{"type":"command","command":"paste","text":"..."}
{"type":"command","command":"open","url":"..."}
{"type":"command","command":"hide"}
{"type":"command","command":
...
Non-negotiable rules (get these right)
- stdout is protocol messages only (render frames and commands). Any other stdout line is treated as a diagnostic log. Send all debug output to stderr.
- Echo
rev on frames responding to a query/select; use rev: 0 for unsolicited pushes (action results, async/background refreshes). Tabame drops frames with a stale rev, so a slow response to "rom" can't overwrite fresh results for "rome".
- Flush stdout after every frame; one JSON object per line, no embedded newlines.
- Every item needs a stable, unique
id — it's echoed back in select/action.
- Exit on
close and on stdin EOF. After close, ~2s grace then the process is killed.
- Read stdin line by line; never block for all input.
- Escape exits the whole plugin unless the frame set
"canGoBack": true — then you get {"type":"back"} and should render the previous screen. Keep your own screen state; leave canGoBack off the root screen (and never set it on a screen you can't leave, or Escape is trapped).
- Slow work: emit a
loading:true frame (echoing the rev) first, then the result frame. Streamed/long work belongs on a worker thread (detail.append frames use rev: 0).
loadMore answers carry the full list (all pages so far), not just the new page.
- Secrets go through the
storage command with "secret": true (Windows Credential Manager) — not a shipped config.json.
- Never crash on bad input — wrap handlers in try/except and render the error as a
detail frame.
- Icons must be a name from spec §11 (case-insensitive), a
#RRGGBB color swatch, or a file:///https:// raster (PNG/JPG, no SVG).
- On Windows, Tabame sets
PYTHONIOENCODING=utf-8 / for Python; Node/Bun are UTF-8 already.
Doing real work
The plugin is an ordinary process: HTTP (requests/urllib, or global fetch in Node 18+/Bun), filesystem, spawning tools. Read secrets from a config.json in the plugin folder (the CWD) or env vars — see plugins/linear/config.example.json. To open a URL, copy/paste text, hide the launcher, or show a toast, print a command (see above) — don't shell out to cmd /c start/clip. Markdown links in detail/preview are clickable and open in the default browser; metadata entries can carry a url for the same effect.
Deliverables
When done, hand the user both plugin.json and the script, the exact install path (%localappdata%\Tabame\plugins\<id>\), and the reminder to reopen the launcher to load it.