Obvious: Unskippable, inevitable, bold, visible dashboard even when you are far, very far from the screen. Good when you are vibing while agents are coding.
Grid: monitor all active sessions on the same screen.
Open the page in any browser and get a full-screen, color-coded status of your agents plus per-session live metrics: tokens, cost, cache rates, request-speed graphs, git branch, and one-click deep links into web sessions. Notifications (ntfy push + alarm sound) are built in.
Zero infrastructure. No separate server, no database, no WebSocket backend — the plugin itself serves the page on http://localhost:8765 (bound to 0.0.0.0, so it's reachable from any device on the network). Install, restart opencode, open the URL.
Runs where opencode runs: WSL, native Linux, macOS, and native Windows. Originally built for opencode inside WSL; since v2.2.0 state lives in the platform temp dir and v2.3.0 completed the cross-platform seams (web-port discovery fallback, per-platform alarm sound, WSL-guarded path translation).
Install from npm (global config) with opencode's own command:
opencode plugin opencode-obvious-grid -g
(-g installs into your global config ~/.config/opencode/opencode.json; omit it for a project-level install, -f replaces an existing version.)
Alternatively, add it to opencode.json (global or project-level) manually:
{
"plugin": ["opencode-obvious-grid"]
}opencode installs npm plugins automatically with Bun at startup (cached in ~/.cache/opencode/node_modules/). Restart opencode, and the status page is live at http://localhost:8765 (bound to 0.0.0.0, so it's reachable from any device on your LAN). To use a different port, set OBVIOUS_GRID_PORT (e.g. OBVIOUS_GRID_PORT=9000 opencode) — every instance should use the same port so they share one page.
Requirements: opencode ≥ the June 2026 plugin refactor (V2 plugin format) — older versions fail to load the plugin.
One install covers every instance: the page aggregates all opencode instances on the machine (CLI and opencode web), so a single install and one open URL is all it takes.
- Aggregate status page — one glance tells you if any agent is working (orange), waiting for your answer (blue), errored (red), or everything is idle (green).
- Per-session cards — state, model, per-request and total tokens, context %, cache hit rates, cost, finish reasons, token & speed graphs, session title, working directory + git branch, run/wait/idle time breakdown.
- Sub-agent visibility — sub-agent sessions (spawned via the task tool) get their own
↳-marked cards, and their work is rolled into the parent session's card (totals + a▸per-agent breakdown chip). - All instances, all modes — CLI and
opencode websessions on the same machine, web sessions deep-linkable into the exact opencode web UI session. - Browser-based alerts — Web Audio beeps when a run finishes or blocks on you (per-card toggle), plus ntfy.sh push + alarm sound (global toggle).
- Self-contained page — a single HTML file, no external assets, no backend beyond the plugin itself.
Grid
Four opencode instances on one page — the grid scales from a single full-page card up to N×N, keeping every agent's status readable at a glance even from across the room:
Parallel sub-agents
Many cards at once when several sub-agents run in parallel — each sub-agent gets its own ↳-marked card while the parent card rolls their work up into its totals:
| Color | Meaning |
|---|---|
| Orange | At least one opencode instance is running (agent working / streaming) — card shows a scrolling "RUNNING" marquee |
| Blue | No instance running, but at least one is waiting for user input (a permission request or a question tool prompt) — card pulses/blinks |
| Red | No instance running/waiting, but at least one hit a session.error |
| Green | All instances idle (default / waiting for input) |
Aggregation rule: error > waiting > running > idle. If any card is waiting for your approval, the page is blue regardless of others. Each session is shown as a card with its live metrics (see below). Cards are per-session: CLI mode has one session per opencode process, web mode can have several.
Card layout: 1 card = full page; 2 = side by side. For more, both the cards per line and the number of lines are ceil(sqrt(x)) (flex-wrap): 3 → 2×2, 4 → 2×2, 5 → 3×2, 8 → 3×3, 9 → 3×3, 16 → 4×4. The last line is shared evenly among its cards, so the last line's cards are wider than the others (e.g. 8 cards → rows of 3, 3, 2 with the last two at 1.5× width; 5 → 3 + 2 at 1.5× width; 7 → 3 + 3 + 1 full-width). All lines get equal height.
Each card shows, from top to bottom:
- Watermark / marquee: when running, a giant low-opacity
RUNNINGmarquee scrolls across the card background (rows alternate left/right, speed adapts to token rate: 90s per half-cycle at <100 t/s, 60s at 100–200, 30s at >200). When idle/waiting, a dim watermark shows the state word (IDLE,WAITING,ERROR). - Session title (
sess), falling back tosession <first-8-of-session-id>. Sub-agent (child) cards are prefixed with↳and show aparent: <parent session title>line underneath (hover for details). For sessions from the web UI it's a link (dotted underline) that opens that exact session in the web UI (http://localhost:<web-port>/<base64url(cwd)>/session/<id>), in a new tab. - Line 1 chips:
- Calls:
<userCalls>u | <apiCalls>a(assistant messages this session; backfilled from message history at startup). - Tokens (per request):
<input> in(+(<contextPct>%)when the context limit is known, color-coded red >50%, orange >20%)| <output> out| <reasoning> reasoning.out/reasoningcount only the current request — they reset when a new user message starts (the request boundary is tracked by message id, so re-delivered events can't corrupt it). Theinfigure is the latest context size, not a sum. - Tokens (total):
total <in>|<out>|<reasoning>— session-wide sums of input/output/reasoning tokens, abbreviated with integer rounding (k,M,G). - Cache:
cache hit <cacheHit>% | avg <cacheAvg>%, color-coded by rate (green >90, blue >80, white >70, orange >60, red below). - Cost:
$<total> | $<lastCost>(last message's cost to 4 decimals). - Finishes:
N too long | N error | N abortedin orange — only when non-zero.
- Calls:
- Line 2 chips:
WEB/CLI(where the session was opened from: opencode web UI vs terminal; purple/gray, fromprocess.argvat plugin load) ·mode· agent (hidden whenagent === mode, e.g. "build") ·SUB(amber, only on sub-agent cards) ·provider/model | variant·<last> tokens/s | avg <avg>/s(rates count output + reasoning tokens — reasoning is also generated). - Sub-agent breakdown chip (parent cards, after line 2):
▸ explore 3a $0.03 · +2 more— the direct sub-agents' calls and cost, active (running/waiting) ones first, up to 3 shown. Finished sub-agents are dimmed at 50% opacity; hovering shows the full per-agent breakdown (calls · cost · tokens, with adone/runningmarker per agent). Entries live as long as the child's card exists (60-min staleness window, session delete, or process exit). - Graphs (between line 2 and the toggles, only when the card has ≥80px of free space, i.e. few cards; cards ≥900px wide show them side by side, narrower cards stack them).
- Token graph (left): one stacked bar per request, split at the vertical midline — upward: input cache-miss (white) + output (orange) + reasoning (purple), downward: prompt-cache hits (blue). Both halves share the same scale (max of up/down totals). When the graph is wide enough (≥420px), four thin cumulative lines are overlaid — one per token type in the same colors as the bars — scaled to a second Y axis on the right (labels: max / half / 0). Hovering shows
req N · time · in · out · reas · cacheplus the cumulative totals so far for that request. - Speed graph (right): a two-line chart over request time. The dim line is each request's speed (output+reasoning tokens ÷ duration), the bright line is the running average (cumulative tokens ÷ cumulative time — same definition as the
avgrate chip, so it converges to it). Y scale auto-fits to the data (95th percentile ×1.2, spikes clipped at the top edge), X is categorical — each request gets an equal-width slot in chronological order. Hovering shows the exact values of the nearest request (number, time, speed, running average). - Both are drawn from per-request series the plugin persists (
tokSeries: time + in/out/reas/cache tokens;rateSeries: time, tokens, duration) — no static legend or hint.
- Token graph (left): one stacked bar per request, split at the vertical midline — upward: input cache-miss (white) + output (orange) + reasoning (purple), downward: prompt-cache hits (blue). Both halves share the same scale (max of up/down totals). When the graph is wide enough (≥420px), four thin cumulative lines are overlaid — one per token type in the same colors as the bars — scaled to a second Y axis on the right (labels: max / half / 0). Hovering shows
- Sound + notify toggles: per-card checkboxes. Sound enables the Web Audio beeps (see below); notify enables push notifications for this session only — the ntfy topic/URL is global and set in the bottom-right field. Both save automatically. The first time you enable notify, a popup explains setup (with a persisted "don't show again" option).
- Close button (×): appears in the card corner when a session is idle/errored/unknown. Clicking it hides the card (stored in
localStorageassp-hidden). The card stays hidden while the session is idle or errored and reappears automatically as soon as it becomes active again (running/waiting). AN hidden — restorepill in the top-left shows how many cards are hidden and restores them all. Hidden cards don't count toward the grid layout. - Path line: the working directory of the session, in the same style as the meta line (monospace, dim), at the top of each card, sharing a header div with the session title so there's no gap between them. When the directory is a git repo, the current branch is appended after a
·, refreshed everyBRANCH_REFRESH_MS(15s) and on directory changes via a boundedgit branch --show-currentcall (so branch switches mid-session show up within ~15s; detached HEAD clears the branch). The card's top padding is removed so the header takes the space the padding used to occupy — nothing else on the card is pushed down. - Meta line (small, monospace, dim):
pid <pid> · v<version> · run <dur> · open <dur> (run <dur> · wait <dur> · idle <dur>) · started <HH:MM>.- The leading
run <dur>(only while the session is running) is how long the current run has been going, reset to 0 each time a new run starts (a run = a running segment between waiting/idle breaks). - The leading
wait <dur>(only while the session is waiting) is how long the current wait has been going, reset to 0 each time a new wait starts. - The leading
idle <dur>(only when the session is idle) is time since the last message finished — anchored to the completion time of the last assistant response (or the creation of the last user message), so it counts from the true end of the last run, not its start. openis how long the session has been open, and is the sum of the breakdown in parentheses:run(model generating),wait(awaiting your answer or a permission), andidle(no activity). Only parts with ≥1s are listed, so transient sub-second state flips never show as0s. The plugin accumulates per-state time on every state transition and persists it per session ID in~/.config/opencode/obvious-grid-session-times.json, so the totals accumulate across restarts of the same session — but time while the instance was closed still never counts (the in-memory clock restarts on each run, so overnight gaps are never accrued). Resuming yesterday's session shows today's work added on top of the historical totals, withstartedshowing the session's original creation time (date-prefixed when not today).
- The leading
- Hover hints: every chip, the state watermark, the session title, the meta line, and the sound/notify toggles have mouseover tooltips explaining what they mean.
- Beeps are generated with the Web Audio API (square + triangle octave, master gain 1.2) — no audio files, no subprocesses.
- running → idle: ascending two-tone (659.3 Hz then 987.8 Hz).
- running → waiting: a repeating low note (523.3 Hz) every 10s until the request is answered.
- The AudioContext can only be started from a user gesture, so sound is armed on page load but actually unlocked the first time the sound checkbox is toggled (or any interaction). Each card remembers its own setting in
localStorage(sp-sound-<pid>); the last toggled value is also stored globally (sp-sound-default) and used for new pids that appear after the page is already open. - Background-tab limitation (browser-imposed): while the page is in a non-foreground tab, the browser suspends the AudioContext (beeps scheduled while suspended are dropped, not queued) and throttles the 2s status poll — after ~5 min hidden, transitions may be detected up to a minute late. To compensate, a transition that happens while hidden is recorded as pending and replayed once when you refocus the tab (idle chime, or the waiting loop restarts); pending sounds are dropped if the session resumes or sound is turned off first. Beeps also can't be heard at all while the tab is hidden — for alerts that must reach you with the browser unfocused, use the notify checkbox instead: the alarm sound plays on the machine (server-side), independent of the browser (see Notifications below).
Events session.idle, session.error, and permission.asked can fire an ntfy.sh push notification (to your phone) and play the platform alarm sound on the machine (powershell.exe + C:\Windows\Media\Alarm01.wav on Windows, afplay on macOS, paplay on Linux; a no-op if the player or sound file is missing).
- Create a topic. ntfy topics need no registration and no payment — any name is instantly valid. Pick one; a long random string (e.g.
opencode-9f3k2x7q) keeps it private, since anyone who knows the name can subscribe. - Tell the page about it. Open the status page and type the topic in the ntfy field (bottom right) — either a bare name (
my-topic→https://ntfy.sh/my-topic) or a full URL (https://ntfy.example.com/my-topicfor a self-hosted server). It's global: one target for all sessions. Saved automatically to~/.config/opencode/obvious-grid-config.json. - Subscribe on your phone. Install the ntfy app, point it at the same server (ntfy.sh by default), and subscribe to the topic — subscribing is what creates it.
- Enable per session. Tick the notify checkbox on the card of the session you care about. You'll get a push (and the alarm sound) when that session goes idle (finished), errors, or asks for your permission. Off by default; deleting a session clears its flag. The first time you enable notify, a popup walks through these steps (dismissible permanently via "don't show again").
curl -d "test" https://ntfy.sh/my-topicYour phone should ping (with the plugin's notify checkbox on, the alarm plays on your machine too).
- One notification per 10s max (
NOTIFY_COOLDOWN_MS). Child sessions (created with aparentID) never notify, so spawning a sub-agent doesn't spam you. - The old hidden dotfile (
.afk-notifyflag) is still honored as fallback until the page saves its config file once.
opencode ──events──▶ obvious-grid.js (entry) + lib/* ──write──▶ <os.tmpdir()>/obvious-grid-<pid>-<sessionID>.txt
│
└── serves ──▶ GET / → fullscreen status page (page.html)
GET /api/status → JSON (polled every 2s)
GET/POST /api/notify → notify flag
- Plugin (runs inside opencode). Listens to bus events and writes one JSON state file per session (
obvious-grid-<pid>-<sessionID>.txt) to the platform temp dir (os.tmpdir()—/tmpin WSL/Linux,%TEMP%on Windows):tool.execute.before/message.part.updated→runningpermission.updated/question.asked→waitingpermission.replied/question.replied/question.rejected→idlesession.idle→idle;session.error→errormessage.updated→ model / variant / mode / agent / path, token counts, cache %, cost, finish counts, token rates, session start/last-activitysession.updated→ session title;session.deleted→ state file removed- Card granularity: one card per session, not per process. In CLI mode that's identical to before (one session per instance). In web mode, where one opencode server process runs several sessions, each session gets its own card. Events without a
sessionID(e.g.tool.execute.before, permissions) are routed to the last-active session. Sub-agent sessions (created with aparentID) get their own cards —↳-prefixed with aparent:line and aSUBchip, titled with the task description, carrying their own tokens/cost/calls — and their activity is rolled into the parent session's card: totals (apiCalls,userCalls, total tokens, cost, finish counts) include sub-agent work, rolled up recursively (a sub-agent's own sub-agents count into it first, so each card shows its whole subtree), and a▸chip on each card lists its direct sub-agents' calls and spend (up to 3 shown, active ones first; finished sub-agents are dimmed; the full per-agent breakdown is in the tooltip). Sub-agent contributions stay as long as the child's card exists (until the 60-min staleness window, session delete, or process exit). Current-request metrics (context %, cache hit, rates) stay per-session. Notifications stay per main session — child sessions never notify, and their notify toggle is hidden. - Startup placeholder: as soon as an instance starts, it writes an idle placeholder card (
obvious-grid-<pid>.txt, no session yet) so the instance is visible immediately; the placeholder is replaced by the session's own card on the first session activity. - Writes are deduped — a file is only rewritten on an actual change.
- While blocked on a pending permission/question, non-error transitions are ignored so the blue state isn't clobbered.
- State files. The plugin writes to the platform temp dir directly (
os.tmpdir()); no Windows profile discovery, no/mnt/ctranslation — which is what makes the plugin platform-independent. (Sessions whose working dir is a Windows path in web mode still get their branch read viawinToWslunder WSL.) - Context limits. On startup it calls
client.config.providers()to build aprovider/model → contextmap used for the input-length percentage. - Web port discovery.
opencode webbinds a random port, so for WEB processes the plugin finds the web UI port: on Linux/WSL it scans/proc/net/tcp{,6}for LISTEN sockets whose inode matches this process's fds (via/proc/<pid>/fd), excluding the status port 8765; elsewhere it falls back tonetstat -ano(Windows) /lsof(macOS), bounded by the same 3s subprocess timeout. That port goes into every state file (webPort) so the page can deep-link into web sessions. Only foropencode webprocesses (detected fromprocess.argv). - Session backfill. On first message of a session, the plugin replays
client.session.messages()so call counts, finish counts, cost, token totals and rates reflect the whole session, not just what changed since the page opened. - Built-in HTTP server (
Bun.serve). Binds0.0.0.0:8765and serves:/— the fullscreen page (self-contained HTML, no external assets)./api/status— JSON aggregate of all state files ({ state, running, errored, waiting, instances }, each instance carrying itsnotifyflag), polled every 2s (and on tab focus)./api/notify— GET returns{ topic, sessions }; POST saves a partial update ({ topic }or{ sessionID, enabled }) toobvious-grid-config.json.- If the port is already taken, another opencode instance is serving — this instance skips starting its own server. Instances that failed to bind keep retrying every
SERVER_RETRY_MS(1s), so when the instance owning the server exits, one of the remaining instances takes over within ~1s and the page stays reachable (the page may briefly show "no connection" during the handoff).
- On opencode exit, the plugin deletes its own state files so its sessions drop out of the aggregate immediately. Sessions also drop out if stale for
STALENESS_MS(60 min) — an idle-but-open session's card reappears on its next activity. - Plugin export form. opencode ≥ the plugin refactor (June 2026) only loads modules whose default export is an object with
id+server; the old V1 function export fails with aSchemaError(no shim — tracked upstream as anomalyco/opencode#39345). This plugin uses the V2 form (export default { id: "obvious-grid", server: async (input) => ({ event }) }).
On startup the plugin logs the URLs, e.g.:
[obvious-grid] v2.1.0 open http://localhost:8765 or http://192.168.1.50:8765
- On the same machine: open
http://localhost:8765. - On another device (phone/tablet) on the same Wi-Fi: open
http://<lan-ip>:8765.
WSL2 note: the server binds
0.0.0.0inside WSL. Reaching it from another device may require Windows-side port forwarding (netsh interface portproxy add v4tov4 listenport=8765 listenaddress=0.0.0.0 connectport=8765 connectaddress=<wsl-ip>), depending on your WSL2 networking mode.localhostalways works from Windows itself.
The page is only up while opencode is running (the plugin owns the server). Close opencode → page goes unreachable.
Each opencode process writes its own state file obvious-grid-<pid>.txt (unique even across concurrent WSL processes). The page aggregates across all of them — orange if any is busy, etc.
Closed instances are dropped immediately — the aggregator checks each file's PID liveness with process.kill(pid, 0) (all instances share the same WSL PID namespace) and deletes the file if the process is gone, regardless of whether the exit handler ran. The process.on("exit"/"SIGINT"/"SIGTERM") cleanup is a best-effort backup, not the primary mechanism — opencode's client/server split means the TUI can close while the plugin's server process lingers (or dies without a clean signal).
The HTTP server has no single point of failure. Whichever instance first binds port 8765 owns the server, but every instance that failed to bind retries every 1s. Closing the first-opened instance therefore doesn't kill the page — a surviving instance takes over the port within ~1s.
Safety net for cases the PID-liveness check can't cover — most notably PID reuse: if a dead instance's PID gets recycled by a new WSL process, process.kill(pid, 0) would report it alive. The aggregator therefore also ignores (and deletes) any state file whose last write is older than 60 minutes.
60 minutes is deliberately long because the state file is only rewritten on state transitions — a long-running single tool call keeps the state at running without refreshing the timestamp, and a shorter window would wrongly show green mid-task. The PID check above handles the common close-instance case immediately; the mtime window is just insurance.
| Path | Role |
|---|---|
~/gitrepo/opencode-obvious-grid/obvious-grid.js |
Plugin entry: plugin export, event wiring, HTTP server, page serving (source of truth, git repo) |
~/gitrepo/opencode-obvious-grid/lib/runtime.js |
PORT, PLUGIN_VERSION, client type, web-port discovery |
~/gitrepo/opencode-obvious-grid/lib/win.js |
Bounded subprocess interop: runCmd/runWin/withTimeout, WSL detection + path translation, alarm sound |
~/gitrepo/opencode-obvious-grid/lib/state.js |
Session tracking, state-file persistence, aggregation, git-branch refresh |
~/gitrepo/opencode-obvious-grid/lib/notify.js |
Alarm sound + ntfy.sh push, .afk-notify flag handling |
~/gitrepo/opencode-obvious-grid/page.html |
The static page (plain file — no template-literal escaping) |
~/gitrepo/opencode-obvious-grid/scripts/check-page-script.mjs |
Dev check: extracts the <script> block from page.html and syntax-checks it (run via npm run check) |
~/gitrepo/opencode-obvious-grid/docs/obvious-grid-view.png |
Screenshot of the page (used by this README) |
~/gitrepo/opencode-obvious-grid/docs/grid.png |
Screenshot of the grid with four opencode instances (used by this README) |
~/.config/opencode/plugins/obvious-grid.js |
Symlink → obvious-grid.js in the repo |
<os.tmpdir()>/obvious-grid-<pid>-<sessionID>.txt |
Per-session state files, auto-created/removed (/tmp in WSL/Linux, %TEMP% on Windows) |
%TEMP%\obvious-grid-<pid>.txt |
Idle placeholder while the instance runs with no session yet (same temp dir) |
~/.config/opencode/obvious-grid-config.json |
Notify settings ({ notify: { topic }, sessions: { <sessionID>: true } }), edited from the page (the old status-page-config.json is honored as fallback until the page saves once) |
~/.config/opencode/.afk-notify |
Legacy notify flag (honored as fallback until the page saves its config) |
~/.config/opencode/obvious-grid-session-times.json |
Durable per-session open/run/wait/idle time accumulators (survive restarts) |
/tmp/opencode/obvious-grid-debug.jsonl |
Debug log of every assistant message.updated (written unconditionally; os.tmpdir() on Windows) |
Edit the plugin modules (obvious-grid.js entry + lib/*):
| Knob | Location | Default |
|---|---|---|
| Port | OBVIOUS_GRID_PORT env var (fallback const PORT in lib/runtime.js) |
8765 |
| Plugin version | const PLUGIN_VERSION in lib/runtime.js |
bump on every change |
| Staleness window | const STALENESS_MS in lib/state.js |
60 min |
| Server takeover retry | const SERVER_RETRY_MS in obvious-grid.js |
1 s |
| Notify cooldown | const NOTIFY_COOLDOWN_MS in lib/notify.js |
10 s |
| Page poll interval | setInterval(refresh, 2000) in page.html |
2000 ms |
| Colors | colors map in page.html |
orange / red / blue / green |
| Marquee speeds | updateCard tier thresholds + CSS var(--marq-dur, 90s) |
90 / 60 / 30 s |
| State file dir | os.tmpdir() in lib/state.js (configureStateDir) |
platform temp dir |
| Project | What it does | How this plugin differs |
|---|---|---|
| opencode-observability | Real-time web dashboard of tool calls, messages, and session lifecycle | It ships a whole stack (plugin → Bun server → SQLite → WebSocket → Vue dashboard on separate ports) and is event-trace focused; this plugin is a single package that serves its own page and tracks per-request tokens/cost/rates, not tool traces |
| opencode-token-monitor | Token/cost analytics as in-session tools + terminal charts, budgets | Terminal-first; no web page, no multi-instance aggregate view |
| opencode-usage | CLI usage tracker with a web dashboard | Not an opencode plugin; usage accounting, not live status |
| opencode-notificator / opencode-notifier / opencode-notify | Desktop/OS notifications for session events | Notifications are a side feature here — this plugin is a status page first, with ntfy push + alarm sound merged in |
The niche this fills: a self-contained, zero-infrastructure status page — one npm install, restart opencode, open a URL — that aggregates every instance (CLI and web) on the machine with live per-session metrics, on every platform opencode itself runs on.
- Dev install (from this repo): symlink
obvious-grid.js(the entry) into~/.config/opencode/plugins/and restart opencode. No registration needed — opencode auto-loads*.js/*.tsplugins in that folder.The repo is the source of truth; the plugins-folder copy is a symlink so edits land in git immediately.ln -s ~/gitrepo/opencode-obvious-grid/obvious-grid.js ~/.config/opencode/plugins/obvious-grid.js - Why one symlink is enough (multi-module plugin): opencode loads file plugins with a real dynamic
import()of the entry. The entry's relative imports (./lib/*.js,./page.html) resolve against the entry's realpath, i.e. the repo — so the whole module graph lives in the repo and the install stays a single symlink. Verified against the upstream loader (packages/opencode/src/plugin/loader.tsload()+config/plugin.ts{plugin,plugins}/*.{ts,js}scan withsymlink: true). The npm package works the same way: itsmainpoints at the entry, and the loader imports it like any npm package. - Do NOT drop module files directly into
~/.config/opencode/plugins/— the auto-discovery scan matches any top-level*.js/*.tsin that folder, so stray files there would be auto-loaded as separate bogus plugins. All modules live in the repo, reached only through the import graph. - Versioning: bump
PLUGIN_VERSIONinlib/runtime.jsandversioninpackage.json(they must stay in sync) after each change. Each instance writes its ownversioninto its state file, and the page shows it per instance — so with several instances running you can immediately see which ones are on the old plugin. Plugins are loaded once at startup and never hot-reloaded, so every instance must be restarted to pick up a new version. - Publishing:
npm login, thennpm run check && npm publish(prepublishOnlyruns the check automatically). No runtime dependencies — the code doesn't import@opencode-ai/plugin; the client comes from the plugin input.
Remove the plugin entry from opencode.json (or delete the symlink) and restart opencode. (The one-time legacy status-border PowerShell cleanup was dropped in v2.2.0 along with the Windows profile discovery.)


