From bb68d90ef2d41edc4685fdad0dd466d554399ff3 Mon Sep 17 00:00:00 2001 From: Hung Pham Sy <25026496+hungps@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:18:16 +0700 Subject: [PATCH 1/5] feat(neuralwatt): add Neuralwatt provider plugin --- plugins/neuralwatt/icon.svg | 3 + plugins/neuralwatt/plugin.js | 148 ++++++++++++ plugins/neuralwatt/plugin.json | 13 ++ plugins/neuralwatt/plugin.test.js | 291 ++++++++++++++++++++++++ src-tauri/src/plugin_engine/host_api.rs | 3 +- 5 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 plugins/neuralwatt/icon.svg create mode 100644 plugins/neuralwatt/plugin.js create mode 100644 plugins/neuralwatt/plugin.json create mode 100644 plugins/neuralwatt/plugin.test.js diff --git a/plugins/neuralwatt/icon.svg b/plugins/neuralwatt/icon.svg new file mode 100644 index 00000000..e011a3e5 --- /dev/null +++ b/plugins/neuralwatt/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/neuralwatt/plugin.js b/plugins/neuralwatt/plugin.js new file mode 100644 index 00000000..71eba38d --- /dev/null +++ b/plugins/neuralwatt/plugin.js @@ -0,0 +1,148 @@ +(function () { + var API_KEY_ENV_VARS = ["NEURALWATT_API_KEY"] + var QUOTA_URL = "https://api.neuralwatt.com/v1/quota" + + function readNumber(value) { + var n = Number(value) + return Number.isFinite(n) ? n : null + } + + function readString(value) { + if (typeof value !== "string") return null + var trimmed = value.trim() + return trimmed || null + } + + function parseDateMs(value) { + if (typeof value === "number") return Number.isFinite(value) ? value : null + if (typeof value === "string") { + var parsed = Date.parse(value) + return Number.isFinite(parsed) ? parsed : null + } + return null + } + + function parseSubscriptionPeriodMs(sub) { + if (!sub || !sub.current_period_start || !sub.current_period_end) return null + var startMs = parseDateMs(sub.current_period_start) + var endMs = parseDateMs(sub.current_period_end) + if (startMs !== null && endMs !== null && endMs > startMs) return endMs - startMs + return null + } + + function loadApiKey(ctx) { + for (var i = 0; i < API_KEY_ENV_VARS.length; i += 1) { + var name = API_KEY_ENV_VARS[i] + var value = null + try { + value = ctx.host.env.get(name) + } catch (e) { + ctx.host.log.warn("env read failed for " + name + ": " + String(e)) + } + if (value && typeof value === "string" && value.trim()) { + ctx.host.log.info("api key loaded from " + name) + return { value: value.trim(), source: name } + } + } + return null + } + + function probe(ctx) { + var apiKeyInfo = loadApiKey(ctx) + if (!apiKeyInfo) { + throw "Neuralwatt API key missing. Set NEURALWATT_API_KEY." + } + + var resp + try { + resp = ctx.util.request({ + method: "GET", + url: QUOTA_URL, + headers: { + Authorization: "Bearer " + apiKeyInfo.value, + Accept: "application/json", + "User-Agent": "OpenUsage", + }, + timeoutMs: 10000, + }) + } catch (e) { + throw "Request failed. Check your connection." + } + + if (ctx.util.isAuthStatus(resp.status)) { + throw "Invalid API key. Check NEURALWATT_API_KEY." + } + if (resp.status < 200 || resp.status >= 300) { + throw "Request failed (HTTP " + String(resp.status) + "). Try again later." + } + + var data = ctx.util.tryParseJson(resp.bodyText) + if (!data || typeof data !== "object") { + throw "Response invalid. Try again later." + } + + var sub = data.subscription && typeof data.subscription === "object" ? data.subscription : null + var balance = data.balance && typeof data.balance === "object" ? data.balance : null + var plan = null + var resetsAt = null + var periodDurationMs = null + + if (sub) { + if (typeof sub.plan === "string" && sub.plan) { + plan = sub.plan.charAt(0).toUpperCase() + sub.plan.slice(1) + } + if (sub.current_period_end) { + var endMs = parseDateMs(sub.current_period_end) + resetsAt = endMs !== null ? ctx.util.toIso(endMs) : null + } + periodDurationMs = parseSubscriptionPeriodMs(sub) + } + + var lines = [] + + // Subscription energy line (hidden if no subscription) + if (sub) { + var kwhIncluded = readNumber(sub.kwh_included) + var kwhUsed = readNumber(sub.kwh_used) + if (kwhIncluded !== null && kwhIncluded > 0 && kwhUsed !== null) { + var energyLine = { + label: "Subscription", + used: Math.round(kwhUsed * 10000) / 10000, + limit: Math.round(kwhIncluded * 10000) / 10000, + format: { kind: "count", suffix: "kWh" }, + } + if (resetsAt) energyLine.resetsAt = resetsAt + if (periodDurationMs) energyLine.periodDurationMs = periodDurationMs + lines.push(ctx.line.progress(energyLine)) + } + } + + // Balance line (hidden if total credits is 0) + if (balance) { + var totalCredits = readNumber(balance.total_credits_usd) + var usedCredits = readNumber(balance.credits_used_usd) + if (totalCredits !== null && totalCredits > 0 && usedCredits !== null) { + lines.push(ctx.line.progress({ + label: "Balance", + used: Math.round(usedCredits * 100) / 100, + limit: Math.round(totalCredits * 100) / 100, + format: { kind: "dollars" }, + })) + } + + // Accounting method badge + var method = readString(balance.accounting_method) + if (method) { + lines.push(ctx.line.badge({ label: "Method", text: method.charAt(0).toUpperCase() + method.slice(1) })) + } + } + + if (lines.length === 0) { + lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" })) + } + + return { plan: plan, lines: lines } + } + + globalThis.__openusage_plugin = { id: "neuralwatt", probe: probe } +})() diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json new file mode 100644 index 00000000..167fbd93 --- /dev/null +++ b/plugins/neuralwatt/plugin.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "id": "neuralwatt", + "name": "Neuralwatt", + "version": "0.0.2", + "entry": "plugin.js", + "icon": "icon.svg", + "brandColor": "#D55934", + "lines": [ + { "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 }, + { "type": "progress", "label": "Balance", "scope": "detail" } + ] +} diff --git a/plugins/neuralwatt/plugin.test.js b/plugins/neuralwatt/plugin.test.js new file mode 100644 index 00000000..ddd47fb9 --- /dev/null +++ b/plugins/neuralwatt/plugin.test.js @@ -0,0 +1,291 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" +import { makeCtx } from "../test-helpers.js" + +const loadPlugin = async () => { + await import("./plugin.js") + return globalThis.__openusage_plugin +} + +const FULL_RESPONSE = { + snapshot_at: "2026-04-16T18:30:00Z", + balance: { + credits_remaining_usd: 32.6774, + total_credits_usd: 52.34, + credits_used_usd: 19.6626, + accounting_method: "energy", + }, + usage: { + lifetime: { cost_usd: 243.9145, requests: 37801, tokens: 1235477176, energy_kwh: 15.6009 }, + current_month: { cost_usd: 160.1463, requests: 23902, tokens: 1116658995, energy_kwh: 9.7278 }, + }, + limits: { overage_limit_usd: null, rate_limit_tier: "standard" }, + subscription: { + plan: "standard", + status: "active", + billing_interval: "month", + current_period_start: "2026-04-11T05:05:25Z", + current_period_end: "2026-05-11T05:05:25Z", + auto_renew: true, + kwh_included: 20.0, + kwh_used: 13.9023, + kwh_remaining: 6.0977, + in_overage: false, + }, + key: { name: "my-production-key", allowance: null }, +} + +describe("neuralwatt plugin", () => { + beforeEach(() => { + delete globalThis.__openusage_plugin + vi.resetModules() + }) + + it("throws when API key is missing", async () => { + const ctx = makeCtx() + const plugin = await loadPlugin() + expect(() => plugin.probe(ctx)).toThrow("Neuralwatt API key missing") + }) + + it("renders subscription + balance + method from full response", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify(FULL_RESPONSE), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + + expect(result.plan).toBe("Standard") + expect(result.lines).toHaveLength(3) + + const sub = result.lines.find((l) => l.label === "Subscription") + expect(sub).toBeTruthy() + expect(sub.used).toBeCloseTo(13.9023, 4) + expect(sub.limit).toBeCloseTo(20, 4) + expect(sub.format).toEqual({ kind: "count", suffix: "kWh" }) + + const bal = result.lines.find((l) => l.label === "Balance") + expect(bal).toBeTruthy() + expect(bal.used).toBe(19.66) + expect(bal.limit).toBe(52.34) + expect(bal.format).toEqual({ kind: "dollars" }) + expect(bal.resetsAt).toBeUndefined() + expect(bal.periodDurationMs).toBeUndefined() + + const method = result.lines.find((l) => l.label === "Method") + expect(method).toBeTruthy() + expect(method.text).toBe("Energy") + }) + + it("includes resetsAt and periodDurationMs from subscription period", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify(FULL_RESPONSE), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + + const sub = result.lines.find((l) => l.label === "Subscription") + expect(sub.resetsAt).toBeTruthy() + expect(sub.periodDurationMs).toBeTruthy() + }) + + it("hides subscription line when subscription is null", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify({ + balance: { credits_remaining_usd: 10, total_credits_usd: 20, credits_used_usd: 10, accounting_method: "token" }, + subscription: null, + }), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + + expect(result.lines.find((l) => l.label === "Subscription")).toBeUndefined() + const bal = result.lines.find((l) => l.label === "Balance") + expect(bal).toBeTruthy() + }) + + it("hides balance line when total credits is 0", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify({ + balance: { credits_remaining_usd: 0, total_credits_usd: 0, credits_used_usd: 0, accounting_method: "energy" }, + subscription: FULL_RESPONSE.subscription, + }), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + + expect(result.lines.find((l) => l.label === "Balance")).toBeUndefined() + expect(result.lines.find((l) => l.label === "Subscription")).toBeTruthy() + }) + + it("hides method badge when accounting_method is missing", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify({ + balance: { credits_remaining_usd: 10, total_credits_usd: 20, credits_used_usd: 10 }, + subscription: FULL_RESPONSE.subscription, + }), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + + expect(result.lines.find((l) => l.label === "Method")).toBeUndefined() + }) + + it("returns badge when no data at all", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify({ subscription: null, balance: null }), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + expect(result.lines).toEqual([{ type: "badge", label: "Status", text: "No usage data", color: "#a3a3a3" }]) + expect(result.plan).toBeNull() + }) + + it("throws on 401", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-bad-key" + return null + }) + ctx.host.http.request.mockReturnValue({ status: 401, bodyText: "" }) + + const plugin = await loadPlugin() + expect(() => plugin.probe(ctx)).toThrow("Invalid API key") + }) + + it("throws on non-2xx", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ status: 500, bodyText: "" }) + + const plugin = await loadPlugin() + expect(() => plugin.probe(ctx)).toThrow("HTTP 500") + }) + + it("throws on invalid JSON", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockReturnValue({ status: 200, bodyText: "not-json" }) + + const plugin = await loadPlugin() + expect(() => plugin.probe(ctx)).toThrow("Response invalid") + }) + + it("throws on network error", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + ctx.host.http.request.mockImplementation(() => { + throw new Error("network down") + }) + + const plugin = await loadPlugin() + expect(() => plugin.probe(ctx)).toThrow("Check your connection") + }) + + it("capitalizes plan name", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + const resp = JSON.parse(JSON.stringify(FULL_RESPONSE)) + resp.subscription.plan = "premium" + + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify(resp), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + expect(result.plan).toBe("Premium") + }) + + it("capitalizes accounting method", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-test-key" + return null + }) + const resp = JSON.parse(JSON.stringify(FULL_RESPONSE)) + resp.balance.accounting_method = "token" + + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify(resp), + }) + + const plugin = await loadPlugin() + const result = plugin.probe(ctx) + expect(result.lines.find((l) => l.label === "Method").text).toBe("Token") + }) + + it("sends correct authorization header", async () => { + const ctx = makeCtx() + ctx.host.env.get.mockImplementation((name) => { + if (name === "NEURALWATT_API_KEY") return "sk-my-key" + return null + }) + ctx.host.http.request.mockReturnValue({ + status: 200, + bodyText: JSON.stringify(FULL_RESPONSE), + }) + + const plugin = await loadPlugin() + plugin.probe(ctx) + + const call = ctx.host.http.request.mock.calls[0][0] + expect(call.headers.Authorization).toBe("Bearer sk-my-key") + expect(call.url).toBe("https://api.neuralwatt.com/v1/quota") + expect(call.method).toBe("GET") + }) +}) diff --git a/src-tauri/src/plugin_engine/host_api.rs b/src-tauri/src/plugin_engine/host_api.rs index a39ac09d..40b94b94 100644 --- a/src-tauri/src/plugin_engine/host_api.rs +++ b/src-tauri/src/plugin_engine/host_api.rs @@ -12,7 +12,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::{Mutex, OnceLock}; -const WHITELISTED_ENV_VARS: [&str; 16] = [ +const WHITELISTED_ENV_VARS: [&str; 17] = [ "CODEX_HOME", "CLAUDE_CONFIG_DIR", "CLAUDE_CODE_OAUTH_TOKEN", @@ -29,6 +29,7 @@ const WHITELISTED_ENV_VARS: [&str; 16] = [ "MINIMAX_CN_API_KEY", "SYNTHETIC_API_KEY", "PI_CODING_AGENT_DIR", + "NEURALWATT_API_KEY", ]; fn last_non_empty_trimmed_line(text: &str) -> Option { From d4f5fc05fab03cf9732eac203bc73ab74fd7feaf Mon Sep 17 00:00:00 2001 From: Hung Pham Sy <25026496+hungps@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:04:00 +0700 Subject: [PATCH 2/5] docs(neuralwatt): add Neuralwatt docs --- README.md | 1 + docs/providers/neuralwatt.md | 70 ++++++++++++++++++++++++++++++++++ plugins/neuralwatt/plugin.js | 8 +++- plugins/neuralwatt/plugin.json | 2 +- 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 docs/providers/neuralwatt.md diff --git a/README.md b/README.md index d992761a..2c96c17e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ OpenUsage lives in your menu bar and shows you how much of your AI coding subscr - [**Kiro**](docs/providers/kiro.md) / credits, bonus credits, overages - [**Kimi Code**](docs/providers/kimi.md) / session, weekly - [**MiniMax**](docs/providers/minimax.md) / coding plan session +- [**Neuralwatt**](docs/providers/neuralwatt.md) / subscription energy, balance credits - [**OpenCode Go**](docs/providers/opencode-go.md) / 5h, weekly, monthly spend limits - [**Windsurf**](docs/providers/windsurf.md) / prompt credits, flex credits - [**Z.ai**](docs/providers/zai.md) / session, weekly, web searches diff --git a/docs/providers/neuralwatt.md b/docs/providers/neuralwatt.md new file mode 100644 index 00000000..3baed3d0 --- /dev/null +++ b/docs/providers/neuralwatt.md @@ -0,0 +1,70 @@ +# Neuralwatt + +> Uses the Neuralwatt quota API with a user-provided API key. + +## Overview + +- **Protocol:** HTTPS (JSON) +- **Endpoint:** `GET https://api.neuralwatt.com/v1/quota` +- **Auth:** `Authorization: Bearer ` +- **Env var:** `NEURALWATT_API_KEY` + +## Authentication + +The plugin reads `NEURALWATT_API_KEY` from the environment. If the key is missing, it throws: + +- `Neuralwatt API key missing. Set NEURALWATT_API_KEY.` + +## Data Source + +Request: + +```http +GET /v1/quota HTTP/1.1 +Host: api.neuralwatt.com +Authorization: Bearer +Accept: application/json +User-Agent: OpenUsage +``` + +Expected payload fields: + +- `balance.credits_remaining_usd`, `balance.total_credits_usd`, `balance.credits_used_usd` +- `balance.accounting_method` (e.g. `"energy"`, `"token"`) +- `subscription.plan`, `subscription.status`, `subscription.billing_interval` +- `subscription.current_period_start`, `subscription.current_period_end` +- `subscription.kwh_included`, `subscription.kwh_used`, `subscription.kwh_remaining` +- `subscription.auto_renew`, `subscription.in_overage` + +## Usage Mapping + +- **Subscription** (progress line): `kwh_used` / `kwh_included` in kWh. Shown only when subscription is present and `kwh_included > 0`. +- **Balance** (progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`. +- **Method** (badge): `accounting_method`, capitalized. Hidden when absent. + +Both lines include `resetsAt` and `periodDurationMs` from the subscription period dates when available. + +## Output + +- **Plan**: from `subscription.plan`, capitalized +- **Subscription** (overview progress line): + - `format`: count with `kWh` suffix + - `used`: `kwh_used` + - `limit`: `kwh_included` + - `resetsAt`: from `current_period_end` + - `periodDurationMs`: `current_period_end` – `current_period_start` +- **Balance** (detail progress line): + - `format`: dollars + - `used`: `credits_used_usd` + - `limit`: `total_credits_usd` +- **Method** (badge): capitalized `accounting_method` + +## Errors + +| Condition | Message | +|---|---| +| Missing API key | `Neuralwatt API key missing. Set NEURALWATT_API_KEY.` | +| HTTP 401/403 | `Invalid API key. Check NEURALWATT_API_KEY.` | +| Non-2xx | `Request failed (HTTP {status}). Try again later.` | +| Network failure | `Request failed. Check your connection.` | +| Unparseable payload | `Response invalid. Try again later.` | diff --git a/plugins/neuralwatt/plugin.js b/plugins/neuralwatt/plugin.js index 71eba38d..8954529b 100644 --- a/plugins/neuralwatt/plugin.js +++ b/plugins/neuralwatt/plugin.js @@ -3,8 +3,12 @@ var QUOTA_URL = "https://api.neuralwatt.com/v1/quota" function readNumber(value) { - var n = Number(value) - return Number.isFinite(n) ? n : null + if (typeof value === "number") return Number.isFinite(value) ? value : null + if (typeof value === "string") { + var parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null + } + return null } function readString(value) { diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json index 167fbd93..e495583d 100644 --- a/plugins/neuralwatt/plugin.json +++ b/plugins/neuralwatt/plugin.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "id": "neuralwatt", "name": "Neuralwatt", - "version": "0.0.2", + "version": "0.0.1", "entry": "plugin.js", "icon": "icon.svg", "brandColor": "#D55934", From e169a6991d376ee38f0a5d538d1b1a53d98bd043 Mon Sep 17 00:00:00 2001 From: Hung Pham Sy <25026496+hungps@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:16:34 +0700 Subject: [PATCH 3/5] docs(neuralwatt): resetAt and periodDurationMs only supports subscription Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/providers/neuralwatt.md | 2 +- plugins/neuralwatt/plugin.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/providers/neuralwatt.md b/docs/providers/neuralwatt.md index 3baed3d0..37a6be7f 100644 --- a/docs/providers/neuralwatt.md +++ b/docs/providers/neuralwatt.md @@ -42,7 +42,7 @@ Expected payload fields: - **Balance** (progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`. - **Method** (badge): `accounting_method`, capitalized. Hidden when absent. -Both lines include `resetsAt` and `periodDurationMs` from the subscription period dates when available. +The **Subscription** line includes `resetsAt` and `periodDurationMs` from the subscription period dates when available; the **Balance** line does not. ## Output diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json index e495583d..558edbf7 100644 --- a/plugins/neuralwatt/plugin.json +++ b/plugins/neuralwatt/plugin.json @@ -8,6 +8,7 @@ "brandColor": "#D55934", "lines": [ { "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 }, - { "type": "progress", "label": "Balance", "scope": "detail" } + { "type": "progress", "label": "Method", "scope": "overview", "primaryOrder": 2 }, + { "type": "progress", "label": "Balance", "scope": "overview", "primaryOrder": 3 } ] } From dde3dd0c581163d90ad9979d62d2fca9cba611e2 Mon Sep 17 00:00:00 2001 From: Hung Pham Sy <25026496+hungps@users.noreply.github.com> Date: Sun, 3 May 2026 14:53:43 +0700 Subject: [PATCH 4/5] fix(neuralwatt): correct Method line type from progress to badge plugin.json declared Method as a progress line with primaryOrder, but probe() emits it as ctx.line.badge(). This mismatch caused wrong UI skeleton rendering and incorrect tray primary-candidate ordering. --- plugins/neuralwatt/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json index 558edbf7..3f80f4af 100644 --- a/plugins/neuralwatt/plugin.json +++ b/plugins/neuralwatt/plugin.json @@ -8,7 +8,7 @@ "brandColor": "#D55934", "lines": [ { "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 }, - { "type": "progress", "label": "Method", "scope": "overview", "primaryOrder": 2 }, + { "type": "badge", "label": "Method", "scope": "overview" }, { "type": "progress", "label": "Balance", "scope": "overview", "primaryOrder": 3 } ] } From fd78d3f0b30d81db74a4431fc09932282895d0a6 Mon Sep 17 00:00:00 2001 From: Hung Pham Sy <25026496+hungps@users.noreply.github.com> Date: Sun, 3 May 2026 20:26:15 +0700 Subject: [PATCH 5/5] fix(neuralwatt): align manifest line order, scopes, and Status badge with probe and docs --- docs/providers/neuralwatt.md | 11 ++++++----- plugins/neuralwatt/plugin.json | 5 +++-- plugins/neuralwatt/plugin.test.js | 3 +++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/providers/neuralwatt.md b/docs/providers/neuralwatt.md index 37a6be7f..d6482a80 100644 --- a/docs/providers/neuralwatt.md +++ b/docs/providers/neuralwatt.md @@ -38,9 +38,9 @@ Expected payload fields: ## Usage Mapping -- **Subscription** (progress line): `kwh_used` / `kwh_included` in kWh. Shown only when subscription is present and `kwh_included > 0`. -- **Balance** (progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`. -- **Method** (badge): `accounting_method`, capitalized. Hidden when absent. +- **Subscription** (overview progress line): `kwh_used` / `kwh_included` in kWh. Shown only when subscription is present and `kwh_included > 0`. +- **Balance** (overview progress line): `credits_used_usd` / `total_credits_usd` in dollars. Shown only when `total_credits_usd > 0`. +- **Method** (detail badge): `accounting_method`, capitalized. Hidden when absent. The **Subscription** line includes `resetsAt` and `periodDurationMs` from the subscription period dates when available; the **Balance** line does not. @@ -53,11 +53,12 @@ The **Subscription** line includes `resetsAt` and `periodDurationMs` from the su - `limit`: `kwh_included` - `resetsAt`: from `current_period_end` - `periodDurationMs`: `current_period_end` – `current_period_start` -- **Balance** (detail progress line): +- **Balance** (overview progress line): - `format`: dollars - `used`: `credits_used_usd` - `limit`: `total_credits_usd` -- **Method** (badge): capitalized `accounting_method` +- **Method** (detail badge): capitalized `accounting_method` +- **Status** (overview badge): shown as "No usage data" (gray) when subscription and balance are both absent ## Errors diff --git a/plugins/neuralwatt/plugin.json b/plugins/neuralwatt/plugin.json index 3f80f4af..cc2f0177 100644 --- a/plugins/neuralwatt/plugin.json +++ b/plugins/neuralwatt/plugin.json @@ -8,7 +8,8 @@ "brandColor": "#D55934", "lines": [ { "type": "progress", "label": "Subscription", "scope": "overview", "primaryOrder": 1 }, - { "type": "badge", "label": "Method", "scope": "overview" }, - { "type": "progress", "label": "Balance", "scope": "overview", "primaryOrder": 3 } + { "type": "progress", "label": "Balance", "scope": "overview" }, + { "type": "badge", "label": "Method", "scope": "detail" }, + { "type": "badge", "label": "Status", "scope": "overview" } ] } diff --git a/plugins/neuralwatt/plugin.test.js b/plugins/neuralwatt/plugin.test.js index ddd47fb9..02dbdfd0 100644 --- a/plugins/neuralwatt/plugin.test.js +++ b/plugins/neuralwatt/plugin.test.js @@ -80,6 +80,9 @@ describe("neuralwatt plugin", () => { const method = result.lines.find((l) => l.label === "Method") expect(method).toBeTruthy() expect(method.text).toBe("Energy") + + // Line order must match manifest: Subscription, Balance, Method + expect(result.lines.map((l) => l.label)).toEqual(["Subscription", "Balance", "Method"]) }) it("includes resetsAt and periodDurationMs from subscription period", async () => {