* Add agent usage collectors that write display-ready data files
One omarchy-agent-usage-scan-<agent> collector per AI coding agent prints a
complete display-ready usage record — identity, tier, status, rate limits,
and today/week/all-time stats. omarchy-agent-usage-update runs every
collector it finds and writes the records atomically to
~/.local/state/omarchy/agents/usage/, so anything that displays usage only
ever reads JSON from there.
The Claude collector absorbs what the shell previously did in-process:
transcript scanning, the stats-cache/history fallback, credentials parsing,
and the OAuth limits probe, now with a probe throttle and last-good limits
kept across network failures. The Codex collector is the existing scanner
reshaped to the shared record contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Redo the model-usage plugin as omarchy.agents watching usage data files
The panel is now strictly a display. It discovers the JSON records that
omarchy-agent-usage-update maintains under
~/.local/state/omarchy/agents/usage/, watches them for changes, and draws
whatever appears — so adding an agent means shipping a collector, never
touching the panel. Marks resolve by convention (assets/<id>.svg with an
optional -light twin), the limits meters read a generic limits array, and
the per-provider QML adapters and in-plugin scanner scripts are gone.
Cross-device sync aggregation stays in the shell and keeps the snapshot
field names older versions wrote, so mixed-version fleets still merge in
both directions.
With the provider fan-out gone, the widget takes its real name: the plugin
id becomes omarchy.agents. A migration renames it wherever a user's config
mentions it — layout entries keep their settings and position, a disabled
widget stays disabled — then primes the data files once and drops the old
scanner cache. The migration test also drops a stale assertion that expected
migrations to restart the shell themselves, which c992cdff moved to
omarchy update.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address Codex review: synced-only tabs, limits retry, history fallback
Three data-availability gaps from review. An agent whose records only exist
in synced snapshots — a collector installed on just one machine — now gets
its tab by unioning the synced aggregate into the provider list, with rate
limits blank since those never travel. A Claude limits probe that reaches no
server at all writes retryAdvised into its record, and the shell honors it
with one 30-second retry instead of waiting out the full refresh interval,
restoring the old boot-before-DHCP behavior. And a machine with only
history.jsonl — no transcripts, no stats-cache — still reports today's
prompt and session counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address second Codex pass: history-only visibility, targeted retries
Today's prompt and session counts now count toward an agent's presence in
the bar, so a machine whose only Claude source is history.jsonl shows up
without waiting for limits. And the 30-second limits retry passes the
advising agent ids to the updater, so an outage at one provider no longer
puts every other collector on a retry treadmill.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop omarchy-cmd-present jq guards from the agents migrations
jq ships in the default package set, which makes it a runtime invariant per
AGENTS.md — call it directly. The migration tests lose their now-unused
omarchy-cmd-present stubs with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the scan infix from the collector command names
Collectors are omarchy-agent-usage-<agent>; the updater skips its own name
when globbing them, and the update test proves it with a decoy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep the credential store out of the printed usage record
The Claude collector now reads .credentials.json once into three scalars —
the access token, its expiry, and the plan label — instead of passing the
parsed store around. The token reaches nothing but the Authorization header
of the limits probe, and only the plan label may travel into the record,
which is what CodeQL's clear-text-logging alert on the record print was
unable to see when the whole dict flowed through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
298 lines
13 KiB
Markdown
298 lines
13 KiB
Markdown
# Omarchy shell
|
|
|
|
`omarchy-shell` is a single long-running [Quickshell](https://quickshell.org/)
|
|
instance that hosts the Omarchy desktop. Hyprland autostart launches one shell
|
|
per graphical session; everything else — the bar, background switcher, panels,
|
|
and overlays — runs **inside** the shell as a plugin.
|
|
|
|
Hosting everything inside one shell means:
|
|
|
|
- shared services and singletons live once, not once per process
|
|
- summoning a panel is an IPC call into a process that is already running,
|
|
not a fresh `quickshell -p ...` cold start
|
|
- third-party plugins can be loaded from disk without changing any source
|
|
code in Omarchy itself
|
|
|
|
The runtime layout:
|
|
|
|
```
|
|
shell/
|
|
shell.qml entry point (ShellRoot)
|
|
services/
|
|
PluginRegistry.qml discovers, validates plugins, looks up enabled state in shell.json
|
|
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
|
|
plugins/
|
|
bar/ first-party plugins (see plugins/README.md)
|
|
image-picker/
|
|
menu/
|
|
notifications/
|
|
panels/
|
|
audio/
|
|
bluetooth/
|
|
monitor/
|
|
network/
|
|
power/
|
|
weather/
|
|
agents/
|
|
services/
|
|
battery/
|
|
idle/
|
|
osd/
|
|
polkit/
|
|
```
|
|
|
|
The plugin discovery path is documented in [plugins/README.md](plugins/README.md).
|
|
|
|
## Plugin manifest
|
|
|
|
Every plugin ships a `manifest.json` describing what it is and how the
|
|
shell should load it. Minimal example:
|
|
|
|
```json
|
|
{
|
|
"schemaVersion": 1,
|
|
"id": "my.org.cool-clock",
|
|
"name": "Cool clock",
|
|
"version": "1.0.0",
|
|
"author": "You",
|
|
"description": "A clock that does cool things",
|
|
"kinds": ["bar-widget"],
|
|
"entryPoints": { "barWidget": "Widget.qml" },
|
|
"barWidget": {
|
|
"displayName": "Cool clock",
|
|
"category": "Time",
|
|
"allowMultiple": false,
|
|
"defaultSection": "left",
|
|
"defaults": { "format": "HH:mm" },
|
|
"schema": [
|
|
{ "key": "format", "type": "string", "label": "Format" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Supported `kinds`:
|
|
|
|
| Kind | What it is |
|
|
|--------------|--------------------------------------------------------------|
|
|
| `bar-widget` | A component that the active bar can drop into a section |
|
|
| `panel` | A persistent or summoned floating window (e.g. OSD) |
|
|
| `overlay` | A fullscreen overlay (e.g. background switcher) |
|
|
| `menu` | A summoned menu surface |
|
|
| `service` | A headless singleton, no UI |
|
|
| `bar` | A full bar option that can replace the built-in `omarchy.bar` |
|
|
|
|
Only one `bar` plugin is active at a time. Missing or invalid selections fall
|
|
back to the built-in `omarchy.bar`, so users always have a safe path home.
|
|
Panels, overlays, and menus are loaded when summoned. Plugins that need
|
|
to outlive a single summon can set `keepLoaded: true` (e.g. the image
|
|
picker keeps its overlay window mounted between summons). First-party
|
|
services are loaded at startup.
|
|
|
|
The full schema lives in `services/PluginRegistry.qml`.
|
|
|
|
## Installing a third-party plugin
|
|
|
|
A plugin is a **git repo** with a `manifest.json` at its root. Adding one
|
|
clones it straight into `~/.config/omarchy/plugins/<id>/` (named by the
|
|
manifest id); updating is a fast-forward pull of that checkout.
|
|
|
|
```bash
|
|
omarchy plugin add https://github.com/acme/omarchy-weather.git
|
|
omarchy plugin update acme.weather # fetches, shows a diff, fast-forwards
|
|
omarchy plugin update # updates every git-managed plugin
|
|
omarchy plugin remove acme.weather
|
|
```
|
|
|
|
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns
|
|
> you before cloning, plugins land disabled so you can review the code before
|
|
> enabling, and updates show a diff of the changes before touching anything.
|
|
> Only add repos whose code you are willing to run.
|
|
|
|
Each command is **interactive** when run bare in a terminal (gum pickers,
|
|
confirmation, a diff to review) and fully **non-interactive** when given
|
|
arguments. Pass `--yes` to skip every prompt — this is the path for scripts and
|
|
AI agents:
|
|
|
|
```bash
|
|
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable --yes
|
|
omarchy plugin update --yes
|
|
```
|
|
|
|
The installer never runs plugin code, install hooks, or sudo — it only clones
|
|
files, validates the manifest, and toggles enabled state over shell IPC. Since
|
|
an installed plugin is a plain git checkout, anything beyond add/update
|
|
(pinning a ref, switching branches) is ordinary git in the plugin directory.
|
|
|
|
### Installing by hand
|
|
|
|
You can still drop a plugin in without git:
|
|
|
|
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
|
|
plus the QML referenced from its `entryPoints`.
|
|
2. `omarchy-shell shell rescanPlugins`.
|
|
3. `omarchy plugin enable <id>`. Bar widgets start in
|
|
`barWidget.defaultSection`, or in the center when it is omitted, and can be
|
|
moved with `omarchy bar move`; a full bar replaces the one in use.
|
|
|
|
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
|
|
`omarchy-shell shell enablePlugin <id> '{}'`, and `omarchy-shell shell listPlugins`.
|
|
The `omarchy plugin` commands wrap those calls. `omarchy bar move` and
|
|
`omarchy bar set` edit the persisted widget layout in `shell.json`.
|
|
|
|
To hack on a built-in plugin safely, clone it into user config instead of
|
|
editing the built-in source. The complete plugin directory is copied, including
|
|
every declared kind and local dependency. A built-in id such as
|
|
`omarchy.clock` becomes `<username>.clock` (e.g. `dhh.clock`), with `My Clock`
|
|
as its display name. The username prefix keeps shared clones from colliding
|
|
with each other or with other plugin authors.
|
|
|
|
```bash
|
|
omarchy plugin clone omarchy.clock
|
|
```
|
|
|
|
Cloning switches from the built-in to the new personal plugin, preserving an
|
|
existing bar widget's position and settings. Setup > Plugins > Clone provides
|
|
the interactive picker, then opens the new `<username>.*` directory in `$EDITOR`.
|
|
Existing shortcuts and shell IPC calls made to the built-in id are routed to
|
|
the enabled clone, so cloning does not require changing its callers. Removing
|
|
an active clone switches back to its built-in source.
|
|
Saving a file anywhere under `~/.config/omarchy/plugins/` reloads plugin code
|
|
automatically; `omarchy-shell shell rescanPlugins` remains available to force a reload.
|
|
|
|
First-party plugins under `shell/plugins/` are discovered the same way and load
|
|
by default. Disabling a non-widget records it in `disabledPlugins[]`; disabling
|
|
a widget removes it from the bar layout while leaving its component available
|
|
to add again. A full bar has no off state and is replaced by enabling another.
|
|
|
|
## IPC contract
|
|
|
|
The shell exposes a single `shell` IPC target plus whatever extra targets
|
|
individual plugins register (e.g. the bar's `bar` target for refresh
|
|
hooks, the image picker's `image-selector` target). `omarchy-menu` uses the
|
|
shell target to summon the first-party `omarchy.menu` plugin instead of
|
|
running a separate Quickshell instance.
|
|
|
|
| Method | Returns | Effect |
|
|
|------------------------------------------|---------|-------------------------------------------------------|
|
|
| `ping` | `ok` | health check |
|
|
| `summon <id> <payloadJson>` | `ok` / `unknown` | load + open a panel/overlay plugin |
|
|
| `hide <id>` | — | close a previously-summoned plugin |
|
|
| `toggle <id> <payloadJson>` | — | summon if closed, hide if open |
|
|
| `call <id> <method> <arg>` | string | call a method on an already-loaded plugin |
|
|
| `rescanPlugins` | — | re-walk plugin dirs and hot-reload plugin code |
|
|
| `reloadConfig` | `ok` | reload `~/.config/omarchy/shell.json` |
|
|
| `setPluginEnabled <id> <enabled>` | `ok` / `unknown` | flip the persisted enabled bit (see note) |
|
|
| `listPlugins` | JSON | every discovered plugin, sorted by name |
|
|
|
|
Direct invocation:
|
|
|
|
```
|
|
quickshell ipc -p $OMARCHY_PATH/shell call shell ping
|
|
```
|
|
|
|
Hyprland autostart launches the shell directly with `quickshell -p
|
|
$OMARCHY_PATH/shell`. Use `omarchy-restart-shell` to stop every running
|
|
instance of that config and launch one fresh shell process.
|
|
|
|
A convenience wrapper, [`omarchy-shell`](../bin/omarchy-shell), forwards IPC
|
|
calls to the running shell. It does not start the shell.
|
|
|
|
```
|
|
omarchy-shell shell ping
|
|
omarchy-shell shell toggle omarchy.menu '{"menu":"root"}'
|
|
omarchy-shell shell listPlugins
|
|
omarchy-shell shell rescanPlugins
|
|
```
|
|
|
|
**Note on `setPluginEnabled`:** the `enabled` argument is a string. Only the
|
|
literal `"true"` enables the plugin; every other value (including `"True"`,
|
|
`"1"`, `"yes"`, or omitted) disables it. This keeps the IPC surface
|
|
type-stable across QML's `string`-only IPC arguments.
|
|
|
|
## Persisted state
|
|
|
|
There is one user config file. Everything that distinguishes your
|
|
customization from the shipped defaults lives in it.
|
|
|
|
| Path | Owner | Purpose |
|
|
|-----------------------------------|----------------|--------------------------------------------------------|
|
|
| `~/.config/omarchy/shell.json` | the shell | full layout + per-entry settings + enabled plugin list |
|
|
| `~/.config/omarchy/plugins/<id>/` | user | drop-in third-party plugin source files |
|
|
|
|
The `config/omarchy/shell.json` default config describes the
|
|
fresh-install state. When the user has no `shell.json`, the shell uses
|
|
the defaults verbatim. Once the user customizes anything, `shell.json`
|
|
becomes the authoritative file — we do **not** deep-merge defaults back in.
|
|
|
|
### shell.json shape
|
|
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"idle": {
|
|
"screensaver": 150,
|
|
"lock": 300
|
|
},
|
|
"bar": {
|
|
"id": "omarchy.bar",
|
|
"position": "top",
|
|
"transparent": false,
|
|
"centerAnchor": "omarchy.clock",
|
|
"layout": {
|
|
"left": [ { "id": "omarchy.menu" }, { "id": "omarchy.workspaces" } ],
|
|
"center": [ { "id": "omarchy.clock", "format": "HH:mm" } ],
|
|
"right": [
|
|
{ "id": "omarchy.audio" }
|
|
]
|
|
}
|
|
},
|
|
"plugins": []
|
|
}
|
|
```
|
|
|
|
### Storage rules
|
|
|
|
1. **The active bar option is `bar.id`.** Omit it or set it to `omarchy.bar`
|
|
to use the built-in bar. Set it to another plugin id whose manifest declares
|
|
`kind: "bar"` to replace the full bar.
|
|
2. **Every plugin instance is one entry.** Either in `bar.layout.<section>`
|
|
for bar widgets, or in `plugins[]` for panels, overlays, services,
|
|
menus, and anything else non-bar.
|
|
3. **Settings are inline on the entry.** No `config:` sub-object, no
|
|
separate per-plugin settings file, no merge layers. The fields on each
|
|
entry are the values the plugin sees.
|
|
4. **Built-in widget ids are namespaced.** Use ids such as `omarchy.clock`,
|
|
`omarchy.audio`, and `omarchy.network`. The migration rewrites older ids
|
|
like `Clock` and `AudioPanel` forward.
|
|
5. **Third-party enabled ⇔ present.** A third-party plugin is enabled iff
|
|
its id appears somewhere in shell.json. For full bar options, that means
|
|
`bar.id`; for bar widgets, plugin enable/disable adds/removes layout entries;
|
|
other plugin kinds are enabled the same way. First-party non-bar plugins
|
|
are enabled unless listed in `disabledPlugins[]`.
|
|
6. **Multiple instances** are allowed when a manifest sets
|
|
`allowMultiple: true`. Each instance is independent — e.g. two clock
|
|
widgets in different timezones are just two `{"id":"omarchy.clock", "timezone": ...}`
|
|
entries with their own values.
|
|
7. **Idle timings are top-level.** `idle.screensaver` and `idle.lock`
|
|
are seconds since user idle began, so the default lock fires at 300s
|
|
even if the 150s screensaver starts first.
|
|
8. **`version: 1` is required** at the top level. The shell will fall back
|
|
to defaults rather than load an unknown version.
|
|
|
|
## Implementation history
|
|
|
|
Built up in phases on this branch:
|
|
|
|
- Phase 1 — `omarchy-shell phase 1: host the existing bar in a single shell`
|
|
- Phase 2 — `omarchy-shell phase 2: plugin registry and bar widget registry`
|
|
- Phase 3 — `omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin`
|
|
- Phase 4 — `omarchy-shell phase 4: absorb background-switcher as a plugin`
|
|
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs`
|
|
- Phase 6 — `omarchy-shell phase 6: reviewer cleanup (path traversal, collision, races)`
|
|
- Phase 7 — `omarchy-shell phase 7: replace socket with IpcHandler, rename to image-picker`
|
|
- Phase 8a — `omarchy-shell phase 8a: unified shell.json with inline plugin settings`
|
|
|
|
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
|
|
out of scope here and deferred to a follow-up after a review pass.
|