diff --git a/.gitignore b/.gitignore index ddda2a9..962e4d4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ out/ dist/ *.vsix coverage/ +*.tsbuildinfo +.vscode-test/ +.playwright-mcp/ diff --git a/.vscodeignore b/.vscodeignore index 5853257..591d713 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -19,10 +19,11 @@ scripts/ .editorconfig .gitattributes .gitignore -AGENTS.md .nvmrc -CONTRIBUTING.md +AGENTS.md +CITATION.cff CODE_OF_CONDUCT.md +CONTRIBUTING.md SECURITY.md !LICENSE !NOTICE diff --git a/AGENTS.md b/AGENTS.md index d557853..3a7bc51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,31 +1,150 @@ -# Repository Guidelines +# AGENTS.md — Chutes Usage Monitor -## Project Structure & Module Organization +Guidance for humans and AI agents working in this repository. Read it fully before changing anything. -This repository is a VS Code extension for monitoring Chutes usage. Extension-host TypeScript lives in `src/`, with API access in `src/services/`, state in `src/state/`, status bar UI in `src/status/`, and webview registration in `src/views/`. Browser-side webview code lives in `webview/` and is compiled separately as ES modules. Tests are in `src/test/`. Static assets are in `media/`, while user-facing docs live in `docs/` plus root files such as `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, and `CHANGELOG.md`. +## Project Overview -## Build, Test, and Development Commands +`Chutes Usage Monitor` is an **unofficial, third-party VS Code extension** that shows a Chutes account's subscription usage, rolling limits, daily request quotas, and pay-as-you-go credit inside a sidebar dashboard and an optional status bar item. It is read-only: it never mutates the user's Chutes account. -- `npm install`: install dependencies from `package-lock.json`. -- `npm run compile`: compile extension-host code, compile webview code, and copy `webview/styles.css` into `out/webview/`. -- `npm test`: run compile, then execute Node test files from `out/src/test/**/*.test.js`. -- `npm run package`: build a VSIX package with `vsce`. -- `npm run vscode:prepublish`: run the compile step used before publishing. +The repository is **public** on GitHub, MIT licensed, and published to both the **Visual Studio Code Marketplace** and **Open VSX** under the publisher `mikesoft` (extension id `mikesoft.chutes-usage-vscode`). -For manual testing, open the repo in VS Code and press `F5` to launch an Extension Development Host. +## Stack And Runtime -## Coding Style & Naming Conventions +- TypeScript with `strict` enabled, compiled by `typescript@^7` +- VS Code Extension API, target `engines.vscode: ^1.103.0` +- Node.js `22` (`.nvmrc` pins `22.17.0`; `@types/node` is pinned to the same major) +- Test runner: built-in `node:test` + `node:assert/strict` +- Package manager: **npm only**, `package-lock.json` is authoritative. Never add a second lockfile or package manager. +- No runtime `dependencies`. Everything shipped is compiled from this repository; `devDependencies` exist only for build, packaging, and Codicons assets. -Use TypeScript with `strict` enabled. Follow the existing style: two-space indentation in JSON, no semicolons in TypeScript, single quotes for strings, and concise comments only where behavior is not obvious. Name classes and types in `PascalCase`, functions and variables in `camelCase`, and tests as `*.test.ts`. Keep extension-host code and browser webview code separated; do not import VS Code APIs into `webview/`. +## Architecture -## Testing Guidelines +Two independently compiled worlds that only talk through `postMessage`: -Tests use the built-in `node:test` runner and `node:assert/strict`. Add or update tests for API normalization, dashboard state transitions, webview bootstrap behavior, lifecycle cleanup, and security-sensitive changes. Prefer focused tests with behavior-oriented names, for example `skips per-chute fallback quota usage when aggregate usage is available`. Run `npm test` before handing off changes. +1. **Extension host** (`src/`, `tsconfig.json`, `module: node16`) + - `services/ChutesApiClient.ts` — authenticated `fetch` against `https://api.chutes.ai`, 15 s abort timeout per request. `/users/me/subscription_usage` and `/users/me/quotas` are required; `/pricing`, `/users/me/quota_usage/me`, `/users/me/quota_usage/{chute_id}`, `/invocations/stats/llm`, `/users/me` are optional and fail soft. + - `services/normalize.ts` — defensive normalization of loose API payloads into `DashboardData`, plus the compact status bar summary. All API shape tolerance lives here. + - `services/SecretStore.ts` — the only place the API key is read or written, always through `vscode.SecretStorage`. + - `services/externalLinks.ts` — https-only allowlist (`https://chutes.ai`) for anything the webview asks the host to open. + - `state/DashboardStore.ts` — state machine (`missing-key` → `loading` → `ready`/`error`) with a monotonic `refreshVersion` guard against out-of-order refreshes. + - `state/webviewState.ts` — projects the host state down to what the webview renders before it crosses the boundary. + - `status/StatusBarController.ts` — the single status bar item. + - `views/ChutesWebviewProvider.ts` — builds the webview HTML with a per-load CSP nonce and validates every inbound message. + - `extension.ts` — wires everything and registers **every** disposable on `context.subscriptions` exactly once during `activate`. + - `lifecycle.ts` / `lifecycleTargets.ts` — best-effort `vscode:uninstall` cleanup of global storage folders. +2. **Webview** (`webview/`, `tsconfig.webview.json`, `module: ES2022`) — plain DOM, no framework, no bundler. `webview/types.ts` mirrors the subset of `src/types.ts` the dashboard needs and must be kept in sync manually. **Never import `vscode` from `webview/`.** -## Commit & Pull Request Guidelines +`scripts/copy-webview-assets.mjs` copies `webview/styles.css` and the Codicons CSS/TTF into `out/webview/`. It is pure Node so the build works on Windows, macOS, and Linux. -Recent history uses Conventional Commit prefixes such as `feat:`, `fix:`, and `chore:`. Keep messages specific, for example `fix: clarify dashboard reset labels`. Pull requests should include a short description, linked issues when relevant, test results, and screenshots or notes for visible webview changes. Update `CHANGELOG.md` for user-visible changes and update docs when setup, behavior, or support guidance changes. +## Commands -## Security & Configuration Tips +All commands are real `npm` scripts from `package.json`: -Never log API keys or account payloads containing sensitive data. API keys must stay in VS Code `SecretStorage`. Validate webview messages defensively and allowlist external links before calling VS Code APIs. If documenting API shape issues, include only redacted payload samples. +| Purpose | Command | +| --- | --- | +| Install | `npm ci` | +| Build | `npm run compile` | +| Watch (host only) | `npm run watch` | +| Unit tests only | `npm run test:unit` | +| Compile + tests | `npm test` | +| Dependency audit | `npm run audit` | +| Tests + audit | `npm run check` | +| Package VSIX | `npm run package` | +| Full gate before a PR | `npm run preflight` | + +There is no separate lint or format step — `tsc` with `strict`, `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`, and `noUncheckedSideEffectImports` is the type and hygiene gate. Do not weaken those flags to make code compile. + +Manual testing: open the repo in VS Code and press `F5` for an Extension Development Host. + +## Coding Conventions + +- Two-space indentation, LF line endings (`.gitattributes` enforces `eol=lf`), UTF-8, final newline. +- TypeScript: **no semicolons**, single quotes, `PascalCase` for classes and types, `camelCase` for functions and variables, tests named `*.test.ts` under `src/test/`. +- Comments only where behavior is not obvious, especially around API-shape tolerance and security decisions. +- Prefer small, behavior-named tests, e.g. `skips per-chute fallback quota usage when aggregate usage is available`. +- Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `ci:`, `build:`). + +## Generated Files — Do Not Edit + +- `out/` — build output, git-ignored. +- `*.vsix` — packaging output, git-ignored. +- `package-lock.json` — change it only through npm, never by hand. +- `allowScripts` in `package.json` — maintained by `npm approve-scripts`, not manually. + +## Assets — Do Not Alter + +`media/icon.png`, `media/icon.svg`, and `media/screenshot-chutes-usage.png` are fixed. Do not redesign, recolor, resize, rename, move, or regenerate them. `media/icon.svg` is intentionally a single-color Activity Bar icon while `media/icon.png` is the full-artwork Marketplace icon — that difference is deliberate and documented in `docs/troubleshooting.md`. The icon composition embeds the Chutes mark under the terms described in `NOTICE`. + +## Security Rules + +- The API key lives **only** in `vscode.SecretStorage`. Never write it to a workspace file, a setting, a log, an error message, or the webview. +- Never log or persist account payloads. The extension keeps no local usage history. +- Every inbound webview message must pass `isWebviewActionMessage` before it reaches an action; every payload the webview receives must pass the validators in `webview/messages.ts` before it is rendered or cached. +- External navigation goes through `getAllowedExternalUri`. Adding a host to that allowlist is a security decision, not a convenience. +- The webview CSP uses a fresh nonce per load with `default-src 'none'`. Do not add `unsafe-eval`, remote origins, or inline scripts. +- Never introduce a new outbound endpoint outside `https://api.chutes.ai` without documenting it in `docs/troubleshooting.md` and the PR privacy section. +- No secrets in the repository, in commits, or in CI logs. Secret scanning and push protection are enabled on GitHub. + +## Compatibility And Anti-Breaking-Change Rules + +- Do not raise `engines.vscode` or drop APIs that existed at `1.103.0` without an explicit, authorized major bump. +- Do not rename commands, setting keys (`chutesUsageVscode.*`), the view container id, the view id, or the `SECRET_KEY_API_TOKEN` value — these are user-visible or user-persisted contracts. +- Bump `SCHEMA_VERSION` in `webview/main.ts` when the shape of the cached webview payload changes; stale caches must be discarded, never misread. +- API responses may change shape at any time. Extend `normalize.ts` defensively and add a test for the new shape rather than assuming a field exists. +- Prefer showing `--` over a possibly wrong `0` when data cannot be verified. + +## Environment Variables + +The extension itself reads no environment variables at runtime. Only tooling does: + +- `VSCE_PAT` — Visual Studio Marketplace token, used by `vsce publish`. +- `OVSX_PAT` — Open VSX token, used by `ovsx publish`. + +Both are supplied by the maintainer at publish time. They are **not** stored in the repository and there are no GitHub Actions secrets configured for them. + +## Validation Criteria (mandatory before handing off) + +1. `npm ci` succeeds. +2. `npm run compile` succeeds with zero TypeScript errors. +3. `npm test` passes — all suites, no skips. +4. `npm run audit` reports no high or critical advisories. +5. `npm run package` produces a VSIX; inspect it with `npx vsce ls --tree` and confirm it contains only `out/` runtime code, `media/`, and the user-facing root documents — no sources, no maps, no tests, no secrets. +6. `CHANGELOG.md`, `package.json`, `package-lock.json`, `CITATION.cff`, the Git tag, and the GitHub Release must all state the same version. + +## Release And Publishing + +Versioning is SemVer: patch for fixes, cleanup, and docs; minor for backward-compatible features; major only for authorized breaking changes. + +`main` is protected: linear history, one required approving review, and the `build` status check. Work on a dedicated branch and open a pull request — never push directly, never force push, never self-approve. + +Release sequence once the PR is merged: + +```bash +npm version --no-git-tag-version # updates package.json and package-lock.json together +# Update CITATION.cff and CHANGELOG.md to the same version before validation. +npm run preflight +git tag v && git push origin v +gh release create v --title "..." --notes-file +npx vsce publish --packagePath chutes-usage-vscode-.vsix # needs VSCE_PAT +npx ovsx publish chutes-usage-vscode-.vsix # needs OVSX_PAT +``` + +Publishing is manual by design: there is no publish workflow and no GitHub Actions secret. If `VSCE_PAT` or `OVSX_PAT` is absent, build and tag the release, then stop and report the missing credential — never simulate a publish. + +## Repository Visibility Behavior + +This repository is public. Assume everything committed here is world-readable forever: + +- no internal endpoints, customer data, account identifiers, or unredacted API payloads +- redact sample payloads in issues and tests, keeping field names and numeric values +- keep documentation accurate and free of unverifiable claims, fake badges, or invented statistics +- community files (`CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, `SUPPORT.md`, issue and PR templates) are part of the public contract — keep them current + +## Notes For AI Agents + +- Read `README.md`, `CONTRIBUTING.md`, `CHANGELOG.md`, and `docs/` before proposing changes. +- Keep diffs minimal and focused. Do not reformat untouched files, and do not "modernize" working code without a concrete defect. +- Never disable a test, a compiler flag, or an audit to make a gate pass. +- When you touch `src/types.ts`, check whether `webview/types.ts` and the validators in `webview/messages.ts` need the same change. +- When you touch a user-visible behavior, update `CHANGELOG.md` and the relevant file in `docs/` in the same change. +- Do not create new top-level files unless they are genuinely required; prefer extending an existing document. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c126bc..5ebc433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## [Unreleased] +## 0.5.4 — 2026-08-01 + +- Stopped sending the per-model quota rows to the dashboard webview. They are only used by the extension host to derive the daily window, so every state message and the state the webview persists are now smaller. No visible change to the dashboard. +- Removed the unused `summarizeStatusBar` helper together with its dead `includePrefix` branch, which selected between two identical results. The compact status bar summary that the extension actually renders is unchanged and now has direct test coverage for unverified request usage (`--`) and unlimited request quotas (`∞`). +- Removed `webview/index.html`. The webview markup has been generated by the extension host with a per-load CSP nonce since `0.4.x`, and the leftover template was neither compiled nor packaged. +- Removed the April 2026 pre-implementation plan and design notes from `docs/`. They described a scope that no longer matches the shipped extension; the content stays available in the Git history. +- Synchronized `CITATION.cff` with the released version and added the release date, software type, and repository metadata. +- Declared the reviewed dependency install scripts through npm's `allowScripts` field so `npm ci` stays deterministic on npm 11 and later. +- Synchronized `package-lock.json` with the 0.5.4 manifest and added a regression test that keeps the manifest, lockfile, and citation version aligned. +- Excluded `CITATION.cff` from the packaged VSIX and added local build and tooling artifacts (`*.tsbuildinfo`, `.vscode-test/`, `.playwright-mcp/`) to `.gitignore`. +- Expanded `README.md` with requirements, the Open VSX install path, verified development commands, and the repository layout, and rewrote `AGENTS.md` as a project-specific contributor and agent guide. + ## 0.5.3 - Hardened webview state handling with runtime validation for extension messages and cached payloads. diff --git a/CITATION.cff b/CITATION.cff index 8198edd..82a555e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,9 +1,12 @@ cff-version: 1.2.0 message: "If you use this software, please cite it as below." -title: "Chutes Usage for VS Code" +title: "Chutes Usage Monitor for VS Code" +type: software authors: - family-names: Gasperini given-names: Michael url: "https://github.com/TheStreamCode/chutes-usage-vscode" -version: "0.5.2" +repository-code: "https://github.com/TheStreamCode/chutes-usage-vscode" +version: "0.5.4" +date-released: "2026-08-01" license: MIT diff --git a/README.md b/README.md index 8b6ded3..2f82522 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,17 @@ This is an unofficial third-party extension and is not affiliated with or endors ![Chutes Usage Monitor dashboard](media/screenshot-chutes-usage.png) +## Requirements + +- VS Code `1.103.0` or newer (or a compatible editor that installs from Open VSX) +- A Chutes account and an API key with access to your own usage data + ## Installation -Install the extension from the [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=mikesoft.chutes-usage-vscode): +Install `mikesoft.chutes-usage-vscode` from either registry: -- `mikesoft.chutes-usage-vscode` +- [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=mikesoft.chutes-usage-vscode) +- [Open VSX](https://open-vsx.org/extension/mikesoft/chutes-usage-vscode) After installation: @@ -42,9 +48,9 @@ After installation: ## Latest Changes +- `0.5.4` trims the dashboard state message to the fields the webview renders, removes dead status bar and webview code, syncs the citation metadata, and expands the project documentation. - `0.5.3` hardens webview message and cache handling, resolves the remaining CodeQL findings, adds stricter local and CI quality gates, improves cross-platform cleanup, and live-validates the current Chutes API integration. - `0.5.2` improves legal documentation, trademark notices, third-party terms references, and project metadata. -- `0.5.1` updates the TypeScript build to Node16 module resolution and TypeScript 7. See the [changelog](CHANGELOG.md) for the complete release history. @@ -92,6 +98,38 @@ Settings changes for refresh interval and status bar visibility apply immediatel - The extension does not keep a local history of usage data. - On uninstall, the extension performs best-effort cleanup of its local extension storage. +## Development + +Requires Node.js `22` (see `.nvmrc`) and `npm`. The lockfile is authoritative — do not switch package manager. + +```bash +npm ci # install the locked dependencies +npm run compile # build extension host + webview and copy webview assets +npm test # compile, then run the node:test suite +npm run audit # npm audit --audit-level=high +npm run check # npm test + npm run audit +npm run package # build the VSIX with vsce +npm run preflight # npm run check + npm run package +``` + +Press `F5` in VS Code to launch an Extension Development Host. + +## Repository Structure + +``` +src/ extension host (TypeScript, VS Code API) + services/ Chutes API client, secret storage, payload normalization, link allowlist + state/ dashboard store and the webview state projection + status/ status bar controller + views/ webview view provider and CSP-nonced HTML + test/ node:test suites +webview/ browser-side dashboard, compiled separately as ES modules +scripts/ build helpers +media/ icon and screenshot assets +docs/ user guide and troubleshooting +out/ build output (generated, not committed) +``` + ## Documentation - [User guide](docs/user-guide.md) diff --git a/docs/superpowers/plans/2026-04-15-chutes-usage-implementation.md b/docs/superpowers/plans/2026-04-15-chutes-usage-implementation.md deleted file mode 100644 index 5653bd5..0000000 --- a/docs/superpowers/plans/2026-04-15-chutes-usage-implementation.md +++ /dev/null @@ -1,91 +0,0 @@ -# Chutes Usage Monitor Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a user-friendly VS Code extension that securely shows Chutes usage and quotas through a status bar item and a sidebar dashboard. - -**Architecture:** The extension host manages authentication, API calls, state, commands, and status bar updates. A single sidebar webview renders normalized data snapshots received through message passing. - -**Tech Stack:** TypeScript, VS Code Extension API, SecretStorage, WebviewView, npm, unit tests. - ---- - -### Task 1: Project scaffold - -**Files:** -- Create: `package.json` -- Create: `tsconfig.json` -- Create: `.gitignore` -- Create: `.vscodeignore` -- Create: `README.md` -- Create: `CHANGELOG.md` -- Create: `LICENSE` - -- [ ] Create the base extension manifest with commands, one custom view container, one sidebar view, and three settings. -- [ ] Add TypeScript compilation and test scripts using `npm`. -- [ ] Add packaging ignore rules and repository metadata. - -### Task 2: Domain types and tests - -**Files:** -- Create: `src/types.ts` -- Create: `src/services/normalize.ts` -- Create: `src/test/normalize.test.ts` - -- [ ] Write failing tests for monthly, 4-hour, daily, and quotas normalization. -- [ ] Implement the minimal normalized model and parsing helpers. -- [ ] Verify the tests pass. - -### Task 3: Extension host core - -**Files:** -- Create: `src/extension.ts` -- Create: `src/services/SecretStore.ts` -- Create: `src/services/ChutesApiClient.ts` -- Create: `src/state/DashboardStore.ts` - -- [ ] Write failing tests for state transitions and summary generation. -- [ ] Implement secure secret storage. -- [ ] Implement API client and store refresh logic. -- [ ] Verify tests pass. - -### Task 4: VS Code integration - -**Files:** -- Create: `src/status/StatusBarController.ts` -- Create: `src/views/ChutesWebviewProvider.ts` - -- [ ] Implement commands for setup, refresh, open dashboard, and remove key. -- [ ] Implement a single status bar item. -- [ ] Implement a single sidebar webview provider. - -### Task 5: Sidebar UI - -**Files:** -- Create: `webview/index.html` -- Create: `webview/main.ts` -- Create: `webview/styles.css` - -- [ ] Build a simple dashboard with onboarding, loading, ready, and error states. -- [ ] Keep the layout friendly in a narrow VS Code sidebar. -- [ ] Use VS Code theme variables with Chutes-inspired accents. - -### Task 6: Docs and packaging polish - -**Files:** -- Modify: `README.md` -- Modify: `CHANGELOG.md` -- Create: `media/icon.svg` - -- [ ] Add setup instructions, disclaimer, author attribution, and GitHub repo references. -- [ ] Add screenshots and extension description text for the marketplace. - -### Task 7: Verification - -**Files:** -- Modify as needed - -- [ ] Run `npm install`. -- [ ] Run `npm run compile`. -- [ ] Run `npm test`. -- [ ] Fix any build or test issues. diff --git a/docs/superpowers/specs/2026-04-15-chutes-usage-design.md b/docs/superpowers/specs/2026-04-15-chutes-usage-design.md deleted file mode 100644 index 47b615e..0000000 --- a/docs/superpowers/specs/2026-04-15-chutes-usage-design.md +++ /dev/null @@ -1,129 +0,0 @@ -# Chutes Usage Monitor Design - -## Goal - -Build a user-friendly VS Code extension that shows Chutes subscription usage, rolling usage windows, and quotas in a simple and modern interface. - -## Product Positioning - -`Chutes Usage Monitor` is a third-party utility for the Chutes service. - -It is not an official Chutes extension and must clearly state this in the README, Marketplace listing, and in-app onboarding copy. - -Author attribution: -- Michael Gasperini -- `mikesoft.it` - -Distribution targets: -- VS Code Marketplace -- public GitHub repository - -## User Experience - -Primary experience: -- one compact status bar item for quick visibility -- one Activity Bar icon with a sidebar dashboard for details - -The extension should feel easy to use for first-time users: -- clear onboarding -- very few settings -- readable labels -- obvious refresh actions -- error states that explain what the user should do next - -## Design Direction - -Visual direction: -- inspired by Chutes -- more native to VS Code -- dark, clean, and metric-first - -UI characteristics: -- compact header -- clear progress sections -- big numeric values -- soft card layout -- limited color palette -- strong readability in narrow sidebars - -## Main Views - -### Status Bar - -Shows a short summary like: - -`Chutes $55.34/$100 | 4h $0/$8.33 | 0/5000` - -Behavior: -- click opens the dashboard -- loading shows spinner -- missing API key prompts setup - -### Sidebar Dashboard - -Sections: -- Header with connection status and actions -- Billing cycle usage card -- 4-hour usage card -- Daily usage card -- Quotas table -- Footer with last update info - -## Data Sources - -Confirmed useful APIs: -- `GET /users/me/subscription_usage` -- `GET /users/me/quotas` -- `GET /pricing` -- `GET /model_aliases/` - -The extension should tolerate incomplete or evolving response shapes by using a normalization layer. - -## Security - -The user provides an API key. - -Rules: -- store it only in VS Code `SecretStorage` -- never store it in workspace files -- never send it into the webview -- never log it - -## Reliability - -Refresh model: -- default polling every 60 seconds -- refresh on demand -- refresh when the sidebar becomes visible -- refresh when VS Code regains focus - -## Scope For V1 - -Included: -- onboarding with API key -- status bar summary -- sidebar dashboard -- quotas table -- loading and error states -- documentation and marketplace setup - -Excluded: -- historical analytics -- notifications -- weekly window assumptions unless returned by the API - -## Testing Strategy - -Core logic must be written test-first for: -- API normalization -- state transitions -- status bar summary generation - -## Success Criteria - -The extension is successful when a user can: -- install it -- set an API key in less than one minute -- immediately understand their current Chutes usage -- recognize monthly, 4-hour, and daily limits without reading docs -- trust that it is unofficial but safe and useful diff --git a/package-lock.json b/package-lock.json index 14a3503..f0f961b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chutes-usage-vscode", - "version": "0.5.3", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chutes-usage-vscode", - "version": "0.5.3", + "version": "0.5.4", "license": "MIT", "devDependencies": { "@types/node": "22.17.0", diff --git a/package.json b/package.json index 3403c2e..9f96e47 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chutes-usage-vscode", "displayName": "Chutes Usage Monitor — Track Chutes API Usage & Quotas", "description": "Monitor your Chutes AI usage, rolling limits, and request quotas in a VS Code sidebar dashboard and status bar — secure key storage, auto-refresh, theme-aware. Unofficial third-party extension.", - "version": "0.5.3", + "version": "0.5.4", "publisher": "mikesoft", "author": { "name": "Michael Gasperini", @@ -156,5 +156,9 @@ "@types/node": "22.17.0", "@types/vscode": "1.103.0", "typescript": "^7.0.0" + }, + "allowScripts": { + "@vscode/vsce-sign@2.0.9": true, + "keytar@7.9.0": true } } diff --git a/src/services/normalize.ts b/src/services/normalize.ts index 089a4e6..f325dab 100644 --- a/src/services/normalize.ts +++ b/src/services/normalize.ts @@ -542,47 +542,6 @@ export function summarizeStatusBarCompact(state: { connectionState: 'missing-key return { text: '$(graph) Chutes', severity: 'info' } } -// Build a compact status bar summary that stays short and easy to scan. -export function summarizeStatusBar(data: DashboardData): string { - const billing = data.windows.find((window) => window.kind === 'billing-cycle') - const rolling = data.windows.find((window) => window.kind === 'rolling-4h') - const daily = data.windows.find((window) => window.kind === 'daily-requests') - - const parts: string[] = [] - - if (billing) { - parts.push(`Chutes ${formatWindowSummary(billing, '$')}`) - } else { - parts.push('Chutes') - } - - if (rolling) { - parts.push(`4h ${formatWindowSummary(rolling, '$', false)}`) - } - - if (daily) { - parts.push(formatWindowSummary(daily, '', false)) - } - - return parts.join(' | ') -} - -function formatWindowSummary(window: UsageWindow, prefix: string, includePrefix = true): string { - const used = window.used - const limit = window.limit - - if (window.unit === 'requests') { - const formattedLimit = limit === null ? '--' : limit === 0 ? 'Unlimited' : `${Math.round(limit)}` - return `${used === null ? '--' : Math.round(used)}/${formattedLimit}` - } - - const safeUsed = used ?? 0 - const safeLimit = limit ?? 0 - - const formatted = `${prefix}${safeUsed.toFixed(2)}/${prefix}${safeLimit.toFixed(2).replace(/\.00$/, '')}` - return includePrefix ? formatted : formatted -} - function asObject(value: unknown): JsonObject | null { return typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as JsonObject) : null } diff --git a/src/state/webviewState.ts b/src/state/webviewState.ts new file mode 100644 index 0000000..9e7feec --- /dev/null +++ b/src/state/webviewState.ts @@ -0,0 +1,13 @@ +import type { DashboardState } from '../types' + +// Send the webview only what the dashboard renders. The per-model `quotas` rows +// are consumed by the extension host to derive the daily window and are never +// read by the webview, so they stay out of every `postMessage` payload and out +// of the state the webview persists through `vscode.setState`. +export function toWebviewState(state: DashboardState): DashboardState { + if (state.data === null || state.data.quotas.length === 0) { + return state + } + + return { ...state, data: { ...state.data, quotas: [] } } +} diff --git a/src/test/normalize.test.ts b/src/test/normalize.test.ts index 52b06c8..08e0759 100644 --- a/src/test/normalize.test.ts +++ b/src/test/normalize.test.ts @@ -1,7 +1,7 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { normalizeDashboardData, normalizePaygCredit, summarizeStatusBar, summarizeStatusBarCompact } from '../services/normalize' +import { normalizeDashboardData, normalizePaygCredit, summarizeStatusBarCompact } from '../services/normalize' import type { DashboardData, DashboardState, JsonObject, UsageWindow } from '../types' test('normalizes known subscription usage and quotas for a pro account', () => { @@ -547,105 +547,32 @@ test('defaults quota label to All Models when the API omits a model name', () => assert.equal(result.quotas[0]?.modelLabel, 'All Models') }) -test('summarizes status bar text in a compact and user friendly format', () => { - const summary = summarizeStatusBar({ - windows: [ - { - id: 'billing', - kind: 'billing-cycle', - label: 'Billing Cycle Cap', - unit: 'usd', - used: 55.339, - limit: 100, - remaining: 44.661, - percentUsed: 55.339, - resetLabel: null - }, - { - id: '4h', - kind: 'rolling-4h', - label: '4-Hour Window', - unit: 'usd', - used: 0, - limit: 8.3333, - remaining: 8.3333, - percentUsed: 0, - resetLabel: null - }, - { - id: 'daily', - kind: 'daily-requests', - label: 'Daily Quota', - unit: 'requests', - used: 0, - limit: 5000, - remaining: 5000, - percentUsed: 0, - resetLabel: null - } - ], - quotas: [], - planLimits: [], - paygCreditUsd: null, - plan: { - planName: 'Pro', - monthlyPriceUsd: 20, - monthlyCapUsd: 100, - fourHourCapUsd: 8.3333, - dailyRequestLimit: 5000, - paygDiscountPercent: 10 - } - }) +test('compact status bar renders daily request windows with used and limit counts', () => { + const summary = summarizeStatusBarCompact(createCompactState({ + connectionState: 'ready', + data: createCompactData([makeWindow('daily-requests', 'requests', 4800, 5000, 96)]) + })) - assert.equal(summary, 'Chutes $55.34/$100 | 4h $0.00/$8.33 | 0/5000') + assert.equal(summary.text, '$(warning) Chutes day 4800/5000') + assert.equal(summary.severity, 'warning') }) -test('summarizes unknown daily request usage without coercing it to zero', () => { - const summary = summarizeStatusBar({ - windows: [ - { - id: 'daily', - kind: 'daily-requests', - label: 'Daily Quota', - unit: 'requests', - used: null, - limit: 5000, - remaining: null, - percentUsed: null, - resetLabel: null - } - ], - quotas: [], - planLimits: [], - paygCreditUsd: null, - plan: null - }) - - assert.equal(summary, 'Chutes | --/5000') +test('compact status bar never coerces unverified request usage to zero', () => { + const summary = summarizeStatusBarCompact(createCompactState({ + connectionState: 'ready', + data: createCompactData([makeWindow('daily-requests', 'requests', null, 5000, 92)]) + })) + + assert.equal(summary.text, '$(warning) Chutes day --/5000') }) -test('summarizes unlimited daily quota and stale sync states clearly', () => { - const summary = summarizeStatusBar({ - windows: [ - { - id: 'daily', - kind: 'daily-requests', - label: 'Daily Quota', - unit: 'requests', - used: null, - limit: 0, - remaining: null, - percentUsed: null, - resetLabel: 'Possible sync delay' - } - ], - quotas: [], - planLimits: [], - paygCreditUsd: null, - plan: null - }) - - assert.equal(summary, 'Chutes | --/Unlimited') +test('compact status bar renders an unlimited request quota instead of a zero limit', () => { + const summary = summarizeStatusBarCompact(createCompactState({ + connectionState: 'ready', + data: createCompactData([makeWindow('daily-requests', 'requests', null, 0, 92)]) + })) + + assert.equal(summary.text, '$(warning) Chutes day --/∞') }) test('compact status bar reports info severity and key codicon when API key is missing', () => { @@ -718,7 +645,7 @@ function createCompactData(windows: UsageWindow[]): DashboardData { return { windows, quotas: [], plan: null, planLimits: [], paygCreditUsd: null } } -function makeWindow(kind: UsageWindow['kind'], unit: 'usd' | 'requests', used: number, limit: number, percentUsed: number): UsageWindow { +function makeWindow(kind: UsageWindow['kind'], unit: 'usd' | 'requests', used: number | null, limit: number, percentUsed: number): UsageWindow { return { id: kind, kind, @@ -726,7 +653,7 @@ function makeWindow(kind: UsageWindow['kind'], unit: 'usd' | 'requests', used: n unit, used, limit, - remaining: Math.max(0, limit - used), + remaining: used === null ? null : Math.max(0, limit - used), percentUsed, resetLabel: null } diff --git a/src/test/releaseMetadata.test.ts b/src/test/releaseMetadata.test.ts new file mode 100644 index 0000000..388c0ae --- /dev/null +++ b/src/test/releaseMetadata.test.ts @@ -0,0 +1,29 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +type PackageMetadata = { + version: string +} + +type LockfileMetadata = PackageMetadata & { + packages: Record +} + +const root = process.cwd() + +test('keeps release metadata versions synchronized', () => { + const packageJson = readJson('package.json') + const packageLock = readJson('package-lock.json') + const citation = readFileSync(join(root, 'CITATION.cff'), 'utf8') + const citationVersion = citation.match(/^version:\s*["']?([^"'\r\n]+)["']?\s*$/m)?.[1]?.trim() + + assert.equal(packageLock.version, packageJson.version) + assert.equal(packageLock.packages['']?.version, packageJson.version) + assert.equal(citationVersion, packageJson.version) +}) + +function readJson(path: string): T { + return JSON.parse(readFileSync(join(root, path), 'utf8')) as T +} diff --git a/src/test/webviewState.test.ts b/src/test/webviewState.test.ts new file mode 100644 index 0000000..d8ba2a6 --- /dev/null +++ b/src/test/webviewState.test.ts @@ -0,0 +1,81 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { toWebviewState } from '../state/webviewState' +import type { DashboardState } from '../types' + +test('keeps states without dashboard data untouched', () => { + const state: DashboardState = { + connectionState: 'missing-key', + connected: false, + lastUpdatedAt: null, + data: null, + errorMessage: null + } + + assert.equal(toWebviewState(state), state) +}) + +test('drops the per-model quota rows the webview never renders', () => { + const state = createReadyState([ + { modelLabel: 'All Models', quota: 5000, lastUpdated: '2026-08-01T10:00:00.000Z' } + ]) + + const projected = toWebviewState(state) + + assert.deepEqual(projected.data?.quotas, []) + assert.equal(projected.connectionState, state.connectionState) + assert.equal(projected.lastUpdatedAt, state.lastUpdatedAt) + assert.deepEqual(projected.data?.windows, state.data?.windows) + assert.deepEqual(projected.data?.plan, state.data?.plan) + assert.deepEqual(projected.data?.planLimits, state.data?.planLimits) + assert.equal(projected.data?.paygCreditUsd, state.data?.paygCreditUsd) +}) + +test('does not copy the state when there is no quota payload to strip', () => { + const state = createReadyState([]) + assert.equal(toWebviewState(state), state) +}) + +test('leaves the extension-host state object unmodified', () => { + const state = createReadyState([{ modelLabel: 'All Models', quota: 5000, lastUpdated: null }]) + + toWebviewState(state) + + assert.equal(state.data?.quotas.length, 1) +}) + +function createReadyState(quotas: NonNullable['quotas']): DashboardState { + return { + connectionState: 'ready', + connected: true, + lastUpdatedAt: '2026-08-01T10:00:00.000Z', + data: { + windows: [ + { + id: 'billing', + kind: 'billing-cycle', + label: 'Billing Cycle Cap', + unit: 'usd', + used: 12.5, + limit: 100, + remaining: 87.5, + percentUsed: 12.5, + resetLabel: null + } + ], + quotas, + plan: { + planName: 'Pro', + monthlyPriceUsd: 20, + monthlyCapUsd: 100, + fourHourCapUsd: 8.3333, + dailyRequestLimit: 5000, + paygDiscountPercent: 10 + }, + planLimits: [], + paygCreditUsd: 4.2 + }, + errorMessage: null + } +} diff --git a/src/views/ChutesWebviewProvider.ts b/src/views/ChutesWebviewProvider.ts index 73d1234..c3989c8 100644 --- a/src/views/ChutesWebviewProvider.ts +++ b/src/views/ChutesWebviewProvider.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode' import { randomBytes } from 'crypto' import { DASHBOARD_VIEW_ID } from '../constants' +import { toWebviewState } from '../state/webviewState' import type { DashboardState, WebviewActionMessage, WebviewStateMessage } from '../types' type Actions = { @@ -79,7 +80,7 @@ export class ChutesWebviewProvider implements vscode.WebviewViewProvider { const message: WebviewStateMessage = { type: 'state', - state, + state: toWebviewState(state), refreshIntervalMs: this.refreshIntervalMs } diff --git a/webview/index.html b/webview/index.html deleted file mode 100644 index ccad695..0000000 --- a/webview/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Chutes Usage Monitor - - -
- -