Adds an OpenCode tab to the Agents panel in Omarchy's top bar, next to the Claude, Codex and Fireworks tabs it ships with.
Omarchy's Agents panel renders one tab per usage record it finds in
~/.local/state/omarchy/agents/usage/. Omarchy ships collectors that write
claude.json, codex.json and fireworks.json. This repo adds one that writes
opencode.json, by reading OpenCode's own local SQLite database.
No Omarchy files are modified, forked or vendored. It is one Python script, two systemd user units, and an installer.
Prompts and sessions for today, tokens by model, a 7-day activity sparkline,
lifetime totals — and the current session's context occupancy in the tab's
subtitle, e.g. Local gateway - session 25.1k.
- Omarchy, with the stock
agentsshell plugin - OpenCode, having been run at least once
- Python 3.11+ (uses
X | Ytype syntax), stdlib only — no pip install
git clone https://github.com/<you>/omarchy-opencode-usage
cd omarchy-opencode-usage
./install.shThe installer copies the collector to ~/.local/bin/, the units to
~/.config/systemd/user/, then enables and starts the timer. It prints what it
is about to do and asks before doing it.
Open the Agents panel. If the tab is not there yet, give the timer one cycle
(2 minutes) or run systemctl --user start opencode-usage.service.
./install.sh --uninstallEverything is optional.
| Environment variable | Default | Meaning |
|---|---|---|
OMARCHY_OPENCODE_PROVIDER |
(empty) | Count only this OpenCode provider. Empty counts all of them. |
Set it in the unit, not your shell — the timer does not read ~/.bashrc:
systemctl --user edit opencode-usage.service[Service]
Environment=OMARCHY_OPENCODE_PROVIDER=myproviderThe filter is read from the environment and never from opencode.json.
That is deliberate: a per-project opencode.json can move the provider block,
so reading the filter out of the same file it is meant to filter would let one
edit move the check and the target together — and the collector would
confidently report on the wrong thing. Config drift is reported in the tab's
status line; it is never authoritative.
The collector opens OpenCode's database read-only (file:...?mode=ro, plus
PRAGMA query_only) and installs a sqlite3 authorizer that is
deny-by-default against a two-entry allowlist: message.session_id and
message.data. Everything else is denied, including a column added to an
already-allowed table by a future OpenCode release.
That matters because the same database holds things a status widget has no
business touching: account and control_account (OAuth tokens), credential
(integration secrets), session_share (share secrets and URLs), and
session_input, part and todo (your prompts and tool output, verbatim).
An allowlist by column is the only shape that expresses "and nothing that
gets added later, either".
It makes no network call, ever — and the systemd unit sets
PrivateNetwork=yes, which turns that from "true because no import does it
today" into a property the kernel enforces against any future edit.
Before the record is written it is shape-asserted: exact key set, every string value constrained to a tight charset, every number checked to be a number. Model ids are sanitized and the number of distinct model keys is capped, so an unexpected id can never become an unbounded new key in a file that may sync to other machines.
It does not report cost. Against a local or self-hosted gateway,
session.cost is 0.0 for every row because local inference has no price
attached. Rendering that as "$0.00 spent" would be a confident lie rather than
a measurement, so the field is not read at all.
It does not invent a context ceiling. The tab shows how large the current
session's prompt is, not how large it may get. OpenCode only knows a model's
context limit if the client config declares one — for a custom provider with no
declared limit, OpenCode's own overflow check is disabled and the ceiling
genuinely is not knowable from the database. Showing a percentage against a
guessed maximum would manufacture a number, so the tab shows the occupancy and
stops there.
Omarchy's omarchy-agent-usage-update only globs "$OMARCHY_PATH"/bin/ for
collectors, so a collector in ~/.local/bin is never discovered by it. The
panel watches the records directory and renders any valid record it finds;
the refresh path runs first-party collectors only. So this ships its own
timer rather than relying on an extension point that does not exist for a user.
The timer runs every 2 minutes, not every 15, because the panel's per-record file watcher applies a rewrite immediately rather than waiting for its own refresh — so the session-context number is only useful at a cadence close to live. Measured cost is ~0.6–0.8 s per run, roughly a 0.5% duty cycle.
If a scan fails on a machine that has no previous record, the collector
writes an honest not-ready record with zero counters — and Omarchy's
providerHasData() gate hides a zero-counter tab. So the tab disappears and
the stated reason is never displayed: could not answer renders identically to
never asked.
Where a previous good record exists, the collector carries those totals forward
(marked ready: false, with the reason and a stale-totals note) specifically so
the tab survives and can tell you something is wrong. Nothing is ever
manufactured on a fresh machine.
MIT — see LICENSE.
Not affiliated with the Omarchy or OpenCode projects.
