{"id":"usage-sidebar","repo":"RUIIIOVO/paseo-usage-sidebar","url":"https://github.com/RUIIIOVO/paseo-usage-sidebar","name":"usage-sidebar","description":"Provider plan usage in the Paseo sidebar: an openable panel plus an always-visible meter under the sidebar entry, read from Paseo's own usage data so the numbers match Settings → Usage.","categories":["monitoring"],"platforms":[],"caveats":["Requires Paseo 0.8.0 or later","The always-visible sidebar meter is desktop and web only; the panel works everywhere","Only providers that report usage to the daemon get a row"],"images":["https://raw.githubusercontent.com/RUIIIOVO/paseo-usage-sidebar/d2b4659d69447af9999afa22db8ac5a1eeebca27/images/overview.png","https://raw.githubusercontent.com/RUIIIOVO/paseo-usage-sidebar/d2b4659d69447af9999afa22db8ac5a1eeebca27/images/sidebar-meter.png","https://raw.githubusercontent.com/RUIIIOVO/paseo-usage-sidebar/d2b4659d69447af9999afa22db8ac5a1eeebca27/images/usage-panel.png"],"health":{"manifestValid":true,"hasReadme":true,"hasLicense":true,"hasTests":false,"hasTypecheckScript":true,"updatedRecently":true},"scannedAt":"2026-09-11T08:04:29.867Z","security":{"status":"passed","blockingFindings":0,"advisoryFindings":0,"scannedAt":"2026-09-11T08:02:40.142Z","commit":"d2b4659d69447af9999afa22db8ac5a1eeebca27"},"license":"MIT","paseoVersionRequirement":">=0.8.0","manifest":{"id":"usage-sidebar","requirements":{"paseo":">=0.8.0"}},"repoMeta":{"stars":0,"defaultBranch":"main","pushedAt":"2026-09-11T03:26:07Z"},"owner":{"login":"RUIIIOVO","avatarUrl":"https://avatars.githubusercontent.com/u/119831056?v=4"},"installNotesHtml":"<p>Requires <strong>Paseo 0.8.0 or later</strong>.</p>\n<pre><code class=\"language-bash\">paseo plugin add RUIIIOVO/paseo-usage-sidebar\n</code></pre>\n<p>Enable plugins first under <strong>Settings → Plugins → Enable plugins</strong> if you have not already. Then\npick <strong>Usage</strong> in the sidebar, or run <strong>Open plan usage</strong> from the Command Center\n(<code>Ctrl</code>/<code>Cmd</code> + <code>K</code>).</p>\n<p>Update later with:</p>\n<pre><code class=\"language-bash\">paseo plugin update usage-sidebar\n</code></pre>\n<blockquote>\n<p>The floor is declared as <code>requirements.paseo: \">=0.8.0\"</code> in <code>paseo-plugin.json</code>. Paseo 0.7 and\nearlier cannot load this plugin: it uses the 0.8 runtime-entry layout.</p>\n</blockquote>\n<hr>","limitationsNotesHtml":"<ul>\n<li><strong>Sidebar placement is host-owned.</strong> The plugin API offers no sidebar widget, badge, or footer\nslot — only <code>{ id, title, icon, surface }</code>. The always-visible meter is an unsupported DOM escape\nhatch (see <a href=\"#the-sidebar-meter\">above</a>) and exists on desktop and web only.</li>\n<li><strong>Only providers that report usage get rows.</strong> Providers without a signed-in session are counted\nin the footer, not rendered.</li>\n<li><strong>Percentages are the daemon's</strong>, including their refresh cadence. The plugin does not re-derive\nor estimate anything, so a provider that rate-limits its own usage endpoint stays stale until\nPaseo refreshes it.</li>\n</ul>\n<hr>","readmeText":"# paseo-usage-sidebar\n\n**English** · [简体中文](./README.zh-CN.md)\n\nA [Paseo](https://paseo.sh) plugin that puts provider plan usage in the sidebar — as a panel you\ncan open, and as an always-visible meter under the sidebar entry.\n\nPaseo already knows how much of your plan is left. It just keeps that behind a settings screen and\na hover tooltip on the composer's context meter. This plugin surfaces the same data where you can\nactually see it.\n\nNo new credentials, no vendor CLI, no second polling path: the numbers come from Paseo's own\n`provider.usage.list` data, so they always match what **Settings → Usage** shows.\n\n![The sidebar meter and the usage panel side by side](images/overview.png)\n\n<p align=\"center\">\n  <img src=\"images/sidebar-meter.png\" alt=\"The always-visible sidebar meter\" width=\"284\">\n</p>\n\n---\n\n## Contents\n\n- [Install](#install)\n- [Features](#features)\n  - [The usage panel](#the-usage-panel)\n  - [The sidebar meter](#the-sidebar-meter)\n  - [Pinning and ordering](#pinning-and-ordering)\n  - [Localization](#localization)\n- [Configuration](#configuration)\n- [How it reads usage](#how-it-reads-usage)\n- [Security](#security)\n- [Limitations](#limitations)\n- [Project structure](#project-structure)\n- [Development](#development)\n- [License](#license)\n\n---\n\n## Install\n\nRequires **Paseo 0.8.0 or later**.\n\n```bash\npaseo plugin add RUIIIOVO/paseo-usage-sidebar\n```\n\nEnable plugins first under **Settings → Plugins → Enable plugins** if you have not already. Then\npick **Usage** in the sidebar, or run **Open plan usage** from the Command Center\n(`Ctrl`/`Cmd` + `K`).\n\nUpdate later with:\n\n```bash\npaseo plugin update usage-sidebar\n```\n\n> The floor is declared as `requirements.paseo: \">=0.8.0\"` in `paseo-plugin.json`. Paseo 0.7 and\n> earlier cannot load this plugin: it uses the 0.8 runtime-entry layout.\n\n---\n\n## Features\n\n### The usage panel\n\nThe surface reproduces the layout of **Settings → Usage**: one bordered card, one row per provider,\nhairline dividers between them.\n\n| Row type | What it shows |\n| --- | --- |\n| **Quota windows** | Session, weekly, and model-scoped windows as `57% · resets in 2h 15m` (or `57% · resets at Nov 12, 10:00` once the reset is more than a day out), with a zero-baseline bar. |\n| **Balances** | Money, credits, requests, or tokens, against a ceiling where one exists. |\n| **Details** | Provider-supplied key/value lines such as `Extra usage: Disabled`. |\n| **Status** | Providers that are not signed in stay listed with an `Unavailable` dot rather than disappearing, so the list matches what Settings shows. |\n\nWindow names are normalized against this plugin's own message table (`5-hour session`, `Weekly`,\n`Weekly (Fable)`) rather than passed through: the daemon calls the 5-hour window `Session`, which\nsays nothing about the period it covers, and spells a model-scoped one `Weekly · Fable` in English\nonly. Going through the message table means every window name follows the app's language setting.\n\nSpacing, type scale, tone thresholds, and the reset/`runs out` wording are taken from Paseo's own\nprovider-usage components, so the panel reads identically to the settings screen. The one\ndifference is provider brand logos: those come from a host-internal icon registry that plugins\ncannot import, so rows lead with the provider name.\n\nThe surface refreshes every 60 seconds and on demand from **Refresh**.\n\n### The sidebar meter\n\n*Desktop and web only.*\n\nUnder the sidebar entry, the plugin renders a compact always-visible meter: one row per pinned quota\nwindow — label, percentage, a thin bar, and the countdown to that window's reset. It refreshes on\nthe same 60-second cycle and needs no click.\n\nThe reset is the point of the row. A percentage on its own cannot be acted on — 90% used is fine\nwith a reset an hour out and a problem with three days to go — so each row carries it underneath,\nor `runs out in 40m` in the danger tone when the daemon projects the window will be exhausted\nbefore it resets.\n\nWhich form the row leads with follows Claude Code's `/usage`, which prints `Resets 3pm` on the\nsession bar and `Resets Nov 12, 3pm` on the weekly ones:\n\n| Reset is | Row shows | Hover shows |\n| --- | --- | --- |\n| under a day out | `resets in 3h 25m` | `resets at 1:35 PM` |\n| a day or more out | `resets at Nov 12, 10:00` | `resets in 1d 2h` |\n\nUnder a day, \"how long do I have\" is the actionable number. Past that, a bare `1d` is too coarse to\nplan around and a date is not — and either way the other form is one hover away.\n\n<details>\n<summary><strong>Why this is an unsupported escape hatch</strong> (read before relying on it)</summary>\n\nPaseo has no sidebar-widget contribution. A sidebar item is `{ id, title, icon, surface }` and the\nrow is rendered by the host, so the meter is a plain DOM node inserted next to that row — which\nworks only because desktop and web clients evaluate plugin client bundles inside the same renderer.\nConsequences:\n\n- **Desktop and web only.** On iOS and Android there is no DOM and the meter simply never mounts.\n- **Anchored on a host testID** (`plugin-sidebar-usage-sidebar-usage`, derived from this plugin's\n  own id). If a future Paseo release renames it, the meter stops appearing. Nothing else breaks.\n- **Fail-soft by construction.** Every step — anchor lookup, colour probing, RPC — degrades to\n  rendering nothing rather than throwing.\n- **Colours are measured, not guessed.** Theme colors reach plugins only as props inside a surface,\n  and this meter is a DOM node outside React, so it identifies the active theme from what is\n  actually painted: the sidebar background is matched against Paseo's seven built-in themes (Light,\n  Dark, Zinc, Midnight, Claude, Ghostty, Pure black), each of which paints a distinct one, and the\n  meter then uses that theme's own track and muted-foreground tokens. An unrecognized theme — a\n  plugin-contributed one — falls back to the row's rendered text colour with a light or dark status\n  palette chosen by luminance. Colours are re-probed every two seconds, so switching themes updates\n  the meter without a reload. Row-sized painted ancestors are skipped during the probe: while the\n  usage panel is open Paseo paints a selection tint on its own sidebar row, and reading that tint\n  instead of the sidebar identified the Light theme and turned the meter dark-on-dark.\n- **Never steals a click.** The node is `pointer-events:none`.\n\nTo disable the meter, remove the `startSidebarMeter(client)` call from `index.client.tsx`.\nThere is no settings toggle yet.\n\n</details>\n\n### Pinning and ordering\n\nEvery quota window in the panel carries a **+ / −** button that pins it to — or hides it from — the\nsidebar meter. Pinned rows appear in a **Sidebar order** block at the top of the panel, where they\ncan be dragged (or moved with the arrow buttons) into the exact order the meter paints them.\n\n- Until you pin anything, the meter shows every window of the first provider that reports usage.\n- A row is identified by `providerId:windowId`, not by index, so a provider that reorders its\n  windows — or temporarily drops one — never silently repoints your selection.\n- Toggling a pin updates the meter immediately rather than on its next poll: the panel and the\n  meter share one in-renderer store.\n- The pin set is persisted to `$XDG_STATE_HOME/paseo-usage-sidebar/selection.json` (default\n  `~/.local/state/…`) and written atomically. It holds provider and window **ids only** — no\n  tokens, no usage numbers, nothing account-identifying.\n\n### Localization\n\nThe panel is localized into every language Paseo ships: Arabic, English, Spanish, French, Japanese,\nKorean, Brazilian Portuguese, Russian, and Simplified Chinese. Arabic renders right-to-left.\n\nPaseo does not pass its language to plugins — `PluginHostProps` carries theme, host, and layout\nonly, and the language preference lives in client-side app settings rather than daemon config. The\nplugin therefore reproduces Paseo's own `resolveSupportedLocale` algorithm against the same\n`navigator.languages` the app reads, which matches Paseo exactly while its language is set to\n**System** (the default). If you override Paseo's language to something other than your system\nlocale, the panel follows the system locale instead.\n\nTwo notes:\n\n- Paseo's own usage copy is hardcoded English (`\"Plan usage\"`, `\"Refresh\"`, …), so on a non-English\n  install this panel is localized where **Settings → Usage** is not.\n- Window names are re-derived from the daemon's window ids (`five_hour`, `weekly`,\n  `weekly_model_fable`) and localized; only the model name inside a scoped window is kept verbatim,\n  because it is the provider's own name for it. Other provider-supplied strings (`Extra usage`,\n  plan labels) are still shown verbatim.\n\nDurations are two-unit (`2d 3h`, `3h 25m`, `40m`) and clock times come from `Intl.DateTimeFormat`,\nso they follow the locale's 12/24-hour convention rather than a hardcoded one.\n\n---\n\n## Configuration\n\n| Variable | Default | Effect |\n| --- | --- | --- |\n| `XDG_STATE_HOME` | `~/.local/state` | Where the pin set is stored. |\n\n---\n\n## How it reads usage\n\nThe plugin reads provider usage through `paseo.providers.listUsage()` from the plugin SDK, in\n`server/usage/read.ts`. The daemon's payload is validated against the plugin's own Zod mirror of\n`provider.usage.list`, so a provider reporting a window shape this plugin does not model degrades to\na missing field rather than crashing the surface.\n\nUp to 0.7 this file also carried a fallback that opened its own WebSocket to the daemon and replayed\nthe `provider.usage.list` handshake by hand, because that SDK release exposed no usage API. The\n`>=0.8.0` requirement makes it unreachable, so it is gone along with the `PASEO_USAGE_SIDEBAR_HOST`\noverride.\n\nEach provider row's footer shows the provider's own source label and how long ago the numbers were\nfetched.\n\n## Security\n\nRead this before trusting the plugin — Paseo plugins are unsandboxed by design.\n\n- **Server code** runs in a daemon subprocess. It calls one SDK method, `paseo.providers.listUsage()`,\n  and performs no other daemon operation. It opens no sockets of its own.\n- **No credentials are read, stored, or transmitted.** The plugin never touches `~/.claude`,\n  `~/.codex`, the macOS Keychain, or any provider token.\n- **No outbound network access.** Nothing leaves the machine; the plugin opens no sockets at all.\n- **One write, and it is yours.** The only file the plugin writes is the pin set described in\n  [Pinning and ordering](#pinning-and-ordering) — provider and window ids, nothing else. No config\n  is touched, no daemon state is mutated.\n- **Client code** renders the response and stores nothing.\n\n## Limitations\n\n- **Sidebar placement is host-owned.** The plugin API offers no sidebar widget, badge, or footer\n  slot — only `{ id, title, icon, surface }`. The always-visible meter is an unsupported DOM escape\n  hatch (see [above](#the-sidebar-meter)) and exists on desktop and web only.\n- **Only providers that report usage get rows.** Providers without a signed-in session are counted\n  in the footer, not rendered.\n- **Percentages are the daemon's**, including their refresh cadence. The plugin does not re-derive\n  or estimate anything, so a provider that rate-limits its own usage endpoint stays stale until\n  Paseo refreshes it.\n\n---\n\n## Project structure\n\n```\n.\n├── index.client.tsx                # Client entry — surface, sidebar item, command item, meter\n├── index.server.ts                 # Server entry — the three RPC handlers\n├── paseo-plugin.json               # Manifest (plugin id + requirements.paseo)\n├── package.json                    # Typecheck-time dependencies only\n├── tsconfig.json\n├── client/\n│   ├── i18n/locale.ts              # Mirrors Paseo's own resolveSupportedLocale\n│   ├── selection/store.ts          # In-renderer store keeping panel and meter in sync\n│   └── ui/\n│       ├── usage-surface.tsx       # The usage panel\n│       ├── sidebar-meter.ts        # The always-visible DOM meter\n│       └── sidebar-title.ts        # Localized sidebar / Command Center label\n├── server/\n│   ├── selection/state.ts          # Atomic pin-set persistence under XDG state\n│   └── usage/read.ts               # paseo.providers.listUsage(), validated\n└── shared/\n    ├── i18n/messages.ts            # Message catalog for the nine locales Paseo ships\n    ├── selection/contract.ts       # Pin-set schema, RPCs, and snapshot resolution\n    └── usage/\n        ├── contract.ts             # Zod mirror of the daemon's provider.usage.list payload\n        ├── format.ts               # Percentage, reset, age, and balance formatting\n        └── window-label.ts         # Daemon window ids → /usage-style window names\n```\n\n**Directories are load-bearing.** Paseo 0.8 builds one bundle per entry and enforces the boundary\nbetween them by directory — the pre-0.8 `*.client.ts` / `*.server.ts` filename suffixes no longer\nmean anything, and a code module left at the repo root is a compile error:\n\n| Directory | Bundle | Rules |\n| --- | --- | --- |\n| `server/` | daemon subprocess | May use `node:*`. Importing it from client code is a build error. |\n| `client/` | renderer | May use React, React Native, DOM. Importing it from server code is a build error. |\n| `shared/` | both | Contracts and pure helpers only — no platform APIs, no runtime-specific SDK entries. |\n\nSDK imports follow the same split: `@getpaseo/plugin` for runtime-neutral helpers (`defineRpc`,\n`PluginTheme`), `@getpaseo/plugin/client` and `@getpaseo/plugin/client/react-native` for client code,\n`@getpaseo/plugin/server` for server code.\n\n## Development\n\n```bash\nnpm install\nnpm run typecheck\n\npaseo plugin install \"$PWD\"\npaseo plugin reload usage-sidebar   # after editing source\npaseo plugin ls                     # expect: running, no error\npaseo plugin logs usage-sidebar\n```\n\n`npm install` only installs typecheck-time dependencies. Paseo supplies every runtime module\n(`@getpaseo/plugin`, `react`, `react-native`, `@tanstack/react-query`, `zod`), so installing the\nplugin never runs a package manager.\n\nIssues and pull requests are welcome. Please run `npm run typecheck` before opening one, and keep\nnew modules inside the `client/` / `server/` / `shared/` layout above.\n\n## License\n\n[MIT](./LICENSE)\n"}