From 335ead5a6f04926dd20e151d4605873a573e1342 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 20 Aug 2026 09:05:02 -0300 Subject: [PATCH 01/12] ai-usagebar: widen the credential scrubber safeText() gated its redaction on a literal `=` or `Bearer `, so a secret written any other way reached the screen. A CLI that fails an HTTP request tends to quote the request, and 5 of 11 realistic shapes survived: an `X-Api-Key:` header, a `{"api_key": "..."}` field, credentials in a URL's userinfo half, and a bare provider key. The gate is now the keyword. A separator is a bad one: `=` and `:` both appear in ordinary readings, so the old check ran three backtracking patterns over almost every string it saw. Measured over a realistic corpus of 165 strings the two cost the same, and the new one runs nothing at all for a plan name. tests/scrub_test.lua reads the function out of service.luau instead of copying it, so it cannot pass against a version that no longer exists. It covers the eleven secrets, twelve readings that must survive untouched, and the length cap. --- ai-usagebar/service.luau | 56 +++++++++++++--- ai-usagebar/tests/scrub_test.lua | 108 +++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 ai-usagebar/tests/scrub_test.lua diff --git a/ai-usagebar/service.luau b/ai-usagebar/service.luau index 7e92a7e4..3b55351e 100644 --- a/ai-usagebar/service.luau +++ b/ai-usagebar/service.luau @@ -17,21 +17,57 @@ end -- here, where it enters the plugin: -- an error can quote the request that failed, and a request can carry a key in -- its query string. A runaway line would also push a bar capsule off screen. +-- A secret's value runs until whitespace or the quote or brace that closes it, +-- so a JSON field loses its value and keeps its punctuation. +local SECRET_VALUE = "[^%s\"',}]+" +local SECRET_WORDS = { + "[Kk][Ee][Yy]", + "[Tt][Oo][Kk][Ee][Nn]", + "[Ss][Ee][Cc][Rr][Ee][Tt]", + "[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]", +} +-- Nine characters before the rest of a provider key, so a bare "sk-" in prose +-- is not mistaken for one. +local KEY_TAIL = string.rep("[%w_%-]", 9) + local function safeText(value) local text = noctalia.string.trim(tostring(value or "")) text = text:gsub("%s+", " ") - -- `scrub` runs this over ~165 strings per two-vendor read, and four - -- backtracking patterns on each one exhaust the callback's CPU budget, - -- which costs the whole report. All four need a literal `=` or `earer` to - -- match, so a plan name or a percentage skips them. - if text:find("=", 1, true) then - text = text:gsub("([%w_%-]*[Kk][Ee][Yy][%w_%-]*=)[^%s]+", "%1") - text = text:gsub("([Tt][Oo][Kk][Ee][Nn][%w_%-]*=)[^%s]+", "%1") - text = text:gsub("([Ss][Ee][Cc][Rr][Ee][Tt][%w_%-]*=)[^%s]+", "%1") + + -- `scrub` runs this over ~165 strings per two-vendor read, and backtracking + -- patterns on every one of them exhaust the callback's CPU budget, which + -- costs the whole report. So nothing expensive runs until a literal search + -- says it could match. The keyword is what opens the gate. A separator will + -- not do: `=` and `:` both turn up in ordinary readings, in a ratio, a clock + -- time, a URL, so gating on those ran the patterns over almost every string. + local lower = text:lower() + + if lower:find("key", 1, true) or lower:find("token", 1, true) + or lower:find("secret", 1, true) or lower:find("password", 1, true) then + for _, word in ipairs(SECRET_WORDS) do + local name = "[%w_%-]*" .. word .. "[%w_%-]*" + -- name=value: a query string or a shell assignment. + text = text:gsub("(" .. name .. "=)" .. SECRET_VALUE, "%1") + -- name: value: an HTTP header or a JSON field. + text = text:gsub("(" .. name .. "\"?%s*:%s*\"?)" .. SECRET_VALUE, "%1") + end end - if text:find("earer", 1, true) then - text = text:gsub("([Bb]earer%s+)[^%s]+", "%1") + + if lower:find("bearer", 1, true) then + text = text:gsub("([Bb][Ee][Aa][Rr][Ee][Rr]%s+)" .. SECRET_VALUE, "%1") end + + -- Credentials in the userinfo half of a URL the CLI echoed back. + if lower:find("://", 1, true) then + text = text:gsub("(://)[^%s/@]+:[^%s/@]+(@)", "%1%2") + end + + -- The provider key shape this plugin sits next to all day. Anchored at a + -- word start, so "desk-top" is not a key. + if lower:find("sk-", 1, true) then + text = text:gsub("%f[%w](sk%-)" .. KEY_TAIL .. "[%w_%-]*", "%1") + end + if #text > 200 then text = string.sub(text, 1, 200) .. "..." end return text end diff --git a/ai-usagebar/tests/scrub_test.lua b/ai-usagebar/tests/scrub_test.lua new file mode 100644 index 00000000..f7932de0 --- /dev/null +++ b/ai-usagebar/tests/scrub_test.lua @@ -0,0 +1,108 @@ +-- Redaction test for service.luau's safeText(). +-- +-- Everything the CLI writes reaches the screen, and a CLI that fails an HTTP +-- request tends to quote the request. safeText is the only thing standing +-- between that and a rendered label, so it gets a test. +-- +-- The function is read out of service.luau rather than copied here: a copy +-- would keep passing after the real one changed. +-- +-- lua tests/scrub_test.lua (or luajit) +-- +-- Run it from the plugin directory. Exits non-zero on the first failure. + +local SOURCE = "service.luau" + +local function loadSafeText() + local file = io.open(SOURCE, "r") + if file == nil then + error("run this from the plugin directory: " .. SOURCE .. " not found") + end + local source = file:read("*a") + file:close() + + -- The slice runs from the redaction constants to the end of the function. + local chunk = source:match("(local SECRET_VALUE.-\nend)\n") + if chunk == nil then + error("could not find safeText in " .. SOURCE .. "; update the markers here") + end + + -- The only host API the function touches. + local env = { + string = string, + ipairs = ipairs, + tostring = tostring, + noctalia = { string = { trim = function(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end } }, + } + local loaded = load(chunk .. "\nreturn safeText", "safeText", "t", env) + return loaded() +end + +local safeText = loadSafeText() + +-- Each case names the material that must not survive. +local SECRETS = { + { "GET /v1/usage?api_key=sk-ant-abc123456 failed", "abc123456" }, + { "request token=eyJhbGciOiJIUzI1NiJ9.SIGNATURE failed", "SIGNATURE" }, + { "client_secret=hunter2 rejected", "hunter2" }, + { "Authorization: Bearer sk-ant-api03-REALKEY", "REALKEY" }, + { '{"api_key": "sk-ant-api03-REALKEY"}', "REALKEY" }, + { '{"token":"eyJhbGciOiJIUzI1NiJ9.PAYLOAD.SIG"}', "PAYLOAD" }, + { "-H 'X-Api-Key: sk-ant-api03-REALKEY'", "REALKEY" }, + { "curl https://user:hunter2@api.anthropic.com/v1/usage", "hunter2" }, + { "authorization: bearer sk-ant-api03-REALKEY", "REALKEY" }, + { "OPENAI_API_KEY sk-proj-REALKEYVALUE not accepted", "REALKEY" }, + { "password=hunter2", "hunter2" }, +} + +-- Readings the plugin draws every minute. A scrubber that eats these is worse +-- than the leak it prevents. +local BENIGN = { + "Claude Pro", + "Session (5h)", + "Weekly (7d)", + "69% of the window elapsed", + "Resets in 4h 01m at 12:40", + "62% of monthly limit consumed", + "10pts under", + "https://github.com/akitaonrails/ai-usagebar", + "ai-usagebar exited with code 2", + "2026-08-20T11:29:59.872624Z", + "Desk-top mode", + "ChatGPT Free", +} + +local failures = 0 + +local function fail(message) + failures = failures + 1 + io.write("FAIL ", message, "\n") +end + +for _, case in ipairs(SECRETS) do + local input, material = case[1], case[2] + local output = safeText(input) + if output:find(material, 1, true) then + fail(material .. " survived: " .. output) + end +end + +for _, input in ipairs(BENIGN) do + local output = safeText(input) + if output ~= input then + fail("mangled a normal reading: " .. input .. " -> " .. output) + end +end + +-- A runaway line would push a bar capsule off the screen. +local long = safeText(string.rep("x", 500)) +if #long > 210 then + fail("long text was not capped: " .. #long .. " characters") +end + +if failures > 0 then + io.write(failures, " failure(s)\n") + os.exit(1) +end + +io.write("ok: ", #SECRETS, " secrets redacted, ", #BENIGN, " readings untouched, length capped\n") From 9ddfa48ead489e5b7f18254df9bdfcb80fdd89f4 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 20 Aug 2026 09:05:20 -0300 Subject: [PATCH 02/12] ai-usagebar: rework the capsule and panel, v1.2.0 The readings now line up. Every percentage is right-aligned in a fixed column, so a stack of cards reads as one ruler and the bar capsule keeps its width from 9% to 100% instead of nudging its neighbours on every read. Capsule: a read in flight dims the row. It used to append a spinner, which shoved every widget to its right once per cycle. A failure draws the plugin glyph in the error colour; the old pair of glyphs read as two problems. Panel: - Selection is a tint. A filled `primary` row had to invert every colour inside it and shouted over the reading it was meant to mark. - Severity ships a word next to the colour, so the tier is readable without separating two accents. - The time story is one line: what is left of the window, when it lands, how much is gone, and whether the spend is running ahead. - `ui.button` for the header refresh and the error actions, replacing rows hand-built to look like buttons. The refresh button becomes the spinner in place. - Skeletons while the first read lands, and an empty state that names what is missing. - Dropped the provider id and a "ready" status from the detail pane. The id is the row that was just clicked and a healthy read is the default. Two layout bugs came out of testing it against the running shell. The root row had no flexGrow, so neither pane was given a bounded height and their ui.scroll children asked for their natural one, which clipped the cards. A bare ui.column also takes a column's free space for itself, which parked the detail title above a hundred pixels of nothing; the wrapping row that prevents it is back, with a comment saying why it is there. textRole and barRole differed only in their resting colour and existed in both entries. They are one severityRole(x, calm). The two skeleton shapes are one. 25 lines lighter. --- ai-usagebar/README.md | 29 +-- ai-usagebar/bar.luau | 42 ++--- ai-usagebar/panel.luau | 307 ++++++++++++++++++++----------- ai-usagebar/plugin.toml | 2 +- ai-usagebar/translations/en.json | 7 +- 5 files changed, 244 insertions(+), 143 deletions(-) diff --git a/ai-usagebar/README.md b/ai-usagebar/README.md index 840a493c..5fe25c0c 100644 --- a/ai-usagebar/README.md +++ b/ai-usagebar/README.md @@ -24,9 +24,9 @@ tarballs on the project's GitHub Releases page. Configure your providers once in `~/.config/ai-usagebar/config.toml`; the CLI owns the credentials and the endpoints, and this plugin never sees them. -`xdg-open` is optional. It is spawned by one row in the panel, the link to the -CLI's project page offered when `ai-usagebar` is not on `PATH`. Without -xdg-utils that row does nothing and the rest of the plugin is unaffected. +`xdg-open` is optional. It is spawned by one button in the panel, the link to +the CLI's project page offered when `ai-usagebar` is not on `PATH`. Without +xdg-utils that button is not drawn and the rest of the plugin is unaffected. ## Usage @@ -81,21 +81,22 @@ with its headline percentage. On the right is the selected one in detail: one card per reported metric, with a quota bar over a thinner "window elapsed" bar, so a fill that outruns the clock bar means quota is burning ahead of pace. Credit balances and free text rows the CLI reports get rendered as well. -Opening the panel asks the CLI for fresh numbers, and the header says how old -the reading is. There is no refresh button and no close button: the read -happens on open, and the panel closes when you click away from it or press the -same widget again. +Opening the panel asks the CLI for fresh numbers, and the detail pane says how +old the reading is. The refresh button in the header asks again; it turns into +a spinner while the CLI is answering. There is no close button: the panel +closes when you click away from it or press the same widget again. The list follows the CLI. A provider that `ai-usagebar` has no credential for never appears, while one that is set up and failing keeps its row and shows the error. -The detail pane spells out everything the CLI reports for that provider instead -of implying it: the plan and account name, the provider id, its status, a stale -flag when the reading is old, and when it was fetched. Each window gets its -label, the severity the CLI assigned it, the percentage, the raw value string -when that says more than the percentage, how much of the window has elapsed, the -time left with the clock time (or date) its reset lands on, and the pace line. +The detail pane spells out what the CLI reports for that provider instead of +implying it: the plan and account name, when it was fetched, a stale flag when +the reading is old, and the status when it is anything other than a healthy +read. Each window gets its label, the percentage, the raw value string when +that says more than the percentage, how much of the window has elapsed, the +time left with the clock time (or date) its reset lands on, the pace line, and +the severity as a word whenever the CLI calls the window high or critical. Credit blocks and free text rows appear as the CLI writes them. To open the panel from a terminal: @@ -148,7 +149,7 @@ noctalia msg plugin felipeartur/ai-usagebar:poller all select anthropic it knows arrives on that command's stdout. - A provider that fails still comes back as an entry with `status = "error"`, so one broken provider does not blank the others. A reading the CLI marks stale - keeps showing, flagged in the capsule and in the panel header. + keeps showing, flagged in the capsule and in the panel's detail pane. - The file watcher follows the `.luau` entries only, so the files in `translations/` are read once, when the plugin loads. Editing a string takes a reload before the new text shows up: diff --git a/ai-usagebar/bar.luau b/ai-usagebar/bar.luau index eb8a992e..210eea44 100644 --- a/ai-usagebar/bar.luau +++ b/ai-usagebar/bar.luau @@ -166,20 +166,14 @@ end -- would be a second source of truth. Text stays in the bar's own colour until -- the reading is high or critical, and the accent colour is used on the bar -- fill only. -local function textRole(metric) - if not colorByUsage then return "on_surface" end +-- `calm` is the colour when the CLI has raised nothing. With the tint switched +-- off it is the colour for everything. +local function severityRole(metric, calm) + if not colorByUsage then return calm end local severity = metric ~= nil and tostring(metric.severity or "") or "" if severity == "critical" then return "error" end if severity == "high" then return "tertiary" end - return "on_surface" -end - -local function barRole(metric) - if not colorByUsage then return "primary" end - local severity = metric ~= nil and tostring(metric.severity or "") or "" - if severity == "critical" then return "error" end - if severity == "high" then return "tertiary" end - return "primary" + return calm end local function shortName(entry) @@ -237,12 +231,15 @@ end -- appended to whatever it produced. local function chip(entry) local metric = headline(entry) - local tint = textRole(metric) - local fill = barRole(metric) + local tint = severityRole(metric, "on_surface") + local fill = severityRole(metric, "primary") local percent = metric ~= nil and tonumber(metric.percent) or nil local text = percent ~= nil and string.format("%d%%", percent) or "—" local glyph = ui.glyph({ name = GLYPHS[tostring(entry.id)] or "brain", size = 13, color = tint }) - local pct = ui.label({ text = text, fontSize = 11, fontWeight = "semibold", color = tint, maxLines = 1 }) + -- Right-aligned in a fixed column, so the capsule is the same width at 9% + -- as at 100% and stops nudging its neighbours on the bar once per read. + local pct = ui.label({ text = text, fontSize = 11, fontWeight = "semibold", color = tint, + maxLines = 1, width = 30, textAlign = "end" }) local name = showName and ui.label({ text = shortName(entry), fontSize = 11, color = "on_surface_variant", maxLines = 1 }) or nil @@ -346,21 +343,22 @@ local function render() children[#children + 1] = chip(entry) end - if polling then - children[#children + 1] = ui.glyph({ name = "loader-2", size = 11, color = "on_surface_variant" }) - end - if #children == 0 then - children[1] = ui.row({ gap = 4, align = "center" }, { - ui.glyph({ name = "brain", size = 13, color = "on_surface_variant" }), - ui.glyph({ name = "alert-circle", size = 12, color = "error" }), + -- One glyph, coloured by the state. A second icon beside it reads as a + -- second problem, and the plugin's own mark in the error colour says + -- the same thing in the space of one. + children[1] = ui.glyph({ + name = "brain", size = 13, + color = failure.code ~= "" and "error" or "on_surface_variant", }) elseif hidden > 0 then children[#children + 1] = ui.label({ text = "+" .. tostring(hidden), fontSize = 10, color = "on_surface_variant", maxLines = 1 }) end - barWidget.render(ui.row({ gap = 7, align = "center" }, children)) + -- A read in flight dims the capsule rather than appending a spinner to it: + -- a node that comes and goes every cycle shoves every widget to its right. + barWidget.render(ui.row({ gap = 6, align = "center", opacity = polling and 0.55 or 1 }, children)) barWidget.setTooltip(tooltip(picked, hidden)) end diff --git a/ai-usagebar/panel.luau b/ai-usagebar/panel.luau index 0a94d839..11982edc 100644 --- a/ai-usagebar/panel.luau +++ b/ai-usagebar/panel.luau @@ -62,20 +62,14 @@ local function resetClock(section) return os.date("%a", at) .. " " .. clock end --- Text stays on the surface colour until the CLI calls the window high or --- critical. The accent colour is used on the bar fill only. -local function textRole(section) +-- The CLI tiers every percentage; copying its thresholds here would be a second +-- source of truth. `calm` is what to use when it has raised nothing: text stays +-- on the surface colour, and the accent is kept for bar fills. +local function severityRole(section, calm) local severity = tostring(section and section.severity or "") if severity == "critical" then return "error" end if severity == "high" then return "tertiary" end - return "on_surface" -end - -local function barRole(section) - local severity = tostring(section and section.severity or "") - if severity == "critical" then return "error" end - if severity == "high" then return "tertiary" end - return "primary" + return calm end -- The CLI reports a vendor it has no credential for as a `credentials error`. @@ -159,37 +153,49 @@ end -- ── Cards ───────────────────────────────────────────────────────────────────── +-- A severity word, and only when the CLI raised one. Colour on its own leaves +-- the reading to anyone who can tell the two accents apart. +local function severityWord(section) + local severity = tostring(section and section.severity or "") + if severity ~= "high" and severity ~= "critical" then return nil end + return noctalia.tr("ui.severity." .. severity) +end + local function metricCard(section) local percent = tonumber(section.percent) or 0 - local tint = textRole(section) - local fill = barRole(section) + local tint = severityRole(section, "on_surface") + local fill = severityRole(section, "primary") local value = tostring(section.value or ""):gsub(" of ", " / ") -- Only worth a column of its own when it says more than the percentage. local showValue = value ~= "" and value ~= string.format("%d%%", percent) local header = { - ui.glyph({ name = metricIcon(section.label), size = 14, color = tint }), - ui.label({ text = tostring(section.label or ""), fontSize = 11, color = "on_surface_variant" }), - ui.label({ - text = tostring(section.severity or ""), - fontSize = 9, fontWeight = "semibold", color = tint, - visible = tostring(section.severity or "") ~= "", - }), + ui.glyph({ name = metricIcon(section.label), size = 14, color = "on_surface_variant" }), + ui.label({ text = tostring(section.label or ""), fontSize = 12, fontWeight = "semibold", + color = "on_surface", maxLines = 1 }), ui.spacer({ flexGrow = 1 }), } + local word = severityWord(section) + if word ~= nil then + header[#header + 1] = ui.label({ text = word, fontSize = 10, fontWeight = "semibold", color = tint }) + end if showValue then - header[#header + 1] = ui.label({ text = value, fontSize = 11, color = "on_surface_variant" }) + header[#header + 1] = ui.label({ text = value, fontSize = 11, color = "on_surface_variant", maxLines = 1 }) end + -- Every card ends on the same right edge, so a column of them reads as one + -- ruler instead of a ragged margin. header[#header + 1] = ui.label({ text = string.format("%d%%", percent), fontSize = 15, fontWeight = "bold", color = tint, + width = 46, + textAlign = "end", }) local body = { ui.row({ gap = 6, align = "center" }, header), - ui.progress({ progress = ratio(percent), fill = fill, track = "on_surface/0.16", radius = 3, height = 5 }), + ui.progress({ progress = ratio(percent), fill = fill, track = "on_surface/0.16", radius = 3, height = 6 }), } -- Two readings: quota spent above, window elapsed below. A shorter clock bar @@ -201,26 +207,39 @@ local function metricCard(section) fill = "on_surface/0.45", track = "on_surface/0.10", radius = 2, - height = 2, - }) - body[#body + 1] = ui.label({ - text = noctalia.tr("ui.elapsed", { percent = elapsed }), - fontSize = 10, color = "on_surface_variant", + height = 3, }) end + -- One line under the bars carries the whole time story: what is left of the + -- window, when it lands, how much of it is gone, and whether the spend is + -- running ahead. Four separate lines said the same thing four times taller. local left = countdown(section) local clock = resetClock(section) local paceText, paceColor = pace(section.detail) - if left ~= "" or paceText ~= "" then - local footer = {} - if left ~= "" then - footer[#footer + 1] = ui.glyph({ name = "clock", size = 12, color = "on_surface_variant" }) - footer[#footer + 1] = ui.label({ text = left, fontSize = 11, color = "on_surface_variant" }) - if clock ~= "" then - footer[#footer + 1] = ui.label({ text = clock, fontSize = 11, fontWeight = "bold", color = "primary" }) - end + local footer = {} + if left ~= "" then + footer[#footer + 1] = ui.glyph({ name = "clock", size = 12, color = "on_surface_variant" }) + footer[#footer + 1] = ui.label({ text = left, fontSize = 11, color = "on_surface_variant" }) + if clock ~= "" then + -- Parenthesised and muted: it is the time the countdown beside it + -- lands on, not a reading of its own. In the accent colour it was + -- the loudest thing in the card after the percentage. + footer[#footer + 1] = ui.label({ + text = "(" .. clock .. ")", fontSize = 11, color = "on_surface_variant", + }) end + end + if elapsed ~= nil then + if #footer > 0 then + footer[#footer + 1] = ui.label({ text = "·", fontSize = 11, color = "on_surface_variant" }) + end + footer[#footer + 1] = ui.label({ + text = noctalia.tr("ui.elapsed", { percent = elapsed }), + fontSize = 11, color = "on_surface_variant", maxLines = 1, + }) + end + if #footer > 0 or paceText ~= "" then footer[#footer + 1] = ui.spacer({ flexGrow = 1 }) if paceText ~= "" then footer[#footer + 1] = ui.label({ text = paceText, fontSize = 11, fontWeight = "semibold", color = paceColor }) @@ -239,19 +258,24 @@ end local function blockCard(section) local body = { ui.row({ gap = 6, align = "center" }, { - ui.glyph({ name = metricIcon(section.label), size = 14, color = "primary" }), - ui.label({ text = tostring(section.label or ""), fontWeight = "bold", color = "on_surface" }), + ui.glyph({ name = metricIcon(section.label), size = 14, color = "on_surface_variant" }), + ui.label({ text = tostring(section.label or ""), fontSize = 12, fontWeight = "semibold", + color = "on_surface", maxLines = 1 }), }), } for _, line in ipairs(section.body or {}) do local text = noctalia.string.trim(tostring(line)) + -- A line the CLI left as a bare "balance:" reads as a row that failed + -- to render. Nothing is a value, and it is spelled the same way here as + -- it is everywhere else in the panel. + if text:find(":$") then text = text .. " —" end body[#body + 1] = ui.label({ text = text ~= "" and text or "—", fontSize = 11, color = "on_surface_variant", }) end - return ui.column({ gap = 4, padding = 10, radius = 8, fill = "surface_variant" }, body) + return ui.column({ gap = 4, padding = 10, radius = 8, fill = "surface_variant/0.45" }, body) end local function textRow(section) @@ -295,17 +319,21 @@ local function providerRow(entry, selected) local metric = headline(entry) local percent = metric ~= nil and tonumber(metric.percent) or nil local broken = entry.status == "error" - local tint = selected and "on_primary" or textRole(metric) - local fill = selected and "on_primary" or barRole(metric) - local muted = selected and "on_primary" or "on_surface_variant" + local tint = severityRole(metric, "on_surface") + -- The reading keeps its own severity colour whether or not the row is + -- selected. A selected row that recolours its number hides the one thing + -- the list exists to compare. local right if broken then - right = ui.glyph({ name = "alert-circle", size = 14, color = selected and "on_primary" or "error" }) + right = ui.row({ width = 34, justify = "end" }, { + ui.glyph({ name = "alert-circle", size = 14, color = "error" }), + }) else right = ui.label({ text = percent ~= nil and string.format("%d%%", percent) or "—", fontSize = 13, fontWeight = "bold", color = tint, + width = 34, textAlign = "end", }) end @@ -313,25 +341,30 @@ local function providerRow(entry, selected) ui.label({ text = tostring(entry.display_name or entry.id), fontSize = 12, fontWeight = "semibold", - color = selected and "on_primary" or "on_surface", maxLines = 1, + color = selected and "primary" or "on_surface", maxLines = 1, }), } if percent ~= nil and not broken then lines[#lines + 1] = ui.progress({ progress = ratio(percent), - fill = fill, - track = selected and "on_primary/0.25" or "on_surface/0.16", + fill = severityRole(metric, "primary"), + track = "on_surface/0.16", radius = 2, height = 3, }) end lines[#lines + 1] = ui.label({ text = broken and noctalia.tr("ui.unavailable") or tostring(entry.plan or entry.id or ""), - fontSize = 10, color = muted, maxLines = 1, + fontSize = 10, color = "on_surface_variant", maxLines = 1, }) return ui.row({ + -- Keyed, so the click handler survives the second tick the countdowns + -- ride on rather than being rebuilt under the pointer once a second. + key = "provider-" .. tostring(entry.id), gap = 8, align = "center", padding = 8, radius = 8, - fill = selected and "primary" or "surface_variant", + -- Selection is a tint, not a slab of accent: a filled `primary` row has + -- to invert every colour inside it, and then it shouts over the reading. + fill = selected and "primary/0.14" or "surface_variant/0.45", onClick = function() -- currentEntry() reads this back, so the panel and the capsule that -- opened it stay on the same provider. @@ -339,7 +372,8 @@ local function providerRow(entry, selected) render() end, }, { - ui.glyph({ name = GLYPHS[tostring(entry.id)] or "brain", size = 16, color = tint }), + ui.glyph({ name = GLYPHS[tostring(entry.id)] or "brain", size = 16, + color = selected and "primary" or "on_surface_variant" }), ui.column({ gap = 3, flexGrow = 1 }, lines), right, }) @@ -347,14 +381,8 @@ end -- ── Render ──────────────────────────────────────────────────────────────────── -local function actionRow(glyph, text, onClick) - return ui.row({ - gap = 5, align = "center", padding = 6, radius = 6, - fill = "surface_variant", onClick = onClick, - }, { - ui.glyph({ name = glyph, size = 12, color = "primary" }), - ui.label({ text = text, fontSize = 11, color = "primary" }), - }) +local function requestRefresh() + noctalia.state.set("command", { action = "refresh", at = os.time() }) end -- The failure and the suggested fix read first; the CLI's own words come last @@ -375,20 +403,47 @@ local function errorBlock() }) end - children[#children + 1] = actionRow("refresh", noctalia.tr("ui.retry"), function() - noctalia.state.set("command", { action = "refresh", at = os.time() }) - end) + -- The shell's own button, so a retry here looks like every other retry in + -- Noctalia and follows the user's theme without being told to. + local actions = { + ui.button({ + text = noctalia.tr("ui.retry"), glyph = "refresh", + variant = "outline", controlSize = "sm", + enabled = not polling, + onClick = requestRefresh, + }), + } -- Retrying is pointless until the CLI exists, so that one failure gets the -- install page as well. The URL is a literal, so there is nothing to quote, - -- and the row is only offered where something can open it. + -- and the button is only offered where something can open it. It reads as a + -- label with the address in its tooltip: a raw URL is not a button caption. if failure.code == "not_installed" and HAS_OPENER then - children[#children + 1] = actionRow("external-link", "github.com/akitaonrails/ai-usagebar", function() - noctalia.runAsync("xdg-open https://github.com/akitaonrails/ai-usagebar") - end) + actions[#actions + 1] = ui.button({ + text = noctalia.tr("ui.install"), glyph = "external-link", + variant = "ghost", controlSize = "sm", + tooltip = "github.com/akitaonrails/ai-usagebar", + onClick = function() + noctalia.runAsync("xdg-open https://github.com/akitaonrails/ai-usagebar") + end, + }) end + children[#children + 1] = ui.row({ gap = 6, align = "center" }, actions) - return ui.column({ gap = 6 }, children) + return ui.column({ gap = 8 }, children) +end + +-- A muted stand-in at the shape of what is coming, so a cold read is not a +-- spinner parked where the content is about to land. One shape serves both +-- panes: it is a placeholder, and two kinds of placeholder is one too many. +local function skeleton(key) + return ui.column({ + key = "skeleton-" .. key, + gap = 6, padding = 10, radius = 8, fill = "surface_variant/0.45", + }, { + ui.box({ width = 96, height = 10, radius = 3, fill = "on_surface/0.10" }), + ui.box({ height = 4, radius = 2, fill = "on_surface/0.06" }), + }) end local function listPane(entry) @@ -399,17 +454,26 @@ local function listPane(entry) end end if #rows == 0 then - rows[1] = ui.label({ text = noctalia.tr("ui.loading"), fontSize = 11, color = "on_surface_variant" }) + for index = 1, 3 do rows[index] = skeleton("row-" .. index) end end return ui.column({ gap = 10, padding = 14, width = 250 }, { ui.row({ gap = 8, align = "center" }, { ui.glyph({ name = "brain", size = 18, color = "primary" }), - ui.label({ text = noctalia.tr("ui.title"), fontSize = 15, fontWeight = "bold", color = "primary" }), + -- The accent belongs to the selection and the bars. A title that + -- takes it too leaves the panel with no quiet level to fall back to. + ui.label({ text = noctalia.tr("ui.title"), fontSize = 15, fontWeight = "bold", color = "on_surface" }), ui.spacer({ flexGrow = 1 }), - -- The panel refreshes when it opens, so the header only has to - -- show whether that read is still running. - ui.glyph({ name = "loader-2", size = 16, color = "primary", visible = polling }), + -- One slot for the read: the button becomes the spinner while the + -- CLI answers, rather than a second glyph appearing beside it and + -- pushing the header around once a cycle. + ui.button({ + glyph = polling and "loader-2" or "refresh", + variant = "ghost", controlSize = "sm", + tooltip = noctalia.tr("ui.refresh"), + enabled = not polling, + onClick = requestRefresh, + }), }), ui.scroll({ gap = 6, flexGrow = 1 }, rows), }) @@ -424,44 +488,60 @@ local function detailPane(entry) if subtitle == title then subtitle = "" end end - local children = { - ui.row({ gap = 8, align = "center" }, { + -- No entry yet means the skeletons below are the whole pane. A title here + -- would only repeat the one the list pane is already showing. + local children = {} + if entry ~= nil then + -- The row keeps the title block honest about its height. A bare + -- ui.column dropped into a column takes the pane's free space for + -- itself, which parks the title at the top of a hundred pixels of + -- nothing and pushes the rest of the header down. Wrapped, the block is + -- only as tall as the two labels in it. + children[#children + 1] = ui.row({ gap = 8, align = "center" }, { ui.column({ gap = 0, flexGrow = 1 }, { - ui.label({ text = title, fontSize = 15, fontWeight = "bold", color = "on_surface" }), - ui.label({ text = subtitle, fontSize = 11, color = "on_surface_variant", visible = subtitle ~= "" }), + ui.label({ text = title, fontSize = 15, fontWeight = "bold", + color = "on_surface", maxLines = 1 }), + ui.label({ text = subtitle, fontSize = 11, color = "on_surface_variant", + maxLines = 1, visible = subtitle ~= "" }), }), - }), - } + }) + end - -- The entry's own fields, spelled out rather than implied by a colour. + -- What the entry says about itself, in words rather than a colour. The + -- provider id and a "ready" status are the plugin talking to itself: the id + -- is the row that was just clicked, and a healthy read is the default. if entry ~= nil then - local chips = { - ui.label({ text = tostring(entry.id or ""), fontSize = 10, color = "on_surface_variant" }), - ui.label({ text = "·", fontSize = 10, color = "on_surface_variant" }), - ui.label({ - text = tostring(entry.status or ""), - fontSize = 10, - color = entry.status == "ready" and "on_surface_variant" or "error", - }), - } + local chips = {} + local function separate() + if #chips > 0 then + chips[#chips + 1] = ui.label({ text = "·", fontSize = 10, color = "on_surface_variant" }) + end + end + if entry.status ~= "ready" then + chips[#chips + 1] = ui.label({ + text = tostring(entry.status or ""), fontSize = 10, color = "error", maxLines = 1, + }) + end if entry.stale == true then - chips[#chips + 1] = ui.label({ text = "·", fontSize = 10, color = "on_surface_variant" }) + separate() chips[#chips + 1] = ui.label({ text = noctalia.tr("ui.stale"), fontSize = 10, color = "tertiary" }) end local fetched = parseIso(entry.fetched_at) if fetched ~= nil then - chips[#chips + 1] = ui.spacer({ flexGrow = 1 }) + separate() chips[#chips + 1] = ui.glyph({ name = "clock", size = 11, color = "on_surface_variant" }) chips[#chips + 1] = ui.label({ text = updatedText(entry) .. " · " .. noctalia.formatTime(noctalia.timeFormat(), fetched), fontSize = 10, color = "on_surface_variant", }) end - children[#children + 1] = ui.row({ gap = 4, align = "center" }, chips) + if #chips > 0 then + children[#children + 1] = ui.row({ gap = 4, align = "center" }, chips) + end end - local status = entry == nil and noctalia.tr("ui.loading") - or entry.status == "error" and tostring(entry.error or noctalia.tr("ui.unavailable")) + local status = entry ~= nil and entry.status == "error" + and tostring(entry.error or noctalia.tr("ui.unavailable")) or nil if status then children[#children + 1] = ui.label({ text = status, fontSize = 11, color = "on_surface_variant" }) end @@ -478,8 +558,18 @@ local function detailPane(entry) end if #cards > 0 then children[#children + 1] = ui.scroll({ gap = 8, flexGrow = 1 }, cards) - else + elseif entry ~= nil then + -- A provider with nothing to draw says so. Half an empty panel is not + -- an answer to the question the panel was opened to answer. + children[#children + 1] = ui.row({ gap = 6, align = "center" }, { + ui.glyph({ name = "info-circle", size = 14, color = "on_surface_variant" }), + ui.label({ text = noctalia.tr("ui.no_usage"), fontSize = 11, color = "on_surface_variant" }), + }) children[#children + 1] = ui.spacer({ flexGrow = 1 }) + else + children[#children + 1] = ui.column({ gap = 8, flexGrow = 1 }, { + skeleton("card-1"), skeleton("card-2"), + }) end return ui.column({ gap = 10, padding = 14, flexGrow = 1 }, children) @@ -487,27 +577,34 @@ end function render() local entry = currentEntry() - -- A failure replaces the report rather than sitting above it. The numbers - -- are from a read that is no longer happening, and leaving them up puts a - -- provider list and a percentage next to an alert saying neither can be - -- trusted. + -- A failure replaces the report. The numbers are from a read that is no + -- longer happening, and leaving them up puts a provider list and a + -- percentage next to an alert saying neither can be trusted. if failure.code ~= "" then - -- The panel keeps the fixed size the manifest gives it, so the block - -- is width-bounded rather than stretched across 720px of button. - panel.render(ui.column({ gap = 10, padding = 14, width = 320 }, { - ui.row({ gap = 8, align = "center" }, { - ui.glyph({ name = "brain", size = 18, color = "primary" }), - ui.label({ text = noctalia.tr("ui.title"), fontSize = 15, fontWeight = "bold", color = "primary" }), + -- The panel keeps the fixed size the manifest gives it, and a failure + -- has nowhere near 720x400 of things to say. The block stays bounded to + -- a readable width and sits in the middle of the panel, where an empty + -- surround reads as composition instead of a half-drawn frame. + panel.render(ui.column({ flexGrow = 1, padding = 14, align = "center", justify = "center" }, { + ui.column({ gap = 10, width = 320 }, { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = "brain", size = 18, color = "primary" }), + ui.label({ text = noctalia.tr("ui.title"), fontSize = 15, + fontWeight = "bold", color = "on_surface" }), + }), + errorBlock(), }), - errorBlock(), })) return end - panel.render(ui.row({ gap = 0 }, { + -- Both panes have to be told to fill the panel, or their ui.scroll children + -- ask for their natural height instead of the height they were given: the + -- cards then overflow the panel and the free space is handed to whatever + -- else in the column will take it, which pushes the header away from them. + panel.render(ui.row({ gap = 0, flexGrow = 1, align = "stretch" }, { listPane(entry), - -- ui.separator is horizontal only; a one-pixel column is the divider. - ui.column({ width = 1, fill = "on_surface/0.12" }, {}), + ui.separator({ orientation = "vertical", color = "outline", opacity = 0.28 }), detailPane(entry), })) end diff --git a/ai-usagebar/plugin.toml b/ai-usagebar/plugin.toml index f4ae1437..0dc13313 100644 --- a/ai-usagebar/plugin.toml +++ b/ai-usagebar/plugin.toml @@ -1,6 +1,6 @@ id = "felipeartur/ai-usagebar" name = "AI Usage" -version = "1.1.0" +version = "1.2.0" plugin_api = 9 author = "felipeartur" license = "MIT" diff --git a/ai-usagebar/translations/en.json b/ai-usagebar/translations/en.json index 6127d2ce..4c0f0e98 100644 --- a/ai-usagebar/translations/en.json +++ b/ai-usagebar/translations/en.json @@ -77,11 +77,16 @@ }, "hidden_label": "Not shown", "hidden_value": "{count} more, click to open the panel", - "loading": "Loading…", + "install": "Install page", "no_usage": "No usage reported", "not_configured": "`{vendor}` is not configured in ai-usagebar", "now": "now", + "refresh": "Refresh now", "retry": "Try again", + "severity": { + "critical": "critical", + "high": "high" + }, "stale": "stale", "stale_hint": "showing last known data", "title": "AI Usage", From 32700dd984fe7f27b49da51370c8e8ea0cd75db0 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 20 Aug 2026 09:05:32 -0300 Subject: [PATCH 03/12] ai-usagebar: retake the thumbnail against the new panel The card was run through the official generator in 5d8b559, but the image it was given was already a composed card with its own title and subtitle. The generator nested that inside its frame, so the name and the description appeared twice and the inner copy was too small to read. Same frame and the same title, tag and accent it was given there. The payload is now a plain screenshot of the panel, cropped to the geometry the compositor reports for the panel layer. --- ai-usagebar/thumbnail.webp | Bin 51412 -> 29280 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ai-usagebar/thumbnail.webp b/ai-usagebar/thumbnail.webp index 6a5ca136aba3eec8d60944be5001fd331a8d6908..d3410ad72195e45a53020fbd1a79f7d2e1157a1e 100644 GIT binary patch literal 29280 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

K5^e~bRs_sD$u$!5j6k> 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 XoA$vnz4oL2%N6B9D@EnVOGW?y$=E*W From 7744b37f2ee4dff1f073e29bf8f4bada65b0fdbc Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 20 Aug 2026 09:12:42 -0300 Subject: [PATCH 04/12] ai-usagebar: share the helpers, drop the busy hold Two entries kept their own copy of the ISO parsing, the duration and clock formatting, the provider glyphs, the severity tiers and the clamp, because require() needs plugin_api 22 and the manifest asked for 9. It asks for 22 now. That is the cost of this commit: the plugin stops installing on a shell older than the one that shipped API 22. shared.luau holds the copies that were identical. resetClock was not: the capsule's version named only a weekday, so a reset three weeks out read as "Sat 02:00" and named no particular Saturday. Both entries use the panel's version, which falls back to a date once a weekday stops being enough, so that is a fix to the capsule tooltip as well as a merge. severityRole stays wrapped in bar.luau, where color_by_usage can still turn the whole thing off, and delegates the thresholds. The busy hold is gone with it: MIN_BUSY_MS, the pending-clear bookkeeping and the 120 ms tick existed to keep `polling` true for 600 ms so a spinner could be seen when the CLI answers from its cache in about ten. The capsule dims now and the panel button swaps glyph in place, and a cold read takes long enough to show either without help. 1055 lines to 1178 across four files, but 123 of those are the new module and its header; the two entries lost 176 lines between them. --- ai-usagebar/bar.luau | 106 +++--------------------------------- ai-usagebar/panel.luau | 97 ++------------------------------- ai-usagebar/plugin.toml | 2 +- ai-usagebar/service.luau | 31 ++--------- ai-usagebar/shared.luau | 112 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 219 deletions(-) create mode 100644 ai-usagebar/shared.luau diff --git a/ai-usagebar/bar.luau b/ai-usagebar/bar.luau index 210eea44..ec96c409 100644 --- a/ai-usagebar/bar.luau +++ b/ai-usagebar/bar.luau @@ -14,87 +14,16 @@ local colorByUsage = noctalia.getConfig("color_by_usage") ~= false local report = nil local polling = false --- The poller names the failure, so the capsule only has a code to translate. --- Anything else in that state slot reads as no failure at all. -local NO_FAILURE = { code = "", detail = "" } - -local function asFailure(value) - return type(value) == "table" and value or NO_FAILURE -end +local shared = require("./shared.luau") +local GLYPHS, parseIso = shared.GLYPHS, shared.parseIso +local countdown, resetClock = shared.countdown, shared.resetClock +local ratio, headline = shared.ratio, shared.headline +local NO_FAILURE, asFailure = shared.NO_FAILURE, shared.asFailure local failure = NO_FAILURE --- Tabler has no Anthropic mark, so providers without a brand glyph get a --- semantic one. Same approach the other CLI-backed meters in this repo take. -local GLYPHS = { - anthropic = "asterisk-simple", - anthropic_api = "asterisk-simple", - openai = "brand-openai", - zai = "bolt", - openrouter = "route", - deepseek = "fish", - kimi = "moon", - moonshot = "moon", - kilo = "robot", - novita = "cloud", - grok = "brand-x", - supergrok = "brand-x", - antigravity = "sparkles", - cursor = "cursor-text", - minimax = "wave-square", - kiro = "ghost", - copilot = "brand-github-copilot", - gemini = "brand-google", -} - -- ── Report helpers ──────────────────────────────────────────────────────────── --- "2026-08-15T11:29:59.872624Z" -> unix seconds. The stamps are UTC, so the --- naive os.time() reading (which assumes local time) is corrected by the local --- offset measured at that same instant. -local function parseIso(value) - if type(value) ~= "string" then return nil end - local y, mo, d, h, mi, s = value:match("^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)") - if y == nil then return nil end - local asLocal = os.time({ - year = tonumber(y), month = tonumber(mo), day = tonumber(d), - hour = tonumber(h), min = tonumber(mi), sec = tonumber(s), - }) - local utcAsLocal = os.time(os.date("!*t", asLocal)) - return asLocal + (asLocal - utcAsLocal) -end - -local function formatDuration(seconds) - if seconds <= 0 then return noctalia.tr("ui.now") end - local minutes = math.floor(seconds / 60) - local days = math.floor(minutes / 1440) - local hours = math.floor((minutes % 1440) / 60) - local rest = minutes % 60 - if days > 0 then return string.format("%dd %dh", days, hours) end - if hours > 0 then return string.format("%dh %dm", hours, rest) end - return string.format("%dm", rest) -end - -local function countdown(metric) - local at = parseIso(metric and metric.reset_at) - if at == nil then return "" end - return formatDuration(at - os.time()) -end - --- The clock time the countdown lands on: "14:20", or "Sat 14:20" past midnight. -local function resetClock(metric) - local at = parseIso(metric and metric.reset_at) - if at == nil then return "" end - local clock = noctalia.formatTime(noctalia.timeFormat(), at) - -- The weekday is prepended here rather than folded into the pattern: the - -- host's format grammar passes unknown text through verbatim, so a "ddd" - -- prefix would render as the literal word. - if os.date("%Y-%m-%d", at) ~= os.date("%Y-%m-%d") then - return os.date("%a", at) .. " " .. clock - end - return clock -end - -- "Resets in 4h 01m · 19% elapsed · 2pts ahead" says how much of the window is -- gone and how far the spend is from that line. local function elapsedPercent(metric) @@ -114,11 +43,6 @@ local function entries() return report.entries end -local function headline(entry) - if type(entry) ~= "table" or type(entry.metrics) ~= "table" then return nil end - return entry.metrics[1] -end - local SEVERITY_RANK = { critical = 3, high = 2, medium = 1, low = 0 } local function rank(entry) @@ -162,18 +86,13 @@ local function shown() return picked, #ready - #picked end --- The CLI already tiers every percentage, and copying its thresholds here --- would be a second source of truth. Text stays in the bar's own colour until --- the reading is high or critical, and the accent colour is used on the bar --- fill only. +-- `calm` is the colour when the CLI has raised nothing. With the tint switched +-- off it is the colour for everything. -- `calm` is the colour when the CLI has raised nothing. With the tint switched -- off it is the colour for everything. local function severityRole(metric, calm) if not colorByUsage then return calm end - local severity = metric ~= nil and tostring(metric.severity or "") or "" - if severity == "critical" then return "error" end - if severity == "high" then return "tertiary" end - return calm + return shared.severityRole(metric, calm) end local function shortName(entry) @@ -184,15 +103,6 @@ end -- ── Rendering ───────────────────────────────────────────────────────────────── --- A provider can report more than it was given, so the reading is clamped --- before it becomes a bar width. -local function ratio(percent) - local value = (tonumber(percent) or 0) / 100 - if value < 0 then return 0 end - if value > 1 then return 1 end - return value -end - -- Quota above, window elapsed below: a fill longer than the clock bar is spend -- running ahead of time. local function bars(percent, elapsed, tint, width) diff --git a/ai-usagebar/panel.luau b/ai-usagebar/panel.luau index 11982edc..77a4e3dd 100644 --- a/ai-usagebar/panel.luau +++ b/ai-usagebar/panel.luau @@ -7,13 +7,11 @@ local report = nil local polling = false --- The poller names the failure, so the panel only has a code to translate. --- Anything else in that state slot reads as no failure at all. -local NO_FAILURE = { code = "", detail = "" } - -local function asFailure(value) - return type(value) == "table" and value or NO_FAILURE -end +local shared = require("./shared.luau") +local GLYPHS, parseIso = shared.GLYPHS, shared.parseIso +local countdown, resetClock = shared.countdown, shared.resetClock +local ratio, headline, severityRole = shared.ratio, shared.headline, shared.severityRole +local NO_FAILURE, asFailure = shared.NO_FAILURE, shared.asFailure local failure = NO_FAILURE @@ -21,57 +19,6 @@ local failure = NO_FAILURE -- would otherwise stat PATH on every second tick it spends in a failure. local HAS_OPENER = noctalia.commandExists("xdg-open") --- Same parsing the capsule does. There is no require() below API 22, so the --- four helpers below are copied instead of shared. -local function parseIso(value) - if type(value) ~= "string" then return nil end - local y, mo, d, h, mi, s = value:match("^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)") - if y == nil then return nil end - local asLocal = os.time({ - year = tonumber(y), month = tonumber(mo), day = tonumber(d), - hour = tonumber(h), min = tonumber(mi), sec = tonumber(s), - }) - local utcAsLocal = os.time(os.date("!*t", asLocal)) - return asLocal + (asLocal - utcAsLocal) -end - -local function formatDuration(seconds) - if seconds <= 0 then return noctalia.tr("ui.now") end - local minutes = math.floor(seconds / 60) - local days = math.floor(minutes / 1440) - local hours = math.floor((minutes % 1440) / 60) - local rest = minutes % 60 - if days > 0 then return string.format("%dd %dh", days, hours) end - if hours > 0 then return string.format("%dh %dm", hours, rest) end - return string.format("%dm", rest) -end - -local function countdown(section) - local at = parseIso(section and section.reset_at) - if at == nil then return "" end - return formatDuration(at - os.time()) -end - -local function resetClock(section) - local at = parseIso(section and section.reset_at) - if at == nil then return "" end - local clock = noctalia.formatTime(noctalia.timeFormat(), at) - if os.date("%Y-%m-%d", at) == os.date("%Y-%m-%d") then return clock end - -- A weekday alone is ambiguous once the window is more than a week out. - if at - os.time() > 6 * 86400 then return os.date("%d %b", at) .. " " .. clock end - return os.date("%a", at) .. " " .. clock -end - --- The CLI tiers every percentage; copying its thresholds here would be a second --- source of truth. `calm` is what to use when it has raised nothing: text stays --- on the surface colour, and the accent is kept for bar fills. -local function severityRole(section, calm) - local severity = tostring(section and section.severity or "") - if severity == "critical" then return "error" end - if severity == "high" then return "tertiary" end - return calm -end - -- The CLI reports a vendor it has no credential for as a `credentials error`. -- Those are not listed, because they were never set up. A configured provider -- that fails for any other reason keeps its row. @@ -137,13 +84,6 @@ local function updatedText(entry) return noctalia.tr("ui.updated_ago", { minutes = minutes }) end -local function ratio(percent) - local value = (tonumber(percent) or 0) / 100 - if value < 0 then return 0 end - if value > 1 then return 1 end - return value -end - local function metricIcon(label) local text = tostring(label or ""):lower() if text:find("week") or text:find("month") then return "calendar" end @@ -288,33 +228,6 @@ end -- ── Provider list ───────────────────────────────────────────────────────────── --- Same map the capsule uses; no require() below API 22, so it is duplicated. -local GLYPHS = { - anthropic = "asterisk-simple", - anthropic_api = "asterisk-simple", - openai = "brand-openai", - zai = "bolt", - openrouter = "route", - deepseek = "fish", - kimi = "moon", - moonshot = "moon", - kilo = "robot", - novita = "cloud", - grok = "brand-x", - supergrok = "brand-x", - antigravity = "sparkles", - cursor = "cursor-text", - minimax = "wave-square", - kiro = "ghost", - copilot = "brand-github-copilot", - gemini = "brand-google", -} - -local function headline(entry) - if type(entry) ~= "table" or type(entry.metrics) ~= "table" then return nil end - return entry.metrics[1] -end - local function providerRow(entry, selected) local metric = headline(entry) local percent = metric ~= nil and tonumber(metric.percent) or nil diff --git a/ai-usagebar/plugin.toml b/ai-usagebar/plugin.toml index 0dc13313..660aadf0 100644 --- a/ai-usagebar/plugin.toml +++ b/ai-usagebar/plugin.toml @@ -1,7 +1,7 @@ id = "felipeartur/ai-usagebar" name = "AI Usage" version = "1.2.0" -plugin_api = 9 +plugin_api = 22 author = "felipeartur" license = "MIT" icon = "brain" diff --git a/ai-usagebar/service.luau b/ai-usagebar/service.luau index 3b55351e..8effb5da 100644 --- a/ai-usagebar/service.luau +++ b/ai-usagebar/service.luau @@ -118,43 +118,23 @@ end local inFlight = false --- The CLI caches for a minute, so a manual refresh usually answers in about ten --- milliseconds, too fast for the loader to survive a frame. The busy state is --- held for a beat instead, timed by the service's own tick. -local MIN_BUSY_MS = 600 -local busyUntil = 0 -local clearPending = false - -- A floor between spawns. Opening the panel asks for a read, and a panel can be -- opened as fast as a pointer can click, so this bounds how often the plugin -- can start a process no matter how the request arrives. local MIN_GAP_MS = 2000 local lastStart = 0 -local function stopPolling() - clearPending = false - noctalia.state.set("polling", false) - noctalia.setUpdateInterval(intervalMs()) -end - local function refresh() if inFlight then return end local now = noctalia.nowMs() if now - lastStart < MIN_GAP_MS then return end lastStart = now inFlight = true - busyUntil = now + MIN_BUSY_MS - clearPending = false noctalia.state.set("polling", true) - noctalia.setUpdateInterval(120) local started = noctalia.runAsync(COMMAND, function(result) inFlight = false - if noctalia.nowMs() >= busyUntil then - stopPolling() - else - clearPending = true - end + noctalia.state.set("polling", false) local decoded = result ~= nil and noctalia.json.decode(result.stdout or "") or nil if type(decoded) == "table" and type(decoded.entries) == "table" then @@ -172,8 +152,8 @@ local function refresh() -- sit in flight forever and stop asking. if not started then inFlight = false + noctalia.state.set("polling", false) noctalia.state.set("error", failure("spawn_failed")) - stopPolling() end end @@ -183,13 +163,8 @@ noctalia.state.watch("command", function(value) end) function update() - -- While a read is in flight the fast tick is the busy timer, not a poll. + -- A read in flight answers on its own callback; the tick only starts them. if inFlight then return end - if clearPending then - if noctalia.nowMs() >= busyUntil then stopPolling() end - return - end - noctalia.setUpdateInterval(intervalMs()) refresh() end diff --git a/ai-usagebar/shared.luau b/ai-usagebar/shared.luau new file mode 100644 index 00000000..7130e6cf --- /dev/null +++ b/ai-usagebar/shared.luau @@ -0,0 +1,112 @@ +--!nonstrict +-- What the capsule and the panel both need. +-- +-- One copy, so the ISO parsing, the severity tiers and the provider glyphs +-- cannot drift between two entries that have to agree with each other on +-- screen. Needs plugin_api 22, which is where require() arrived. + +local M = {} + +-- Tabler has no Anthropic mark, so providers without a brand glyph get a +-- semantic one. Same approach the other CLI-backed meters in this repo take. +M.GLYPHS = { + anthropic = "asterisk-simple", + anthropic_api = "asterisk-simple", + openai = "brand-openai", + zai = "bolt", + openrouter = "route", + deepseek = "fish", + kimi = "moon", + moonshot = "moon", + kilo = "robot", + novita = "cloud", + grok = "brand-x", + supergrok = "brand-x", + antigravity = "sparkles", + cursor = "cursor-text", + minimax = "wave-square", + kiro = "ghost", + copilot = "brand-github-copilot", + gemini = "brand-google", +} + +-- The poller names the failure, so a subscriber only ever has a code to +-- translate. Anything else in that state slot reads as no failure at all. +M.NO_FAILURE = { code = "", detail = "" } + +function M.asFailure(value) + return type(value) == "table" and value or M.NO_FAILURE +end + +-- "2026-08-15T11:29:59.872624Z" -> unix seconds. The stamps are UTC, so the +-- naive os.time() reading (which assumes local time) is corrected by the local +-- offset measured at that same instant. +function M.parseIso(value) + if type(value) ~= "string" then return nil end + local y, mo, d, h, mi, s = value:match("^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)") + if y == nil then return nil end + local asLocal = os.time({ + year = tonumber(y), month = tonumber(mo), day = tonumber(d), + hour = tonumber(h), min = tonumber(mi), sec = tonumber(s), + }) + local utcAsLocal = os.time(os.date("!*t", asLocal)) + return asLocal + (asLocal - utcAsLocal) +end + +function M.formatDuration(seconds) + if seconds <= 0 then return noctalia.tr("ui.now") end + local minutes = math.floor(seconds / 60) + local days = math.floor(minutes / 1440) + local hours = math.floor((minutes % 1440) / 60) + local rest = minutes % 60 + if days > 0 then return string.format("%dd %dh", days, hours) end + if hours > 0 then return string.format("%dh %dm", hours, rest) end + return string.format("%dm", rest) +end + +-- How long the window this section describes has left. +function M.countdown(section) + local at = M.parseIso(section and section.reset_at) + if at == nil then return "" end + return M.formatDuration(at - os.time()) +end + +-- The clock time the countdown lands on: "14:20" today, "Sat 14:20" past +-- midnight, and a date once a weekday alone stops naming one day. +function M.resetClock(section) + local at = M.parseIso(section and section.reset_at) + if at == nil then return "" end + local clock = noctalia.formatTime(noctalia.timeFormat(), at) + if os.date("%Y-%m-%d", at) == os.date("%Y-%m-%d") then return clock end + -- The weekday is prepended here instead of folded into the pattern: the + -- host's format grammar passes unknown text through verbatim, so a "ddd" + -- prefix would render as the literal word. + if at - os.time() > 6 * 86400 then return os.date("%d %b", at) .. " " .. clock end + return os.date("%a", at) .. " " .. clock +end + +-- A provider can report more than it was given, so the reading is clamped +-- before it becomes a bar width. +function M.ratio(percent) + local value = (tonumber(percent) or 0) / 100 + if value < 0 then return 0 end + if value > 1 then return 1 end + return value +end + +function M.headline(entry) + if type(entry) ~= "table" or type(entry.metrics) ~= "table" then return nil end + return entry.metrics[1] +end + +-- The CLI tiers every percentage; copying its thresholds here would be a second +-- source of truth. `calm` is what to use when it has raised nothing: text stays +-- on the surface colour, and the accent is kept for bar fills. +function M.severityRole(section, calm) + local severity = tostring(section and section.severity or "") + if severity == "critical" then return "error" end + if severity == "high" then return "tertiary" end + return calm +end + +return M From 18e613066d8b725c1915aa55d0c25660e5e32045 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 20 Aug 2026 09:20:12 -0300 Subject: [PATCH 05/12] ai-usagebar: settings button, v1.3.0 openSettings() needs plugin API 15, so the panel could not offer it while the manifest asked for 9. The move to 22 makes it available, and the panel is where someone is already looking at one provider and deciding the capsule should follow another. The capsule still answers a middle click the same way. The version is 1.3.0 rather than another 1.2.x because asking for API 22 is a compatibility break: on a shell older than that the plugin no longer installs. Requirements says so, since that is the page people read before installing. Dropped the note about reloading the plugin to pick up an edited translation. README.md is the plugin's page on noctalia.dev, written for someone installing it; which files the shell's watcher follows is only of interest to whoever is editing the plugin, and the note prescribed a full disable/enable when touching any .luau entry is enough. --- ai-usagebar/README.md | 17 +++++++---------- ai-usagebar/panel.luau | 9 +++++++++ ai-usagebar/plugin.toml | 2 +- ai-usagebar/translations/en.json | 1 + 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/ai-usagebar/README.md b/ai-usagebar/README.md index 5fe25c0c..93ac1519 100644 --- a/ai-usagebar/README.md +++ b/ai-usagebar/README.md @@ -28,6 +28,10 @@ endpoints, and this plugin never sees them. the CLI's project page offered when `ai-usagebar` is not on `PATH`. Without xdg-utils that button is not drawn and the rest of the plugin is unaffected. +The plugin asks for **plugin API 22**, which is where Noctalia gained +`require()`. On a shell older than that it will not install. Version 1.1.0 asked +for API 9 and still runs there. + ## Usage Add `felipeartur/ai-usagebar:bar` to a bar in Settings, Bar. The capsule shows @@ -83,8 +87,9 @@ so a fill that outruns the clock bar means quota is burning ahead of pace. Credit balances and free text rows the CLI reports get rendered as well. Opening the panel asks the CLI for fresh numbers, and the detail pane says how old the reading is. The refresh button in the header asks again; it turns into -a spinner while the CLI is answering. There is no close button: the panel -closes when you click away from it or press the same widget again. +a spinner while the CLI is answering. The gear beside it opens this plugin's +settings. There is no close button: the panel closes when you click away from +it or press the same widget again. The list follows the CLI. A provider that `ai-usagebar` has no credential for never appears, while one that is set up and failing keeps its row and shows the @@ -150,11 +155,3 @@ noctalia msg plugin felipeartur/ai-usagebar:poller all select anthropic - A provider that fails still comes back as an entry with `status = "error"`, so one broken provider does not blank the others. A reading the CLI marks stale keeps showing, flagged in the capsule and in the panel's detail pane. -- The file watcher follows the `.luau` entries only, so the files in - `translations/` are read once, when the plugin loads. Editing a string takes - a reload before the new text shows up: - - ```sh - noctalia msg plugins disable felipeartur/ai-usagebar - noctalia msg plugins enable felipeartur/ai-usagebar - ``` diff --git a/ai-usagebar/panel.luau b/ai-usagebar/panel.luau index 77a4e3dd..2f2559b9 100644 --- a/ai-usagebar/panel.luau +++ b/ai-usagebar/panel.luau @@ -387,6 +387,15 @@ local function listPane(entry) enabled = not polling, onClick = requestRefresh, }), + -- The capsule already answers a middle click with this, but the + -- panel is where someone is looking at a provider and deciding the + -- capsule should show a different one. + ui.button({ + glyph = "settings", + variant = "ghost", controlSize = "sm", + tooltip = noctalia.tr("ui.settings"), + onClick = function() noctalia.openSettings() end, + }), }), ui.scroll({ gap = 6, flexGrow = 1 }, rows), }) diff --git a/ai-usagebar/plugin.toml b/ai-usagebar/plugin.toml index 660aadf0..dcce3e5f 100644 --- a/ai-usagebar/plugin.toml +++ b/ai-usagebar/plugin.toml @@ -1,6 +1,6 @@ id = "felipeartur/ai-usagebar" name = "AI Usage" -version = "1.2.0" +version = "1.3.0" plugin_api = 22 author = "felipeartur" license = "MIT" diff --git a/ai-usagebar/translations/en.json b/ai-usagebar/translations/en.json index 4c0f0e98..f5a5074f 100644 --- a/ai-usagebar/translations/en.json +++ b/ai-usagebar/translations/en.json @@ -83,6 +83,7 @@ "now": "now", "refresh": "Refresh now", "retry": "Try again", + "settings": "Plugin settings", "severity": { "critical": "critical", "high": "high" From 36521867153f596f3e3d188413fabdf766fce29b Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 21 Aug 2026 01:31:38 -0300 Subject: [PATCH 06/12] ai-usagebar: keep the redaction inside the callback's CPU budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since the panel rework the poller has been losing every read: the async callback overran its CPU budget partway through scrubbing the report, so `state.set("report", ...)` never ran and the capsule sat on nothing. The shell named the line each time, always inside safeText. Three things made it expensive, and the report itself is not big — 165 strings for a two-vendor read. The four keyword pattern pairs were concatenated on every call, so they were rebuilt 165 times per report. They are constants; they are now built once, at load. The gate was one test for all four keywords, so a string carrying "key" — which is most of what an AI usage CLI writes about, along with "tokens" — ran all eight substitutions instead of the two belonging to its own keyword. Each keyword now opens only its own pair. The 200-character cap ran after the redaction rather than before it, which left the patterns scanning a runaway line in full. Capping first bounds their work by what the plugin was going to draw anyway; a secret past the cut is not truncated into view, it is gone with the rest of the line. Measured against a real `usage --json`: 0.676 ms down to 0.393 ms for the whole report, and 6.44 ms down to 0.37 ms for a 4.4 KB line. No budget overruns in eleven cycles on the running shell, against one on nearly every cycle before. --- ai-usagebar/service.luau | 51 +++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/ai-usagebar/service.luau b/ai-usagebar/service.luau index 8effb5da..62ed6284 100644 --- a/ai-usagebar/service.luau +++ b/ai-usagebar/service.luau @@ -20,12 +20,21 @@ end -- A secret's value runs until whitespace or the quote or brace that closes it, -- so a JSON field loses its value and keeps its punctuation. local SECRET_VALUE = "[^%s\"',}]+" -local SECRET_WORDS = { - "[Kk][Ee][Yy]", - "[Tt][Oo][Kk][Ee][Nn]", - "[Ss][Ee][Cc][Rr][Ee][Tt]", - "[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]", -} +-- Built once. `scrub` calls safeText on every string in the report — ~165 for a +-- two-vendor read — and rebuilding these four pattern pairs on each of those +-- calls cost more than matching them did. +local SECRET_PATTERNS = {} +for _, word in ipairs({ "key", "token", "secret", "password" }) do + local anyCase = (word:gsub("%a", function(c) return "[" .. c:upper() .. c .. "]" end)) + local name = "[%w_%-]*" .. anyCase .. "[%w_%-]*" + SECRET_PATTERNS[#SECRET_PATTERNS + 1] = { + word = word, + -- name=value: a query string or a shell assignment. + assign = "(" .. name .. "=)" .. SECRET_VALUE, + -- name: value: an HTTP header or a JSON field. + colon = "(" .. name .. "\"?%s*:%s*\"?)" .. SECRET_VALUE, + } +end -- Nine characters before the rest of a provider key, so a bare "sk-" in prose -- is not mistaken for one. local KEY_TAIL = string.rep("[%w_%-]", 9) @@ -34,22 +43,23 @@ local function safeText(value) local text = noctalia.string.trim(tostring(value or "")) text = text:gsub("%s+", " ") - -- `scrub` runs this over ~165 strings per two-vendor read, and backtracking - -- patterns on every one of them exhaust the callback's CPU budget, which - -- costs the whole report. So nothing expensive runs until a literal search - -- says it could match. The keyword is what opens the gate. A separator will - -- not do: `=` and `:` both turn up in ordinary readings, in a ratio, a clock - -- time, a URL, so gating on those ran the patterns over almost every string. + -- Capped before the redaction runs rather than after. The patterns are the + -- expensive part of the callback, and a callback that overruns its CPU + -- budget loses the whole report — so the work they do is bounded by what + -- the plugin would draw anyway. + if #text > 200 then text = string.sub(text, 1, 200) .. "..." end + + -- Nothing expensive runs until a literal search says it could match, and + -- each keyword opens only its own two patterns. The keyword is what opens + -- the gate. A separator will not do: `=` and `:` both turn up in ordinary + -- readings, in a ratio, a clock time, a URL, so gating on those ran the + -- patterns over almost every string. local lower = text:lower() - if lower:find("key", 1, true) or lower:find("token", 1, true) - or lower:find("secret", 1, true) or lower:find("password", 1, true) then - for _, word in ipairs(SECRET_WORDS) do - local name = "[%w_%-]*" .. word .. "[%w_%-]*" - -- name=value: a query string or a shell assignment. - text = text:gsub("(" .. name .. "=)" .. SECRET_VALUE, "%1") - -- name: value: an HTTP header or a JSON field. - text = text:gsub("(" .. name .. "\"?%s*:%s*\"?)" .. SECRET_VALUE, "%1") + for _, secret in ipairs(SECRET_PATTERNS) do + if lower:find(secret.word, 1, true) then + text = text:gsub(secret.assign, "%1") + text = text:gsub(secret.colon, "%1") end end @@ -68,7 +78,6 @@ local function safeText(value) text = text:gsub("%f[%w](sk%-)" .. KEY_TAIL .. "[%w_%-]*", "%1") end - if #text > 200 then text = string.sub(text, 1, 200) .. "..." end return text end From 4cf6a84f05e5da0d9797982d9abaf522191d4d1a Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 21 Aug 2026 01:43:45 -0300 Subject: [PATCH 07/12] ai-usagebar: put a CPU budget under the redaction test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test covered what the scrubber redacts and what it leaves alone, which is why the rewrite that just landed could be checked at all. It did not cover what the scrubber costs, which is the half that broke: the output was correct on every string right up to the point the shell killed the callback for overrunning its budget, and a correct answer nobody receives is not one. So the test now scrubs a report shaped like a real `usage --json` — four vendors, six metrics each, and the credential error the CLI writes for a provider it has no key for, which is the string that opens the redaction patterns on an otherwise healthy run — and asserts what that costs. The meter is `string.gsub`, wrapped for the length of the call. Counting VM instructions the way keymap's budget tests do reads nothing useful here: the work happens inside the C matcher, where the count hook is blind, and the old scrubber and the new one came out one block apart. What separates them is how much text the patterns are handed: 37352 bytes for this report before, 17664 now. The ceiling sits between the two, near enough that either half of the regression trips it on its own. The slice the test loads was widened to take `scrub` along with `safeText`, so the recursion over the report is measured rather than assumed, and README gained the section that says how to run it, as keymap and udiskie do. --- ai-usagebar/README.md | 16 ++++++ ai-usagebar/tests/scrub_test.lua | 89 +++++++++++++++++++++++++++++--- 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/ai-usagebar/README.md b/ai-usagebar/README.md index 93ac1519..757fb07f 100644 --- a/ai-usagebar/README.md +++ b/ai-usagebar/README.md @@ -155,3 +155,19 @@ noctalia msg plugin felipeartur/ai-usagebar:poller all select anthropic - A provider that fails still comes back as an entry with `status = "error"`, so one broken provider does not blank the others. A reading the CLI marks stale keeps showing, flagged in the capsule and in the panel's detail pane. + +## Tests + +The redaction that stands between the CLI's output and the screen is the one +part of this plugin worth a test, so it has one. From the `ai-usagebar` +directory: + +```sh +lua tests/scrub_test.lua +``` + +It reads `safeText` and `scrub` out of `service.luau` rather than copying them, +and checks three things: that a set of real credential shapes never survive, that +ordinary readings pass through unchanged, and that scrubbing a four-vendor report +stays inside the CPU budget the poller's async callback is given — an overrun +there loses the whole reading, not just time. diff --git a/ai-usagebar/tests/scrub_test.lua b/ai-usagebar/tests/scrub_test.lua index f7932de0..0bfe8237 100644 --- a/ai-usagebar/tests/scrub_test.lua +++ b/ai-usagebar/tests/scrub_test.lua @@ -4,7 +4,7 @@ -- request tends to quote the request. safeText is the only thing standing -- between that and a rendered label, so it gets a test. -- --- The function is read out of service.luau rather than copied here: a copy +-- The functions are read out of service.luau rather than copied here: a copy -- would keep passing after the real one changed. -- -- lua tests/scrub_test.lua (or luajit) @@ -21,8 +21,9 @@ local function loadSafeText() local source = file:read("*a") file:close() - -- The slice runs from the redaction constants to the end of the function. - local chunk = source:match("(local SECRET_VALUE.-\nend)\n") + -- The slice runs from the redaction constants through scrub, which is what + -- the poller's callback actually calls. + local chunk = source:match("(local SECRET_VALUE.-)\nlocal function failure") if chunk == nil then error("could not find safeText in " .. SOURCE .. "; update the markers here") end @@ -31,14 +32,16 @@ local function loadSafeText() local env = { string = string, ipairs = ipairs, + pairs = pairs, + type = type, tostring = tostring, noctalia = { string = { trim = function(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end } }, } - local loaded = load(chunk .. "\nreturn safeText", "safeText", "t", env) + local loaded = load(chunk .. "\nreturn safeText, scrub", "scrubber", "t", env) return loaded() end -local safeText = loadSafeText() +local safeText, scrub = loadSafeText() -- Each case names the material that must not survive. local SECRETS = { @@ -53,6 +56,9 @@ local SECRETS = { { "authorization: bearer sk-ant-api03-REALKEY", "REALKEY" }, { "OPENAI_API_KEY sk-proj-REALKEYVALUE not accepted", "REALKEY" }, { "password=hunter2", "hunter2" }, + -- The cap runs before the patterns, so a secret in a runaway line has to + -- survive being truncated around. + { "api_key=sk-ant-REALKEY123 " .. string.rep("noise ", 60), "REALKEY123" }, } -- Readings the plugin draws every minute. A scrubber that eats these is worse @@ -100,9 +106,80 @@ if #long > 210 then fail("long text was not capped: " .. #long .. " characters") end +-- The poller scrubs the whole report inside one async callback, and a callback +-- that overruns its CPU budget is killed by the shell: the reading is lost, not +-- merely late. So the cost of a scrub is asserted, not just its output. +-- +-- The meter is `string.gsub`: every pattern in safeText runs through it, and the +-- work happens inside the C matcher, where an instruction-count hook sees +-- nothing. Counting the calls and the bytes handed to them measures the two +-- things that made the 1.2.0 scrubber overrun — a keyword opening all four +-- keywords' substitutions, and the length cap running after them instead of +-- before. +-- +-- The report below is the shape of a real `usage --json`: four vendors, six +-- metrics each, and the credential error the CLI writes for a provider it has no +-- key for — the string that opens the redaction patterns on an otherwise healthy +-- run. +local function sampleReport() + local entries = {} + for _, vendor in ipairs({ "anthropic", "openai", "zai", "openrouter" }) do + local metrics = {} + for index = 1, 6 do + metrics[index] = { + label = "Session (5h)", + value = "62% of monthly limit consumed", + detail = "Resets in 4h 01m at 12:40", + reset_at = "2026-08-20T11:29:59.872624Z", + severity = "normal", + percent = 62, + } + end + entries[#entries + 1] = { + id = vendor, + name = vendor, + display_name = "Claude Pro", + plan = "Claude Pro", + status = "ok", + stale = false, + fetched_at = "2026-08-20T11:29:59.872624Z", + metrics = metrics, + sections = { { type = "session" }, { type = "weekly" } }, + error = "credentials error: " .. vendor .. ": no API key. Either set an API key in a" + .. " valid environment variable or set `api_key` under [" .. vendor .. "] in the" + .. " config file. " .. string.rep("Retry later. ", 40), + } + end + return { entries = entries } +end + +local calls, bytes = 0, 0 +local realGsub = string.gsub +string.gsub = function(subject, ...) + calls = calls + 1 + bytes = bytes + #subject + return realGsub(subject, ...) +end +scrub(sampleReport()) +string.gsub = realGsub + +-- Bytes, not calls: the count barely moves, because normalising whitespace is one +-- gsub per string either way. What moved is how much text the redaction patterns +-- were handed — 37352 bytes for this report in 1.2.0, against 17664 now. The +-- ceiling sits between the two, close enough that widening the gate back to all +-- four keywords at once (22400) trips it as surely as putting the length cap back +-- after the patterns (37352) does. +local MAX_BYTES = 20000 +if bytes > MAX_BYTES then + fail("the redaction patterns were handed " .. bytes .. " bytes of a four-vendor report" + .. " in " .. calls .. " gsub calls, past the " .. MAX_BYTES .. " bytes this callback" + .. " budgets for") +end + if failures > 0 then io.write(failures, " failure(s)\n") os.exit(1) end -io.write("ok: ", #SECRETS, " secrets redacted, ", #BENIGN, " readings untouched, length capped\n") +io.write("ok: ", #SECRETS, " secrets redacted, ", #BENIGN, " readings untouched, length capped, ", + calls, " gsub calls over ", bytes, " bytes per report\n") From bd7169ddb025c1a9bd7176cde32c65c737dbdccd Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 21 Aug 2026 01:52:40 -0300 Subject: [PATCH 08/12] ai-usagebar: rank mid severity, share the detail parser, cut the comments Three passes over the plugin. The CLI tiers severity as low, mid, high and critical. The capsule's rank table answered "medium", which nothing ever sends, so a mid provider sorted level with a low one and "auto" could put the calmer plan on the bar. The table now keys on what the CLI actually writes, and drops the two rows that were already the default. `elapsedPercent` was parsed the same way in both entries. It belongs with the other shared readings, and the capsule's copy of `parseIso` was left over from before the split. The panel's pace lookup had a branch that returned exactly what the branch under it returns. The rest is prose. The comments had grown into an argument for each decision rather than a note about it, and the argument is what a reader has to skip to reach the fact. What survives is what the code cannot say for itself: why the patterns are built once, why the cap runs before them, why the title block is wrapped in a row, why a row is keyed, why status 127 has to agree with its message. The rest went, along with the em dashes; the ones left are the "no reading" placeholder the panel and the capsule both draw. No behaviour changed beyond the severity rank. Verified against the running shell: eleven cycles, no errors, no budget overruns. --- ai-usagebar/README.md | 13 ++- ai-usagebar/bar.luau | 65 ++++++--------- ai-usagebar/panel.luau | 137 ++++++++++++------------------- ai-usagebar/service.luau | 79 ++++++++---------- ai-usagebar/shared.luau | 47 ++++++----- ai-usagebar/tests/scrub_test.lua | 48 +++++------ 6 files changed, 161 insertions(+), 228 deletions(-) diff --git a/ai-usagebar/README.md b/ai-usagebar/README.md index 757fb07f..6ba749c5 100644 --- a/ai-usagebar/README.md +++ b/ai-usagebar/README.md @@ -158,16 +158,15 @@ noctalia msg plugin felipeartur/ai-usagebar:poller all select anthropic ## Tests -The redaction that stands between the CLI's output and the screen is the one -part of this plugin worth a test, so it has one. From the `ai-usagebar` -directory: +Everything the CLI prints is redacted on its way to the screen, and that is the +part worth a test. From the `ai-usagebar` directory: ```sh lua tests/scrub_test.lua ``` It reads `safeText` and `scrub` out of `service.luau` rather than copying them, -and checks three things: that a set of real credential shapes never survive, that -ordinary readings pass through unchanged, and that scrubbing a four-vendor report -stays inside the CPU budget the poller's async callback is given — an overrun -there loses the whole reading, not just time. +then checks that real credential shapes never survive, that ordinary readings +pass through unchanged, and that scrubbing a four-vendor report stays inside the +CPU budget the poller's async callback is given. An overrun there loses the whole +reading, not just time. diff --git a/ai-usagebar/bar.luau b/ai-usagebar/bar.luau index ec96c409..7c1bfb45 100644 --- a/ai-usagebar/bar.luau +++ b/ai-usagebar/bar.luau @@ -1,8 +1,7 @@ --!nonstrict --- Bar capsule. Reads whatever the poller published and draws one provider, or --- the busiest few when `provider_limit` is raised. --- --- Per-instance settings, so two capsules can follow two different providers. +-- Bar capsule. Draws what the poller published: one provider, or the busiest few +-- when `provider_limit` is raised. Settings are per-instance, so a second capsule +-- can follow a second provider. local vendor = tostring(noctalia.getConfig("vendor") or "auto") local style = tostring(noctalia.getConfig("style") or "pill") @@ -15,22 +14,15 @@ local report = nil local polling = false local shared = require("./shared.luau") -local GLYPHS, parseIso = shared.GLYPHS, shared.parseIso +local GLYPHS = shared.GLYPHS local countdown, resetClock = shared.countdown, shared.resetClock -local ratio, headline = shared.ratio, shared.headline +local ratio, headline, elapsedPercent = shared.ratio, shared.headline, shared.elapsedPercent local NO_FAILURE, asFailure = shared.NO_FAILURE, shared.asFailure local failure = NO_FAILURE -- ── Report helpers ──────────────────────────────────────────────────────────── --- "Resets in 4h 01m · 19% elapsed · 2pts ahead" says how much of the window is --- gone and how far the spend is from that line. -local function elapsedPercent(metric) - local value = tostring(metric and metric.detail or ""):match("(%d+)%%%s*elapsed") - return value ~= nil and tonumber(value) or nil -end - -- Returns points and direction: 2, "ahead" is burning faster than the clock. local function pace(metric) local points, word = tostring(metric and metric.detail or ""):match("(%d+)pts%s+(%a+)") @@ -43,7 +35,7 @@ local function entries() return report.entries end -local SEVERITY_RANK = { critical = 3, high = 2, medium = 1, low = 0 } +local SEVERITY_RANK = { critical = 3, high = 2, mid = 1 } local function rank(entry) local metric = headline(entry) @@ -51,8 +43,9 @@ local function rank(entry) return SEVERITY_RANK[tostring(metric.severity or "")] or 0, tonumber(metric.percent) or 0 end --- A pinned vendor shows only itself. "auto" shows the busiest providers, so --- the one closest to running out is the one on the bar. `primary` breaks ties. +-- A pinned vendor shows only itself. "auto" ranks by severity then percentage, +-- so the provider closest to running out is the one on the bar. `primary` breaks +-- ties. local function shown() local all = entries() if vendor ~= "auto" then @@ -80,16 +73,12 @@ local function shown() local picked = {} for i = 1, math.min(limit, #ready) do picked[i] = ready[i] end if #picked == 0 then return {}, 0 end - -- Someone who asked for one provider does not need a count of the others, - -- so the "+N" only appears once the capsule carries more than one. if limit == 1 then return picked, 0 end return picked, #ready - #picked end --- `calm` is the colour when the CLI has raised nothing. With the tint switched --- off it is the colour for everything. --- `calm` is the colour when the CLI has raised nothing. With the tint switched --- off it is the colour for everything. +-- `calm` is the colour when the CLI has raised nothing, and every colour when +-- the tint is switched off. local function severityRole(metric, calm) if not colorByUsage then return calm end return shared.severityRole(metric, calm) @@ -97,13 +86,14 @@ end local function shortName(entry) local name = tostring(entry.display_name or entry.name or entry.id or "") - -- "Claude · gmail" is the panel's business; the bar has room for the product. + -- "Claude · gmail" is the panel's business; the bar only has room for the + -- product name. return (name:gsub("%s*·.*$", "")) end -- ── Rendering ───────────────────────────────────────────────────────────────── --- Quota above, window elapsed below: a fill longer than the clock bar is spend +-- Quota above, window elapsed below: a longer fill than clock bar is spend -- running ahead of time. local function bars(percent, elapsed, tint, width) local stack = { @@ -137,8 +127,6 @@ local function countdownNode(metric) return ui.label({ text = left, fontSize = 10, color = "on_surface_variant", maxLines = 1 }) end --- One provider's chip. The style decides the shape, and the extras are --- appended to whatever it produced. local function chip(entry) local metric = headline(entry) local tint = severityRole(metric, "on_surface") @@ -146,8 +134,8 @@ local function chip(entry) local percent = metric ~= nil and tonumber(metric.percent) or nil local text = percent ~= nil and string.format("%d%%", percent) or "—" local glyph = ui.glyph({ name = GLYPHS[tostring(entry.id)] or "brain", size = 13, color = tint }) - -- Right-aligned in a fixed column, so the capsule is the same width at 9% - -- as at 100% and stops nudging its neighbours on the bar once per read. + -- Fixed width, right-aligned: the capsule is the same size at 9% as at 100% + -- and stops nudging its neighbours once per read. local pct = ui.label({ text = text, fontSize = 11, fontWeight = "semibold", color = tint, maxLines = 1, width = 30, textAlign = "end" }) local name = showName and ui.label({ text = shortName(entry), fontSize = 11, @@ -157,7 +145,6 @@ local function chip(entry) local function add(node) if node ~= nil then nodes[#nodes + 1] = node end end if style == "meter" and percent ~= nil then - -- Five ticks instead of digits: the reading at a glance, no numbers. local ticks = {} for i = 0, 4 do ticks[#ticks + 1] = ui.box({ @@ -168,18 +155,17 @@ local function chip(entry) add(glyph); add(name) add(ui.row({ gap = 2, align = "center" }, ticks)) elseif style == "label" and percent ~= nil then - -- Name and number stacked over the bar, for a bar with room to spare. add(glyph) add(ui.column({ gap = 1, align = "center" }, { ui.row({ gap = 3, align = "center" }, { ui.label({ text = shortName(entry), fontSize = 10, color = "on_surface_variant", maxLines = 1 }), pct, }), - bars(percent, elapsedPercent(metric), fill, 44), + bars(percent, elapsedPercent(metric and metric.detail), fill, 44), })) elseif style == "gauge" and percent ~= nil then add(glyph); add(name) - add(bars(percent, elapsedPercent(metric), fill, 26)) + add(bars(percent, elapsedPercent(metric and metric.detail), fill, 26)) add(pct) else add(glyph); add(name); add(pct) @@ -243,9 +229,9 @@ end local function render() local picked, hidden = shown() - -- A failure drops the reading here too, so the bar cannot be read as a - -- live percentage while the panel behind it says the CLI is unreachable. - -- Empty is already the shape that draws the alert glyph. + -- A failure drops the reading, so the capsule cannot show a live percentage + -- while the panel behind it says the CLI is unreachable. Empty already draws + -- the alert glyph. if failure.code ~= "" then picked, hidden = {}, 0 end local children = {} @@ -254,9 +240,8 @@ local function render() end if #children == 0 then - -- One glyph, coloured by the state. A second icon beside it reads as a - -- second problem, and the plugin's own mark in the error colour says - -- the same thing in the space of one. + -- One glyph, coloured by the state. A second icon beside it would read as + -- a second problem. children[1] = ui.glyph({ name = "brain", size = 13, color = failure.code ~= "" and "error" or "on_surface_variant", @@ -266,8 +251,8 @@ local function render() color = "on_surface_variant", maxLines = 1 }) end - -- A read in flight dims the capsule rather than appending a spinner to it: - -- a node that comes and goes every cycle shoves every widget to its right. + -- A read in flight dims the capsule instead of appending a spinner: a node + -- that comes and goes every cycle shoves every widget to its right. barWidget.render(ui.row({ gap = 6, align = "center", opacity = polling and 0.55 or 1 }, children)) barWidget.setTooltip(tooltip(picked, hidden)) end diff --git a/ai-usagebar/panel.luau b/ai-usagebar/panel.luau index 2f2559b9..26f2a2a8 100644 --- a/ai-usagebar/panel.luau +++ b/ai-usagebar/panel.luau @@ -1,8 +1,6 @@ --!nonstrict --- Expanded panel for one provider. --- --- It renders `sections[]`, which is the CLI's lossless view, so credit blocks --- and free text that the shorter `metrics[]` view drops still show up. +-- Expanded panel for one provider. It renders `sections[]`, the CLI's lossless +-- view, so the credit blocks and free text that `metrics[]` drops still show up. local report = nil local polling = false @@ -11,17 +9,18 @@ local shared = require("./shared.luau") local GLYPHS, parseIso = shared.GLYPHS, shared.parseIso local countdown, resetClock = shared.countdown, shared.resetClock local ratio, headline, severityRole = shared.ratio, shared.headline, shared.severityRole +local elapsedPercent = shared.elapsedPercent local NO_FAILURE, asFailure = shared.NO_FAILURE, shared.asFailure local failure = NO_FAILURE --- Read once: a session either has xdg-utils or it does not, and the panel --- would otherwise stat PATH on every second tick it spends in a failure. +-- Read once: a session either has xdg-utils or it does not, and the panel would +-- otherwise stat PATH on every second tick it spends in a failure. local HAS_OPENER = noctalia.commandExists("xdg-open") --- The CLI reports a vendor it has no credential for as a `credentials error`. --- Those are not listed, because they were never set up. A configured provider --- that fails for any other reason keeps its row. +-- A vendor with no credential comes back as a `credentials error`. It was never +-- set up, so it is not listed. A configured provider that fails for any other +-- reason keeps its row. local function configured(entry) if entry.status ~= "error" then return true end return not tostring(entry.error or ""):lower():find("credentials error") @@ -29,12 +28,7 @@ end -- ── Detail line parsing ─────────────────────────────────────────────────────── -- "Resets in 1h 58m · 60% elapsed · 30pts ahead". The reset half is already in --- `reset_at`; what is left is the pace pair. - -local function elapsedPercent(detail) - local value = tostring(detail or ""):match("(%d+)%%%s*elapsed") - return value ~= nil and tonumber(value) or nil -end +-- `reset_at`; what is left is the pace. local function pace(detail) local text = tostring(detail or "") @@ -43,9 +37,8 @@ local function pace(detail) for part in text:gmatch("[^·]+") do last = part end last = noctalia.string.trim(last) if last:find("elapsed") then return "", "on_surface_variant" end - -- Ahead of the clock is worth flagging. Under it means there is room left. + -- Ahead of the clock is worth flagging; under it means there is room left. if last:find("ahead") then return last, "tertiary" end - if last:find("under") then return last, "on_surface_variant" end return last, "on_surface_variant" end @@ -93,8 +86,6 @@ end -- ── Cards ───────────────────────────────────────────────────────────────────── --- A severity word, and only when the CLI raised one. Colour on its own leaves --- the reading to anyone who can tell the two accents apart. local function severityWord(section) local severity = tostring(section and section.severity or "") if severity ~= "high" and severity ~= "critical" then return nil end @@ -122,8 +113,7 @@ local function metricCard(section) if showValue then header[#header + 1] = ui.label({ text = value, fontSize = 11, color = "on_surface_variant", maxLines = 1 }) end - -- Every card ends on the same right edge, so a column of them reads as one - -- ruler instead of a ragged margin. + -- Fixed width, so a column of cards ends on one right edge. header[#header + 1] = ui.label({ text = string.format("%d%%", percent), fontSize = 15, @@ -138,8 +128,6 @@ local function metricCard(section) ui.progress({ progress = ratio(percent), fill = fill, track = "on_surface/0.16", radius = 3, height = 6 }), } - -- Two readings: quota spent above, window elapsed below. A shorter clock bar - -- than fill bar is quota burning ahead of time. local elapsed = elapsedPercent(section.detail) if elapsed ~= nil then body[#body + 1] = ui.progress({ @@ -151,9 +139,6 @@ local function metricCard(section) }) end - -- One line under the bars carries the whole time story: what is left of the - -- window, when it lands, how much of it is gone, and whether the spend is - -- running ahead. Four separate lines said the same thing four times taller. local left = countdown(section) local clock = resetClock(section) local paceText, paceColor = pace(section.detail) @@ -162,9 +147,8 @@ local function metricCard(section) footer[#footer + 1] = ui.glyph({ name = "clock", size = 12, color = "on_surface_variant" }) footer[#footer + 1] = ui.label({ text = left, fontSize = 11, color = "on_surface_variant" }) if clock ~= "" then - -- Parenthesised and muted: it is the time the countdown beside it - -- lands on, not a reading of its own. In the accent colour it was - -- the loudest thing in the card after the percentage. + -- Parenthesised and muted: it is where the countdown beside it lands, + -- not a reading of its own. footer[#footer + 1] = ui.label({ text = "(" .. clock .. ")", fontSize = 11, color = "on_surface_variant", }) @@ -205,9 +189,8 @@ local function blockCard(section) } for _, line in ipairs(section.body or {}) do local text = noctalia.string.trim(tostring(line)) - -- A line the CLI left as a bare "balance:" reads as a row that failed - -- to render. Nothing is a value, and it is spelled the same way here as - -- it is everywhere else in the panel. + -- A bare "balance:" from the CLI would read as a row that failed to + -- render, so nothing gets spelled the way it is everywhere else. if text:find(":$") then text = text .. " —" end body[#body + 1] = ui.label({ text = text ~= "" and text or "—", @@ -234,9 +217,8 @@ local function providerRow(entry, selected) local broken = entry.status == "error" local tint = severityRole(metric, "on_surface") - -- The reading keeps its own severity colour whether or not the row is - -- selected. A selected row that recolours its number hides the one thing - -- the list exists to compare. + -- The reading keeps its severity colour whether or not the row is selected: + -- recolouring it hides the one thing the list exists to compare. local right if broken then right = ui.row({ width = 34, justify = "end" }, { @@ -271,16 +253,13 @@ local function providerRow(entry, selected) }) return ui.row({ - -- Keyed, so the click handler survives the second tick the countdowns - -- ride on rather than being rebuilt under the pointer once a second. + -- Keyed, so the click handler survives the second tick the countdowns ride + -- on instead of being rebuilt under the pointer. key = "provider-" .. tostring(entry.id), gap = 8, align = "center", padding = 8, radius = 8, - -- Selection is a tint, not a slab of accent: a filled `primary` row has - -- to invert every colour inside it, and then it shouts over the reading. + -- A tint, not a slab: a filled `primary` row inverts every colour in it. fill = selected and "primary/0.14" or "surface_variant/0.45", onClick = function() - -- currentEntry() reads this back, so the panel and the capsule that - -- opened it stay on the same provider. noctalia.state.set("selected", tostring(entry.id)) render() end, @@ -298,8 +277,7 @@ local function requestRefresh() noctalia.state.set("command", { action = "refresh", at = os.time() }) end --- The failure and the suggested fix read first; the CLI's own words come last --- and smallest, where a bug report can still quote them. +-- The CLI's own words come last and smallest, where a bug report can quote them. local function errorBlock() local key = "ui.error." .. failure.code local children = { @@ -316,8 +294,6 @@ local function errorBlock() }) end - -- The shell's own button, so a retry here looks like every other retry in - -- Noctalia and follows the user's theme without being told to. local actions = { ui.button({ text = noctalia.tr("ui.retry"), glyph = "refresh", @@ -327,10 +303,10 @@ local function errorBlock() }), } - -- Retrying is pointless until the CLI exists, so that one failure gets the - -- install page as well. The URL is a literal, so there is nothing to quote, - -- and the button is only offered where something can open it. It reads as a - -- label with the address in its tooltip: a raw URL is not a button caption. + -- Retrying is pointless until the CLI exists, so that failure gets the install + -- page too. The URL is a literal, and the button is only offered where + -- something can open it. The address lives in the tooltip: a raw URL is not a + -- button caption. if failure.code == "not_installed" and HAS_OPENER then actions[#actions + 1] = ui.button({ text = noctalia.tr("ui.install"), glyph = "external-link", @@ -346,9 +322,8 @@ local function errorBlock() return ui.column({ gap = 8 }, children) end --- A muted stand-in at the shape of what is coming, so a cold read is not a --- spinner parked where the content is about to land. One shape serves both --- panes: it is a placeholder, and two kinds of placeholder is one too many. +-- A muted stand-in shaped like what is coming, so a cold read is not a spinner +-- parked where the content will land. One shape serves both panes. local function skeleton(key) return ui.column({ key = "skeleton-" .. key, @@ -373,13 +348,12 @@ local function listPane(entry) return ui.column({ gap = 10, padding = 14, width = 250 }, { ui.row({ gap = 8, align = "center" }, { ui.glyph({ name = "brain", size = 18, color = "primary" }), - -- The accent belongs to the selection and the bars. A title that - -- takes it too leaves the panel with no quiet level to fall back to. + -- The accent belongs to the selection and the bars. A title that took + -- it too would leave the panel with no quiet level. ui.label({ text = noctalia.tr("ui.title"), fontSize = 15, fontWeight = "bold", color = "on_surface" }), ui.spacer({ flexGrow = 1 }), - -- One slot for the read: the button becomes the spinner while the - -- CLI answers, rather than a second glyph appearing beside it and - -- pushing the header around once a cycle. + -- One slot for the read: the button becomes the spinner while the CLI + -- answers, instead of a second glyph pushing the header around. ui.button({ glyph = polling and "loader-2" or "refresh", variant = "ghost", controlSize = "sm", @@ -387,9 +361,8 @@ local function listPane(entry) enabled = not polling, onClick = requestRefresh, }), - -- The capsule already answers a middle click with this, but the - -- panel is where someone is looking at a provider and deciding the - -- capsule should show a different one. + -- The capsule answers a middle click with this too, but the panel is + -- where someone decides the capsule should follow another provider. ui.button({ glyph = "settings", variant = "ghost", controlSize = "sm", @@ -410,15 +383,14 @@ local function detailPane(entry) if subtitle == title then subtitle = "" end end - -- No entry yet means the skeletons below are the whole pane. A title here - -- would only repeat the one the list pane is already showing. + -- With no entry the skeletons below are the whole pane, and a title here would + -- repeat the list pane's. local children = {} if entry ~= nil then - -- The row keeps the title block honest about its height. A bare - -- ui.column dropped into a column takes the pane's free space for - -- itself, which parks the title at the top of a hundred pixels of - -- nothing and pushes the rest of the header down. Wrapped, the block is - -- only as tall as the two labels in it. + -- The row keeps the title block honest about its height: a bare ui.column + -- dropped into a column claims the pane's free space, parking the title at + -- the top of a hundred pixels of nothing. Wrapped, it is as tall as the two + -- labels in it. children[#children + 1] = ui.row({ gap = 8, align = "center" }, { ui.column({ gap = 0, flexGrow = 1 }, { ui.label({ text = title, fontSize = 15, fontWeight = "bold", @@ -429,9 +401,8 @@ local function detailPane(entry) }) end - -- What the entry says about itself, in words rather than a colour. The - -- provider id and a "ready" status are the plugin talking to itself: the id - -- is the row that was just clicked, and a healthy read is the default. + -- The id and a "ready" status are skipped: the id is the row that was just + -- clicked, and a healthy read is the default. if entry ~= nil then local chips = {} local function separate() @@ -481,8 +452,6 @@ local function detailPane(entry) if #cards > 0 then children[#children + 1] = ui.scroll({ gap = 8, flexGrow = 1 }, cards) elseif entry ~= nil then - -- A provider with nothing to draw says so. Half an empty panel is not - -- an answer to the question the panel was opened to answer. children[#children + 1] = ui.row({ gap = 6, align = "center" }, { ui.glyph({ name = "info-circle", size = 14, color = "on_surface_variant" }), ui.label({ text = noctalia.tr("ui.no_usage"), fontSize = 11, color = "on_surface_variant" }), @@ -499,14 +468,12 @@ end function render() local entry = currentEntry() - -- A failure replaces the report. The numbers are from a read that is no - -- longer happening, and leaving them up puts a provider list and a - -- percentage next to an alert saying neither can be trusted. + -- A failure replaces the report: those numbers came from a read that is no + -- longer happening. if failure.code ~= "" then - -- The panel keeps the fixed size the manifest gives it, and a failure - -- has nowhere near 720x400 of things to say. The block stays bounded to - -- a readable width and sits in the middle of the panel, where an empty - -- surround reads as composition instead of a half-drawn frame. + -- The panel keeps the fixed size the manifest gives it, and a failure has + -- nowhere near 720x400 to say. Bounded to a readable width and centred, the + -- empty surround reads as composition rather than a half-drawn frame. panel.render(ui.column({ flexGrow = 1, padding = 14, align = "center", justify = "center" }, { ui.column({ gap = 10, width = 320 }, { ui.row({ gap = 8, align = "center" }, { @@ -520,10 +487,9 @@ function render() return end - -- Both panes have to be told to fill the panel, or their ui.scroll children - -- ask for their natural height instead of the height they were given: the - -- cards then overflow the panel and the free space is handed to whatever - -- else in the column will take it, which pushes the header away from them. + -- Both panes have to be told to fill the panel, or their ui.scroll children ask + -- for their natural height: the cards overflow, and the free space goes to + -- whatever else in the column will take it. panel.render(ui.row({ gap = 0, flexGrow = 1, align = "stretch" }, { listPane(entry), ui.separator({ orientation = "vertical", color = "outline", opacity = 0.28 }), @@ -552,9 +518,8 @@ noctalia.state.watch("polling", function(value) end) function onOpen(_context) - -- Every open asks for fresh numbers. The CLI answers from its own cache - -- when it has one, and the poller drops requests that arrive too close - -- together, so reopening the panel repeatedly is cheap. + -- Every open asks for fresh numbers. The CLI answers from its own cache when it + -- has one, and the poller drops requests that arrive too close together. noctalia.state.set("command", { action = "refresh", at = os.time() }) report = noctalia.state.get("report") failure = asFailure(noctalia.state.get("error")) diff --git a/ai-usagebar/service.luau b/ai-usagebar/service.luau index 62ed6284..bb87438d 100644 --- a/ai-usagebar/service.luau +++ b/ai-usagebar/service.luau @@ -1,8 +1,7 @@ --!nonstrict --- Headless poller: the single owner of `ai-usagebar usage --json`. --- --- One call returns every configured vendor, so the capsules and the panel are --- pure subscribers of noctalia.state and never spawn a process of their own. +-- Headless poller: the single owner of `ai-usagebar usage --json`. One call +-- returns every configured vendor, so the capsules and the panel are subscribers +-- of noctalia.state and never spawn a process of their own. -- `ai-usagebar` is a declared dependency, so it is expected on PATH. local COMMAND = "ai-usagebar usage --json" @@ -13,16 +12,14 @@ local function intervalMs() return math.floor(minutes * 60 * 1000) end --- Everything the CLI produces ends up on screen, so all of it is cleaned once, --- here, where it enters the plugin: --- an error can quote the request that failed, and a request can carry a key in --- its query string. A runaway line would also push a bar capsule off screen. --- A secret's value runs until whitespace or the quote or brace that closes it, +-- Everything the CLI prints reaches the screen, so it is cleaned here, on the +-- way in: an error can quote the request that failed, and that request can carry +-- a key. A secret's value runs to the whitespace, quote or brace that closes it, -- so a JSON field loses its value and keeps its punctuation. local SECRET_VALUE = "[^%s\"',}]+" --- Built once. `scrub` calls safeText on every string in the report — ~165 for a --- two-vendor read — and rebuilding these four pattern pairs on each of those --- calls cost more than matching them did. +-- Built once: safeText runs on every string in the report, about 165 of them for +-- a two-vendor read, and rebuilding these pairs each time cost more than matching +-- them. local SECRET_PATTERNS = {} for _, word in ipairs({ "key", "token", "secret", "password" }) do local anyCase = (word:gsub("%a", function(c) return "[" .. c:upper() .. c .. "]" end)) @@ -43,17 +40,14 @@ local function safeText(value) local text = noctalia.string.trim(tostring(value or "")) text = text:gsub("%s+", " ") - -- Capped before the redaction runs rather than after. The patterns are the - -- expensive part of the callback, and a callback that overruns its CPU - -- budget loses the whole report — so the work they do is bounded by what - -- the plugin would draw anyway. + -- Capped before the redaction, not after. The patterns are the expensive part + -- of the callback, and a callback that overruns its CPU budget loses the whole + -- report, so they only ever scan what the plugin would draw. if #text > 200 then text = string.sub(text, 1, 200) .. "..." end - -- Nothing expensive runs until a literal search says it could match, and - -- each keyword opens only its own two patterns. The keyword is what opens - -- the gate. A separator will not do: `=` and `:` both turn up in ordinary - -- readings, in a ratio, a clock time, a URL, so gating on those ran the - -- patterns over almost every string. + -- A literal search gates each keyword's own two patterns. Gating on the + -- separator instead does not work: `=` and `:` turn up in ordinary readings, + -- in ratios, clock times and URLs. local lower = text:lower() for _, secret in ipairs(SECRET_PATTERNS) do @@ -72,8 +66,7 @@ local function safeText(value) text = text:gsub("(://)[^%s/@]+:[^%s/@]+(@)", "%1%2") end - -- The provider key shape this plugin sits next to all day. Anchored at a - -- word start, so "desk-top" is not a key. + -- Anchored at a word start, so "desk-top" is not a key. if lower:find("sk-", 1, true) then text = text:gsub("%f[%w](sk%-)" .. KEY_TAIL .. "[%w_%-]*", "%1") end @@ -81,8 +74,8 @@ local function safeText(value) return text end --- Every string in the report, not just the error: a plan name, an account name --- or a metric detail is CLI text too, and any of them can arrive long. +-- Every string, not just the error: a plan name or a metric detail is CLI text +-- too, and any of them can arrive long. local function scrub(value) if type(value) == "string" then return safeText(value) end if type(value) ~= "table" then return value end @@ -90,25 +83,21 @@ local function scrub(value) return value end --- The one place a failure is named. Subscribers translate the code, and the --- CLI's own text travels with it as `detail`, redacted like any other string --- that reaches the screen. +-- The one place a failure is named. Subscribers translate the code; the CLI's +-- own words travel with it as `detail`. local function failure(code, detail) return { code = code, detail = safeText(detail) } end --- The run's outcome as one code. A missing binary is split out from the --- generic failure because the panel can offer an install link for that one, --- and shells report it as one of two messages. Both arrive with status 127, --- which a CLI that merely cannot open its own config file does not use, so the --- code has to agree with the message before the install link is offered. +-- The run's outcome as one code. A missing binary gets its own, because the +-- panel offers an install link for that one. Shells report it as one of two +-- messages, both with status 127, so code and message have to agree. local function classify(result) if result == nil then return failure("spawn_failed") end if result.timedOut then return failure("timed_out") end - -- Matched raw. `failure` scrubs what it is given, and scrubbing first would - -- mean matching against text already capped at 200 characters, so a noisy - -- run could push the message that names the failure out of reach. + -- Matched raw: `failure` scrubs what it is given, and matching after the + -- 200-character cap would let a noisy run push the message out of reach. local stderr = tostring(result.stderr or "") local lower = stderr:lower() if result.exitCode == 127 @@ -117,8 +106,8 @@ local function classify(result) return failure("not_installed", stderr) end if result.exitCode ~= 0 then - -- Whitespace-only stderr scrubs down to nothing, so the exit code has - -- to answer for it rather than a detail that arrives on screen empty. + -- Whitespace-only stderr scrubs down to nothing, so the exit code answers + -- for it instead. return failure("failed", stderr:find("%S") and stderr or ("ai-usagebar exited with code " .. tostring(result.exitCode))) end @@ -127,9 +116,8 @@ end local inFlight = false --- A floor between spawns. Opening the panel asks for a read, and a panel can be --- opened as fast as a pointer can click, so this bounds how often the plugin --- can start a process no matter how the request arrives. +-- A floor between spawns. Opening the panel asks for a read, and a panel opens as +-- fast as a pointer can click. local MIN_GAP_MS = 2000 local lastStart = 0 @@ -147,8 +135,8 @@ local function refresh() local decoded = result ~= nil and noctalia.json.decode(result.stdout or "") or nil if type(decoded) == "table" and type(decoded.entries) == "table" then - -- A vendor that failed still comes back as an entry with `status = - -- "error"`, so a non-zero exit is not a reason to drop the report. + -- A failed vendor still comes back as an entry with `status = "error"`, + -- so a non-zero exit is no reason to drop the report. noctalia.state.set("report", scrub(decoded)) noctalia.state.set("error", failure("")) return @@ -157,8 +145,8 @@ local function refresh() noctalia.state.set("error", classify(result)) end, 30000) - -- A refusal to spawn never calls back, and without this the poller would - -- sit in flight forever and stop asking. + -- A refusal to spawn never calls back, and the poller would sit in flight + -- forever. if not started then inFlight = false noctalia.state.set("polling", false) @@ -166,7 +154,6 @@ local function refresh() end end --- Manual refresh from a capsule or the panel. noctalia.state.watch("command", function(value) if type(value) == "table" and value.action == "refresh" then refresh() end end) diff --git a/ai-usagebar/shared.luau b/ai-usagebar/shared.luau index 7130e6cf..12823f0e 100644 --- a/ai-usagebar/shared.luau +++ b/ai-usagebar/shared.luau @@ -1,14 +1,12 @@ --!nonstrict --- What the capsule and the panel both need. --- --- One copy, so the ISO parsing, the severity tiers and the provider glyphs --- cannot drift between two entries that have to agree with each other on --- screen. Needs plugin_api 22, which is where require() arrived. +-- What the capsule and the panel both need, in one copy, so the ISO parsing and +-- the severity tiers cannot drift between two entries that have to agree on +-- screen. Needs plugin_api 22, where require() arrived. local M = {} --- Tabler has no Anthropic mark, so providers without a brand glyph get a --- semantic one. Same approach the other CLI-backed meters in this repo take. +-- Tabler has no Anthropic mark, so a provider without a brand glyph gets a +-- semantic one. M.GLYPHS = { anthropic = "asterisk-simple", anthropic_api = "asterisk-simple", @@ -30,17 +28,16 @@ M.GLYPHS = { gemini = "brand-google", } --- The poller names the failure, so a subscriber only ever has a code to --- translate. Anything else in that state slot reads as no failure at all. +-- Anything else in the `error` slot means no failure. M.NO_FAILURE = { code = "", detail = "" } function M.asFailure(value) return type(value) == "table" and value or M.NO_FAILURE end --- "2026-08-15T11:29:59.872624Z" -> unix seconds. The stamps are UTC, so the --- naive os.time() reading (which assumes local time) is corrected by the local --- offset measured at that same instant. +-- "2026-08-15T11:29:59.872624Z" -> unix seconds. The stamps are UTC, and +-- os.time() reads its table as local, so the offset is measured at that same +-- instant and added back. function M.parseIso(value) if type(value) ~= "string" then return nil end local y, mo, d, h, mi, s = value:match("^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)") @@ -71,22 +68,28 @@ function M.countdown(section) return M.formatDuration(at - os.time()) end --- The clock time the countdown lands on: "14:20" today, "Sat 14:20" past --- midnight, and a date once a weekday alone stops naming one day. +-- Where the countdown lands: "14:20" today, "Sat 14:20" past midnight, and a date +-- once a weekday alone stops naming one day. function M.resetClock(section) local at = M.parseIso(section and section.reset_at) if at == nil then return "" end local clock = noctalia.formatTime(noctalia.timeFormat(), at) if os.date("%Y-%m-%d", at) == os.date("%Y-%m-%d") then return clock end - -- The weekday is prepended here instead of folded into the pattern: the - -- host's format grammar passes unknown text through verbatim, so a "ddd" - -- prefix would render as the literal word. + -- Prepended rather than folded into the pattern: the host's format grammar + -- passes unknown text through verbatim, so "ddd" would render as the word. if at - os.time() > 6 * 86400 then return os.date("%d %b", at) .. " " .. clock end return os.date("%a", at) .. " " .. clock end --- A provider can report more than it was given, so the reading is clamped --- before it becomes a bar width. +-- How much of the window is gone, out of "Resets in 1h 58m · 60% elapsed · 30pts +-- ahead". Both entries draw it under the quota bar. +function M.elapsedPercent(detail) + local value = tostring(detail or ""):match("(%d+)%%%s*elapsed") + return value ~= nil and tonumber(value) or nil +end + +-- A provider can report more than it was given, so clamp before this becomes a +-- bar width. function M.ratio(percent) local value = (tonumber(percent) or 0) / 100 if value < 0 then return 0 end @@ -99,9 +102,9 @@ function M.headline(entry) return entry.metrics[1] end --- The CLI tiers every percentage; copying its thresholds here would be a second --- source of truth. `calm` is what to use when it has raised nothing: text stays --- on the surface colour, and the accent is kept for bar fills. +-- The CLI tiers every percentage, and copying its thresholds here would be a +-- second source of truth. `calm` is for when it raised nothing: text stays on the +-- surface colour, and the accent is kept for bar fills. function M.severityRole(section, calm) local severity = tostring(section and section.severity or "") if severity == "critical" then return "error" end diff --git a/ai-usagebar/tests/scrub_test.lua b/ai-usagebar/tests/scrub_test.lua index 0bfe8237..0fca3171 100644 --- a/ai-usagebar/tests/scrub_test.lua +++ b/ai-usagebar/tests/scrub_test.lua @@ -1,15 +1,13 @@ --- Redaction test for service.luau's safeText(). +-- Redaction test for service.luau's safeText() and scrub(). -- --- Everything the CLI writes reaches the screen, and a CLI that fails an HTTP --- request tends to quote the request. safeText is the only thing standing --- between that and a rendered label, so it gets a test. --- --- The functions are read out of service.luau rather than copied here: a copy --- would keep passing after the real one changed. +-- A CLI that fails an HTTP request tends to quote the request, and safeText is +-- the only thing between that and a rendered label. The functions are read out of +-- service.luau rather than copied, so a copy cannot keep passing after the real +-- one changes. -- -- lua tests/scrub_test.lua (or luajit) -- --- Run it from the plugin directory. Exits non-zero on the first failure. +-- Run it from the plugin directory. Exits non-zero if anything fails. local SOURCE = "service.luau" @@ -57,7 +55,7 @@ local SECRETS = { { "OPENAI_API_KEY sk-proj-REALKEYVALUE not accepted", "REALKEY" }, { "password=hunter2", "hunter2" }, -- The cap runs before the patterns, so a secret in a runaway line has to - -- survive being truncated around. + -- survive the truncation. { "api_key=sk-ant-REALKEY123 " .. string.rep("noise ", 60), "REALKEY123" }, } @@ -106,21 +104,17 @@ if #long > 210 then fail("long text was not capped: " .. #long .. " characters") end --- The poller scrubs the whole report inside one async callback, and a callback --- that overruns its CPU budget is killed by the shell: the reading is lost, not --- merely late. So the cost of a scrub is asserted, not just its output. +-- The poller scrubs the whole report inside one async callback, and the shell +-- kills a callback that overruns its CPU budget: the reading is lost, not just +-- late. So the cost is asserted, not only the output. -- --- The meter is `string.gsub`: every pattern in safeText runs through it, and the --- work happens inside the C matcher, where an instruction-count hook sees --- nothing. Counting the calls and the bytes handed to them measures the two --- things that made the 1.2.0 scrubber overrun — a keyword opening all four --- keywords' substitutions, and the length cap running after them instead of --- before. +-- The meter is `string.gsub`, which every pattern in safeText runs through. The +-- work itself happens inside the C matcher, where an instruction-count hook sees +-- nothing, so what gets counted is the calls and the bytes handed to them. -- --- The report below is the shape of a real `usage --json`: four vendors, six +-- The report below has the shape of a real `usage --json`: four vendors, six -- metrics each, and the credential error the CLI writes for a provider it has no --- key for — the string that opens the redaction patterns on an otherwise healthy --- run. +-- key for, which is the string that opens the redaction patterns. local function sampleReport() local entries = {} for _, vendor in ipairs({ "anthropic", "openai", "zai", "openrouter" }) do @@ -163,12 +157,12 @@ end scrub(sampleReport()) string.gsub = realGsub --- Bytes, not calls: the count barely moves, because normalising whitespace is one --- gsub per string either way. What moved is how much text the redaction patterns --- were handed — 37352 bytes for this report in 1.2.0, against 17664 now. The --- ceiling sits between the two, close enough that widening the gate back to all --- four keywords at once (22400) trips it as surely as putting the length cap back --- after the patterns (37352) does. +-- Bytes, not calls: the count barely moves, since normalising whitespace is one +-- gsub per string either way. What moves is how much text the patterns are handed, +-- 37352 bytes for this report before the rewrite against 17664 after. The ceiling +-- sits between the two, near enough that widening the gate back to all four +-- keywords at once (22400) trips it as surely as moving the cap back after the +-- patterns (37352). local MAX_BYTES = 20000 if bytes > MAX_BYTES then fail("the redaction patterns were handed " .. bytes .. " bytes of a four-vendor report" From 8875f0298b60f19c591dfeecaddb9f50ce453eb6 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 21 Aug 2026 11:41:54 -0300 Subject: [PATCH 09/12] ai-usagebar: retake the thumbnail in the official generator The card now shows the panel the way this release draws it: both providers in the list, the two quota bars, and the severity word beside the weekly reading. The previous one was assembled by hand, which the contribution checklist asks against, and it was cropped loose enough that the percentages did not survive being scaled into a catalog card. --- ai-usagebar/thumbnail.webp | Bin 29280 -> 47798 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/ai-usagebar/thumbnail.webp b/ai-usagebar/thumbnail.webp index d3410ad72195e45a53020fbd1a79f7d2e1157a1e..6398d3a6effa7ae93c193a74839ef134ad195529 100644 GIT binary patch literal 47798 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 literal 29280 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 Date: Fri, 21 Aug 2026 12:04:40 -0300 Subject: [PATCH 10/12] ai-usagebar: stamp the refresh command with a millisecond clock `at` is in that payload for one reason: to make each request distinct from the one before it, so a watcher has something to tell them apart by. `os.time()` is whole-second, which is a weak way to promise that, and the state store's contract says nothing either way about what it does with a repeated value. `noctalia.nowMs()` is the only sub-second clock the API offers, and it is already what the poller measures MIN_GAP_MS with. Two clocks for one question was the oversight. The rate limit belongs in the poller, where it is written down and can be read; a coarse stamp in the transport is a second limit nobody declared. Reported by Copilot on #427, which reached the same line by a different route. --- ai-usagebar/bar.luau | 2 +- ai-usagebar/panel.luau | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ai-usagebar/bar.luau b/ai-usagebar/bar.luau index 7c1bfb45..5c1075fa 100644 --- a/ai-usagebar/bar.luau +++ b/ai-usagebar/bar.luau @@ -285,7 +285,7 @@ function onClick() end function onRightClick() - noctalia.state.set("command", { action = "refresh", at = os.time() }) + noctalia.state.set("command", { action = "refresh", at = noctalia.nowMs() }) end report = noctalia.state.get("report") diff --git a/ai-usagebar/panel.luau b/ai-usagebar/panel.luau index 26f2a2a8..1c93cd7c 100644 --- a/ai-usagebar/panel.luau +++ b/ai-usagebar/panel.luau @@ -274,7 +274,7 @@ end -- ── Render ──────────────────────────────────────────────────────────────────── local function requestRefresh() - noctalia.state.set("command", { action = "refresh", at = os.time() }) + noctalia.state.set("command", { action = "refresh", at = noctalia.nowMs() }) end -- The CLI's own words come last and smallest, where a bug report can quote them. @@ -520,7 +520,7 @@ end) function onOpen(_context) -- Every open asks for fresh numbers. The CLI answers from its own cache when it -- has one, and the poller drops requests that arrive too close together. - noctalia.state.set("command", { action = "refresh", at = os.time() }) + noctalia.state.set("command", { action = "refresh", at = noctalia.nowMs() }) report = noctalia.state.get("report") failure = asFailure(noctalia.state.get("error")) polling = noctalia.state.get("polling") == true From 31c0a9c7d78471d19fda599c4775c4a3af6ba596 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 21 Aug 2026 12:15:59 -0300 Subject: [PATCH 11/12] ai-usagebar: keep the refresh request in one place The payload was written out three times, in two files, in the release whose point was to stop both entries from carrying their own copy of things. The millisecond fix had to be made in all three, which is how the duplication announced itself. `shared.requestRefresh()` now owns it, and with it the note that `at` is never read: the poller looks at `action` and nothing else, so the field is there to keep two requests in a row from being the same value. Written down once, in the place a reader will find it, rather than inferred three times from a literal. --- ai-usagebar/bar.luau | 3 ++- ai-usagebar/panel.luau | 7 ++----- ai-usagebar/shared.luau | 8 ++++++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ai-usagebar/bar.luau b/ai-usagebar/bar.luau index 5c1075fa..39042c10 100644 --- a/ai-usagebar/bar.luau +++ b/ai-usagebar/bar.luau @@ -18,6 +18,7 @@ local GLYPHS = shared.GLYPHS local countdown, resetClock = shared.countdown, shared.resetClock local ratio, headline, elapsedPercent = shared.ratio, shared.headline, shared.elapsedPercent local NO_FAILURE, asFailure = shared.NO_FAILURE, shared.asFailure +local requestRefresh = shared.requestRefresh local failure = NO_FAILURE @@ -285,7 +286,7 @@ function onClick() end function onRightClick() - noctalia.state.set("command", { action = "refresh", at = noctalia.nowMs() }) + requestRefresh() end report = noctalia.state.get("report") diff --git a/ai-usagebar/panel.luau b/ai-usagebar/panel.luau index 1c93cd7c..82acf75c 100644 --- a/ai-usagebar/panel.luau +++ b/ai-usagebar/panel.luau @@ -10,6 +10,7 @@ local GLYPHS, parseIso = shared.GLYPHS, shared.parseIso local countdown, resetClock = shared.countdown, shared.resetClock local ratio, headline, severityRole = shared.ratio, shared.headline, shared.severityRole local elapsedPercent = shared.elapsedPercent +local requestRefresh = shared.requestRefresh local NO_FAILURE, asFailure = shared.NO_FAILURE, shared.asFailure local failure = NO_FAILURE @@ -273,10 +274,6 @@ end -- ── Render ──────────────────────────────────────────────────────────────────── -local function requestRefresh() - noctalia.state.set("command", { action = "refresh", at = noctalia.nowMs() }) -end - -- The CLI's own words come last and smallest, where a bug report can quote them. local function errorBlock() local key = "ui.error." .. failure.code @@ -520,7 +517,7 @@ end) function onOpen(_context) -- Every open asks for fresh numbers. The CLI answers from its own cache when it -- has one, and the poller drops requests that arrive too close together. - noctalia.state.set("command", { action = "refresh", at = noctalia.nowMs() }) + requestRefresh() report = noctalia.state.get("report") failure = asFailure(noctalia.state.get("error")) polling = noctalia.state.get("polling") == true diff --git a/ai-usagebar/shared.luau b/ai-usagebar/shared.luau index 12823f0e..85439fb3 100644 --- a/ai-usagebar/shared.luau +++ b/ai-usagebar/shared.luau @@ -28,6 +28,14 @@ M.GLYPHS = { gemini = "brand-google", } +-- Ask the poller for a read. It only looks at `action`; `at` is never read, and +-- is there so two requests in a row are not the same value. nowMs is the only +-- sub-second clock the API has, so os.time() would stamp two clicks in the same +-- second identically. +function M.requestRefresh() + noctalia.state.set("command", { action = "refresh", at = noctalia.nowMs() }) +end + -- Anything else in the `error` slot means no failure. M.NO_FAILURE = { code = "", detail = "" } From 87a3a04f8353effecd647e0ca48559aa64f37a37 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 21 Aug 2026 13:14:54 -0300 Subject: [PATCH 12/12] ai-usagebar: make the right click a gesture binding Reported as a broken button. It was not broken: a probe on the callback and on the poller's chain caught 39 requests from a burst of right clicks, every one of them reaching the watcher. MIN_GAP_MS honoured five and dropped thirty-five, in silence, and the five that ran came back from the CLI's cache fast enough that the capsule's dim was over before it could be seen. On the `meter` style, which draws ticks rather than digits, a fresh reading of the same number looks like nothing happened at all. So the gesture worked and had no way to say so. The half of that worth fixing is not the feedback. It is that the gesture was invisible: `onRightClick` does not appear in the widget's settings, so there was nothing to discover it by and no way to point it elsewhere. Every other plugin in this repo that answers a gesture declares it, and the API notes say why, that a declared action is listed where a Luau callback is not. This one now declares it too, and the callback is gone rather than left to shadow it. Left stays in the script. It sets `selected` before opening the panel so the panel lands on the provider that capsule tracks, which `panel-toggle` on its own cannot do, and the manifest says so next to the binding. README claimed the click "refreshes immediately", which stops being true the second time you press it. It now says a read is asked for, that one process serves every capsule, and that the poller will not start another within two seconds. Also that right is a binding, so it can be reassigned or turned off. --- ai-usagebar/README.md | 7 ++++++- ai-usagebar/bar.luau | 4 ---- ai-usagebar/plugin.toml | 10 ++++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ai-usagebar/README.md b/ai-usagebar/README.md index 6ba749c5..77445014 100644 --- a/ai-usagebar/README.md +++ b/ai-usagebar/README.md @@ -77,9 +77,14 @@ start = [ "clock", "ai_usage" ] clock time the reset lands on. - **Left click** opens the `AI Usage` panel for the provider that capsule tracks. -- **Right click** refreshes immediately. +- **Right click** asks the poller for a read. One process serves every capsule, + and it will not start a second one within two seconds of the last, so holding + the button down does not spawn a queue of them. - **Middle click** opens the widget's settings, as everywhere else in the shell. +Left and middle are the script's; right is a gesture binding, so it is listed in +the widget's settings and can be pointed at any other action, or at `none`. + The panel is a two pane view. On the left is every provider you have set up, with its headline percentage. On the right is the selected one in detail: one card per reported metric, with a quota bar over a thinner "window elapsed" bar, diff --git a/ai-usagebar/bar.luau b/ai-usagebar/bar.luau index 39042c10..0d33091a 100644 --- a/ai-usagebar/bar.luau +++ b/ai-usagebar/bar.luau @@ -18,7 +18,6 @@ local GLYPHS = shared.GLYPHS local countdown, resetClock = shared.countdown, shared.resetClock local ratio, headline, elapsedPercent = shared.ratio, shared.headline, shared.elapsedPercent local NO_FAILURE, asFailure = shared.NO_FAILURE, shared.asFailure -local requestRefresh = shared.requestRefresh local failure = NO_FAILURE @@ -285,9 +284,6 @@ function onClick() noctalia.togglePanel("felipeartur/ai-usagebar:panel") end -function onRightClick() - requestRefresh() -end report = noctalia.state.get("report") failure = asFailure(noctalia.state.get("error")) diff --git a/ai-usagebar/plugin.toml b/ai-usagebar/plugin.toml index dcce3e5f..969172e4 100644 --- a/ai-usagebar/plugin.toml +++ b/ai-usagebar/plugin.toml @@ -36,6 +36,16 @@ entry = "service.luau" id = "bar" entry = "bar.luau" +# Right click asks the poller for a read. Declared rather than handled in +# bar.luau because a binding is what the settings editor lists and what a user +# can point somewhere else; an onRightClick callback is neither. +# +# Left stays in the script: it sets `selected` before opening the panel, so the +# panel lands on the provider this capsule tracks, which `panel-toggle` alone +# cannot do. + [widget.actions] + right = "plugin felipeartur/ai-usagebar:poller all refresh" + # Per-instance, so a second capsule can track a second provider. [[widget.setting]] key = "vendor"