Skip to content

Add felipeartur/ai-usagebar - #376

Merged
ItsLemmy merged 21 commits into
noctalia-dev:mainfrom
FelipeArtur:ai-usagebar-plugin
Aug 16, 2026
Merged

Add felipeartur/ai-usagebar#376
ItsLemmy merged 21 commits into
noctalia-dev:mainfrom
FelipeArtur:ai-usagebar-plugin

Conversation

@FelipeArtur

Copy link
Copy Markdown
Contributor

Plugin

  • Id: felipeartur/ai-usagebar
  • New plugin
  • Update to an existing plugin (version bumped in plugin.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 tertiary
or error. Four styles: a plain percentage, a gauge, five segments, or the name
and 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 in dependencies and run by name from PATH. One
process per read: ai-usagebar usage --json. The CLI owns credentials, provider
endpoints 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_limit from 1 to 4; the tooltip;
left click opening the panel; right click refreshing; provider selection inside
the panel; noctalia msg panel-toggle and panel-open; both IPC events on the
poller; 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 lint and this repo's
validate-plugins.py are both clean.

  • Tested on Niri
  • Tested on Hyprland
  • Tested on Sway
  • Tested on another compositor:
  • Noctalia version tested against: 5.0.0_beta.8
  • Plugin API level: 9

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

  • The directory name matches the part of id after the / in plugin.toml exactly.
  • It ships plugin.toml, README.md, thumbnail.webp, and translations/en.json.
  • README.md follows the
    README template, documents
    every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
  • I created thumbnail.webp with the thumbnail generator.

Unchecked deliberately. The thumbnail is composed from screenshots of the plugin running and exported as a
960x540 WebP, but not through the generator page. Say the word and I will re-export it there.

  • version follows semver and is bumped in this PR; plugin_api is the oldest API level this plugin requires.
  • Every non-English translation in this PR uses a locale supported by Noctalia core, and I can read, write, and
    understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
  • I did not edit catalog.toml; CI generates it.
  • This PR touches exactly one plugin directory.

Code review attestation

  • The code is readable and not obfuscated, minified, or generated.
  • It does not download and execute remote code.
  • Every network call, filesystem write, and spawned process is something the description above accounts for.
  • I have the right to publish this code under the license declared in plugin.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 the poller service, and the poller refuses to start one within two
seconds 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= or Bearer <value> before it
reaches a tooltip.

Notes

plugin_api is 9 because row selection in the panel uses a closure as a click
handler. Everything else the plugin needs exists at 3.

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.
Copilot AI lite review requested due to automatic review settings August 16, 2026 04:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/100 without 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.

Comment thread ai-usagebar/service.luau Outdated
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 thread ai-usagebar/service.luau Outdated
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
Comment thread ai-usagebar/bar.luau Outdated
return ui.column({ gap = 1, align = "center" }, stack)
end

local function paceNodes(metric, tint)
Comment thread ai-usagebar/bar.luau Outdated
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 })
Comment thread ai-usagebar/panel.luau Outdated

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 }),
Comment thread ai-usagebar/panel.luau Outdated
}
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.
@ItsLemmy
ItsLemmy merged commit d1038ba into noctalia-dev:main Aug 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants