Live session badges on every model name — right inside the OpenCode model picker.
Do you run 5-7 chat sessions at once and lose track of which model is busy and which is idle? This plugin appends a live badge to every model name in the picker:
┌─ model picker ────────────────────────────────┐
│ MiMo-V2.6-Flash Free ● [3] ← 3 sessions │
│ DeepSeek V4.1 Flash Free ● ← 1 session │
│ GLM-5.3 Flash Free ↻ ← rate-limited │
│ Big Pickle ✕ ← limit used up │
└───────────────────────────────────────────────┘
| Badge | Meaning |
|---|---|
| (nothing) | No session is currently running on that model |
● |
Exactly 1 chat session is running |
● [N] |
N (2+) sessions are running on the same model in parallel |
↻ |
Rate-limited — the model hit a rate/usage limit; OpenCode is retrying with back-off |
✕ |
Rate/usage limit exhausted — the retry policy gave up on a limit; shown until the provider's reset epoch (default 10 min, capped 24 h). Plain transport failures (502 storms, timeouts) never earn ✕ [#10] |
Only currently running sessions are counted — never history. A model earns a badge only while it is actively working.
Community: questions, ideas, show-and-tell → Discussions. The v0.4 roadmap vote lives in discussion #16.
Option A — plugins folder (recommended):
# Windows
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.config\opencode\plugins\model-badges" | Out-Null
Copy-Item index.ts, lib.ts, autobg.ts, package.json "$env:USERPROFILE\.config\opencode\plugins\model-badges\"# macOS / Linux
mkdir -p ~/.config/opencode/plugins/model-badges
cp index.ts lib.ts autobg.ts package.json ~/.config/opencode/plugins/model-badges/Option B — config reference (add to your opencode.jsonc):
Reload: just touch the LastWriteTime of opencode.jsonc (or restart OpenCode) — the plugin loads instantly:
(Get-Item "$env:USERPROFILE\.config\opencode\opencode.jsonc").LastWriteTime = Get-DateUninstall: delete the plugins\model-badges folder — done. No config entries, no registry, no background service.
- Multi-agent workflows — 5-7 agents run on different models in parallel; open the picker and instantly see each model's load.
- Free-model rotation — a free model got rate-limited? It shows
↻(retrying) or✕(limit used up) — switch to a healthy free one immediately. - Stuck session detection — a session building for 10 minutes keeps its
●on the same model (tool-call aware). - Fair model choice — when opening a new session, pick a model that is not already overloaded.
- Live status board — the model picker doubles as a dashboard; no separate monitoring tool needed.
- AutoBG hands-free — a 4-minute build unmanned at 2 min keeps running in background with a visible countdown — the foreground stays yours.
A session whose tool call (bash / scripts / MCP) keeps the foreground blocked longer than 1 minute is moved to background automatically — exactly what Ctrl+B does, exactly when you'd have wanted to press it:
- T-15s: one toast —
AutoBG: 15s — "bash" moves to background automatically(you see it coming) - T=0: the session runs the same operation Ctrl+B runs (
POST /api/session/{id}/background) +AutoBG: moved to background (ran 1m01s)— and the tool keeps running in background (backgrounding never kills tools, that's the engine's own contract) - Scope is tools-only: a model thinking/writing for 10 minutes is never touched — only open tool calls count (
session.tool.input.started → success/failedon both v1 and v2 event vocabularies) - Won't touch: subagent children (
parentID, #12 guard), sessions not yet visible in the model map, already-ended tools (<60s), and one-shot per session turn (turn end re-arms it) - Cross-process safe (v0.4.3): running several OpenCode processes at once? An NTFS-atomic lock file (
.autobg-fire-<sid>.lock, 10s TTL) means only the FIRST process posts the move and shows the toast — the rest stand down - 10-minute zombie cap: a tool whose end event was missed stands down silently instead of firing on a stale clock
Threshold lives at the top of autobg.ts (THRESHOLD_MS = 60_000, WARN_BEFORE_MS = 15_000). If your OpenCode build removed the client toast route, the countdown falls back to debug.log (which also logs the one-time client-shape probe: autobg client keys …) and the background move still happens.
┌────────────────────────┐ SSE stream (fetch, localhost) ┌─────────────────────┐
│ OpenCode server │ ────────────────────────────────▶ │ model-badges plugin │
│ GET /api/event │ session.status / *.delta / │ │
│ │ session.tool.* / usage.updated │ runningCount map │
└────────────────────────┘ └──────────┬──────────┘
│ ctx.model.transform()
┌──────────▼──────────┐
│ model name + badge │
│ "DeepSeek ●" │
└─────────────────────┘
- Events: the server's SSE endpoint (
/api/event) is read with plainfetch()— no child processes, no terminal flash, no external dependencies. - Liveness logic (four layers, combined):
- Authoritative —
session.statusevents (busy/retry/idle). - Activity TTL — generation-stream events (
.delta,session.step.*,session.tool.*,usage.updated) keep a badge alive for 15 seconds. - Tool-call tracking — during a long build/script the model goes silent; open tool calls are tracked by
toolCallIDso the badge survives (max 10-minute safety cap). - Rate-limit tracking — a session whose status is
retryis blocked, not running: the model earns↻. If the retry policy gives up (status drops to idle), the model keeps✕until the provider-providednextepoch (retry-after), capped at 24 h, default window 10 min. The back-off reason is honoured (#10): the event's ownmessage/errortext decides — explicit rate-limit vocabulary (or typedprovider.free-limitclasses) earns✕, transport wording (502/gateway/timeout/network) earns nothing at all, and unrecognised wording stays permissive (it can still mark✕).
- Authoritative —
- Accuracy safety: stale "idle" and "retry" marks are overridden by fresh activity, deleted sessions have all state cleaned, and a 10-minute safety refresh catches missed events.
- Model switches: when an event carries the selected model, its session's badge moves immediately without listing sessions again. Incomplete model events refresh the paginated map after the normal 0.5-second debounce; bulk session updates keep their 8-second debounce.
- OpenCode v1 + v2 channels: the same badge contract covers both event vocabularies — v1's
session.status/session.idle, and v2'ssession.model.selected/session.retry.scheduled/session.execution.*(a model switch moves its badge instantly on both). Durable v2 frames replayed after a reconnect may fix bookkeeping but never paint liveness — no ghost ● on sessions that idled before the connection. - Badge hygiene: names are rebuilt from the bare name every transform (old suffix stripped, new suffix appended) — double badges or stuck badges are impossible.
- Tuning: constants at the top of
index.ts—ACTIVITY_TTL_MS,STATUS_DEBOUNCE_MS,TOOL_MAX_MS,MIN_MULTI,RETRY_MARK_DEFAULT_MS,RETRY_MARK_MAX_MS.
Q. Why isn't the badge instant? Updates are pushed after a ~0.5s debounce. Updating per-token would reload the picker constantly and make it flicker — this is the sweet spot.
Q. Why plain Unicode glyphs instead of emoji?
● ↻ ✕ are text glyphs, not emoji — they render monochrome and identical in every terminal and font (no color-emoji dependency, no Segoe UI surprises on Linux/SSH), and they align cleanly in monospace. Names carrying the legacy ✅ / ✔️ [N] / ⭐ badges from v0.1.0 are still recognized and cleaned up on the first refresh.
Q. Does history count? No — only currently running sessions count. Old sessions never earn a badge.
Q. How long does the ✕ stay?
Until the provider-provided reset epoch (from retry-after) if the event carried one, capped at 24 hours — otherwise a fixed 10-minute window. Any fresh activity (or a new run) replaces it with the normal badges.
Q. How do I debug?
~/.config/opencode/plugins/model-badges/debug.log contains map: running=[...] retry=[...] exhausted=[...] lines showing the live state.
Q. Does it work with any provider/model? Yes — the plugin only transforms model names, so any provider is supported.
Rate/usage limit aaya? This plugin aborts the stuck turn, puts the failed model on a doubling cooldown, and transparently re-sends the work on the next healthy model — your session, context and agent keep flowing. When the original model recovers, the next prompt goes back to it.
big-pickle ✕ (limit) → auto → glm-5.3-flash-free ✅ → auto → back to big-pickle 🔄
- Config:
~/.config/opencode/fallback.json— the chain is the full free pool (big-pickle,glm-5.3-flash-free,deepseek-v4.1-flash-free,mimo-v2.6-flash-free,beam-free,flint-40b-free,nemotron-3.5-super-vl-free). Delete lines to shrink it. Seefallback.example.json. - Modes:
cycle(wrap the chain) /stop(toast and give up); transient errors (429/5xx) first let OpenCode's own retry policy takemaxRetriesswings, immediate classes (usage/quota) switch at once. - Mid-run failures send a configurable continue message (the assistant already produced work) instead of replaying the prompt; optional revert keeps history clean.
- State survives restarts — cooldowns persist in
~/.local/share/opencode/model-fallback-state.json; every switch logs viaclient.app.log+plugins/fallback.log. - Install: copy both plugins into
~/.config/opencode/plugins/:
Copy-Item index.ts, lib.ts, fallback.ts, package.json "$env:USERPROFILE\.config\opencode\plugins\"
Copy-Item fallback.example.json "$env:USERPROFILE\.config\opencode\fallback.json"- Upstream watch (no submodules, no Watch needed):
upstream.jsonrecords feature sources + snapshot SHAs of 12 community repos;tools/upstream-check.mjsruns daily on cron (.github/workflows/upstream-check.yml) and opens a tracking issue here the moment any upstream moves. The engine is an independent implementation — the manifest is traceability, not vendored code.
chain cycles · cycle/stop modes · doubling cooldown · persistent cooldown state · toasts (azumag) · immediate vs transient classification · retry thresholds (HyeokjaeLee) · last-user replay · revert of partial turns (peva3) · flow-preserving switch (zaplakhov) · mid-run continue message (renjfk) — deferred ideas (benchmarks, discovery scoring, account balancing, quota tools) tracked as (deferred) in upstream.json.
Issues and PRs are welcome! index.ts is a single file, so it's easy to read. See CONTRIBUTING.md for the workflow. Good-first-issue ideas live on the Issues page.
Made with ❤️ for OpenCode power users. Star ⭐ if it saved you a "which model is busy right now?" moment!
![live demo: running ● / ● [N] sessions, instant badge move on model switch (session.model.updated), rate-limited ↻, limit exhausted ✕](/engrtarun/opencode-model-badges/raw/main/docs/demo.gif)
{ "plugin": ["C:/path/to/opencode-model-badges/index.ts"] }