Add felipeartur/ai-usagebar - #376
Merged
Merged
Conversation
A headless poller runs `ai-usagebar usage --json` on an interval and publishes the report to plugin state; the capsules and the panel are pure subscribers, so a second capsule or a second monitor costs no extra process. Severity and absolute reset stamps come from the CLI, so the plugin carries no vendor table and no countdown parsing.
Nest translations/en.json, add the 960x540 thumbnail and rewrite the README on the template, so validate-plugins.py passes. Fix the reset weekday: formatTime passes unknown text through verbatim, so a "ddd" prefix rendered literally. Build it with os.date instead. Shrink the panel to 380px; the footer does not stretch to the bottom, so the taller box only added empty space.
`ai-usagebar` is a declared dependency, so it is on PATH; the setting only added a row to a dialog the shell already fills with its own panel controls. Keep placement and position declared: the shell renders those rows for every plugin panel either way, and omitting them only defaults the panel to floating and adds a third row.
Bring back the two styles the QML capsule had and the shell can draw: `meter`, five segments filled in twenties, and `label`, name and percentage stacked over the bars. The gauge gets the elapsed underlay the panel already draws, so a fill longer than the clock bar reads as spend running ahead. An `extras` setting puts the time until reset, the pace against the clock, or both beside the percentage — all of it from `reset_at` and `detail`, ticking locally between CLI calls. `provider_limit` lets one capsule carry up to four providers, busiest first, with a `+N` for the rest; `auto` now means the busiest provider rather than the first one that reported. Each gets its own icon: Tabler has no Anthropic mark, so providers without a brand glyph get a semantic one. Settings keys are snake_case to match the rest of the repo, and the tags gain `indicator`.
Every runtime string now goes through noctalia.tr with keys in translations/en.json, the way most plugins in this repo do it, so the translation service has something to pick up.
A raw id in a bar list is an anonymous instance, and an anonymous instance has no per-widget settings: the gear opens empty.
The provider list moves into the panel, in the shell's own two-pane shape: every configured provider on the left with its headline percentage, the selected one's cards on the right. Row selection needs a closure per row, which is plugin_api 9.
The panel listed every vendor the CLI knows about, including the ones it has no credential for. This is a front-end: a provider that was never set up is not a row. One that is set up and failing keeps its row and its error. Colours follow the shell instead of the severity: text stays on_surface until the reading earns warning or error, and the accent lives on the bar fill. The capsule now reads like the widgets next to it in the bar.
The detail pane was implying fields it had in hand: severity was only a colour, elapsed only a two-pixel bar, status and fetch time only a relative phrase. All of it is on screen now, and a reset more than a week out carries its date instead of a weekday that could mean either week. Adds a select IPC event so a script can point the panel at one provider.
520px left a third of the box empty; the cards and the provider list both end around 380.
Error output is rendered as-is in a tooltip and in the panel, and an error can quote the request that failed, which can carry a key in its query string. It is now cleaned once, where it enters the plugin: whitespace collapsed, key, token, secret and bearer values redacted, and the line capped at 200 characters so it cannot push a bar capsule off screen. Also drops the polledAt state key, which nothing read.
Tabler has no Anthropic mark, but it has asterisk-simple, which is the shape of the Claude symbol. Better than the chat bubble it borrowed before. The README and the comments lose the em dashes, the three-part lists and the lines that sounded like slogans.
A cold call to the CLI takes a second or two, and until now nothing on screen said so. The panel's refresh button gives way to a loader while the poller is in flight, and the capsule grows a small one beside the reading. Two arcs alternate on a 220ms tick, which reads as one turning loader; the tick goes back to its normal rate as soon as the read lands. The QML capsule spun its refresh button the same way. The shell has no rotation property on a ui node, so the turn is drawn as two frames instead.
The CLI caches for a minute, so a manual refresh usually answers in about ten milliseconds and the loader never survived a frame. The poller now holds the busy state for 600ms, timed by its own tick, and goes back to the normal interval as soon as it lets go.
Two glyph frames at 4.5fps read as a stutter, not a turn, and a smooth one would cost frame ticks and a newer plugin API for a 600ms event. The loader is now drawn once and held, so the capsule and the panel stop re-rendering four times a second while a read is in flight.
Opening the panel is the refresh, so the button that asked for one is gone, and so is the close button, since clicking away already dismisses the panel. The header keeps a loader while the read is in flight. The CLI answers from its own cache on a quick reopen, so this costs nothing.
A probe of the four roles in a running panel showed warning and success falling back to plain text: this build renders tertiary and error. The high tier was therefore invisible. It now uses tertiary, which is what the QML capsule used for the same tier, and the pace, stale and status marks follow. The poller also refuses to start a process within two seconds of the last one, since opening the panel now asks for a read and a panel can be opened as fast as a pointer can click. Twenty requests in four seconds spawn one process. The thumbnail was still the single column panel from before the rewrite.
runAsync answers whether it launched anything. When it says no, no callback ever arrives, so the poller stayed in flight and stopped asking for readings until the plugin was reloaded.
There was a problem hiding this comment.
Pull request overview
Adds a new Noctalia community plugin (felipeartur/ai-usagebar) that visualizes AI provider quota/usage by polling the external ai-usagebar CLI and rendering a bar capsule plus a detailed panel UI.
Changes:
- Introduces a headless poller service to run
ai-usagebar usage --json, parse results, and publish plugin state. - Adds a bar widget capsule (multiple styles/extras) and a master/detail panel to browse provider usage windows.
- Adds plugin metadata, README documentation, and English translations for settings/UI strings.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| ai-usagebar/plugin.toml | Declares the plugin, dependency, settings, widgets, service, and panel entrypoints. |
| ai-usagebar/service.luau | Implements the poller that spawns ai-usagebar, parses JSON, and updates shared state. |
| ai-usagebar/bar.luau | Renders the bar capsule from poller state, with styles/tooltips and interactions. |
| ai-usagebar/panel.luau | Renders the detailed two-pane panel for provider selection and window cards. |
| ai-usagebar/translations/en.json | Adds English strings for settings and UI labels/messages. |
| ai-usagebar/README.md | Documents installation, usage, settings, and IPC commands for the plugin. |
Suppressed comments (1)
ai-usagebar/panel.luau:180
- The elapsed bar uses
elapsed/100without clamping, which can similarly exceed the expected [0,1] range.
progress = elapsed / 100,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| text = text:gsub("([%w_%-]*[Kk][Ee][Yy][%w_%-]*=)[^%s]+", "%1<redacted>") | ||
| text = text:gsub("([Tt][Oo][Kk][Ee][Nn][%w_%-]*=)[^%s]+", "%1<redacted>") | ||
| text = text:gsub("([Ss][Ee][Cc][Rr][Ee][Tt][%w_%-]*=)[^%s]+", "%1<redacted>") | ||
| text = text:gsub("([Bb]earer%s+)[%w%._%-]+", "%1<redacted>") |
Comment on lines
+75
to
+77
| for _, entry in ipairs(decoded.entries) do | ||
| if entry.error ~= nil then entry.error = safeText(entry.error) end | ||
| end |
| return ui.column({ gap = 1, align = "center" }, stack) | ||
| end | ||
|
|
||
| local function paceNodes(metric, tint) |
Comment on lines
+188
to
+193
| ui.progress({ progress = percent / 100, fill = tint, track = "on_surface/0.16", | ||
| radius = 3, width = width, height = 4 }), | ||
| } | ||
| if elapsed ~= nil then | ||
| stack[#stack + 1] = ui.progress({ progress = elapsed / 100, fill = "on_surface/0.45", | ||
| track = "on_surface/0.10", radius = 1, width = width, height = 2 }) |
|
|
||
| local body = { | ||
| ui.row({ gap = 6, align = "center" }, header), | ||
| ui.progress({ progress = percent / 100, fill = fill, track = "on_surface/0.16", radius = 3, height = 5 }), |
| } | ||
| if percent ~= nil and not broken then | ||
| lines[#lines + 1] = ui.progress({ | ||
| progress = percent / 100, |
Clamp every percentage before it becomes a bar width. A provider can report more than it was given, and a value outside [0,1] draws wrong. Clean every string in the report, not just the error. A plan name, an account name and a metric detail are CLI text too, and the comment above the routine claimed as much while only the error went through it. Widen the Bearer pattern to any run of non-space, since base64 tokens carry characters the old class missed. Drop the unused tint parameter from paceNodes, left over from the colour work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plugin
felipeartur/ai-usagebarplugin.toml)What it does
Puts your AI plan quota in the bar. The capsule shows the headline percentage of
a provider behind that provider's icon, and reads in the bar's own colour until
the CLI calls the window high or critical, at which point it picks up
tertiaryor
error. Four styles: a plain percentage, a gauge, five segments, or the nameand percentage stacked over the bars. Beside that it can carry the time left in
the window and the pace against the clock, and one capsule can carry up to four
providers, busiest first.
Clicking it opens a two pane panel: every provider you have set up on the left,
the selected one in detail on the right, with a card per quota window. Each card
shows the label, the severity the CLI assigned, the percentage, how much of the
window has elapsed as a second thinner bar, the time left with the clock time or
date its reset lands on, and the pace line. Credit blocks and free text rows the
CLI reports are rendered too.
The numbers come from ai-usagebar,
a Rust CLI that reads Claude, Codex, Cursor, Antigravity, Kiro, Z.AI, OpenRouter,
DeepSeek, Kimi and Grok, among others. The plugin never talks to a provider,
holds a token, or reads a credential file.
External dependencies
ai-usagebar, declared independenciesand run by name fromPATH. Oneprocess per read:
ai-usagebar usage --json. The CLI owns credentials, providerendpoints and its own caching; the plugin parses that stdout and draws it.
Testing
Running in my bar against a live Claude Pro plan and a ChatGPT Free plan, with
three more providers configured in the CLI without credentials. Exercised: all
four capsule styles and both extras;
provider_limitfrom 1 to 4; the tooltip;left click opening the panel; right click refreshing; provider selection inside
the panel;
noctalia msg panel-toggleandpanel-open; both IPC events on thepoller; the widget settings dialog and the plugin settings dialog. Providers
with no credential are filtered out of the list, and a reset more than a week
out was checked for its absolute date.
noctalia plugins lintand this repo'svalidate-plugins.pyare both clean.Screenshots / Videos
The capsule and the panel, from a running session:
https://raw.githubusercontent.com/FelipeArtur/community-plugins/ai-usagebar-plugin/ai-usagebar/thumbnail.webp
Checklist
idafter the/inplugin.tomlexactly.plugin.toml,README.md,thumbnail.webp, andtranslations/en.json.README.mdfollows theREADME template, documents
every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
thumbnail.webpwith the thumbnail generator.versionfollows semver and is bumped in this PR;plugin_apiis the oldest API level this plugin requires.understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
catalog.toml; CI generates it.Code review attestation
licensedeclared inplugin.toml.English is the only translation shipped. The plugin makes no network calls and writes no files. The only process it
spawns is
ai-usagebar usage --json, from thepollerservice, and the poller refuses to start one within twoseconds of the last. Text that comes back from the CLI is rendered, so it is first collapsed to one line, capped at
200 characters, and stripped of anything shaped like
key=,token=,secret=orBearer <value>before itreaches a tooltip.
Notes
plugin_apiis 9 because row selection in the panel uses a closure as a clickhandler. Everything else the plugin needs exists at 3.