Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/social-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The repository is **public** on GitHub, MIT licensed, and published to both the
Two independently compiled worlds that only talk through `postMessage`:

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/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; `/users/me/quota_usage/me`, `/users/me/quota_usage/{chute_id}`, `/invocations/stats/llm`, and `/users/me` are optional and fail soft. Per-chute fallbacks run with bounded concurrency.
- `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.
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## [Unreleased]

- Refreshed the public project presentation with a current dashboard screenshot rendered from the real webview using synthetic values, a custom GitHub social preview, clearer installation links, and a more user-focused README introduction.
- Cleared the `fast-uri` and `undici` development dependency advisories with an npm-managed lockfile refresh.
- Stopped persisting dashboard account snapshots in webview state. Only the Plan Limits collapsed preference is retained, and the cache schema is now version 3.
- Removed the unrelated `/pricing` request. Plan reference prices and PAYG discounts are clearly identified as public references, while account-specific limits are shown only when returned for the current plan; unavailable values use `--`.
- Tightened host-to-webview projections and runtime validators so quota rows and unknown fields cannot cross the message boundary.
- Improved accessibility with contrast-safe light-theme accents, live sync announcements, truthful progressbar values, semantic section headings, and text labels for low or exhausted PAYG credit.
- Limited per-chute quota fallback requests to five concurrent calls and pinned CI to the exact Node.js version in `.nvmrc`.

## 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.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thanks for your interest in improving `Chutes Usage Monitor`.

## Prerequisites

- Node.js 22 or newer
- Node.js 22.17.0 (the exact version in `.nvmrc`)
- `npm`
- Visual Studio Code

Expand Down
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Sponsor](https://img.shields.io/badge/Sponsor-TheStreamCode-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/TheStreamCode)

Monitor Chutes subscription usage, rolling limits, and request quotas directly inside VS Code.
Keep Chutes usage visible while you code. The sidebar shows subscription spend, rolling limits, daily requests, and pay-as-you-go credit without leaving VS Code.

[Install from the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=mikesoft.chutes-usage-vscode) · [Install from Open VSX](https://open-vsx.org/extension/mikesoft/chutes-usage-vscode) · [Read the user guide](docs/user-guide.md)

This is an unofficial third-party extension and is not affiliated with or endorsed by Chutes.

![Chutes Usage Monitor dashboard](media/screenshot-chutes-usage.png)

_Current dashboard rendered with synthetic, illustrative values._

## Requirements

- VS Code `1.103.0` or newer (or a compatible editor that installs from Open VSX)
Expand All @@ -31,28 +35,14 @@ After installation:
2. Run `Chutes Usage Monitor: Set API Key`.
3. Open the `Chutes Usage Monitor` view from the Activity Bar.

## Features

- Sidebar dashboard aligned with the chutes.ai look: pure-black on dark themes, pill buttons, mint pill badges, headline-violet section titles, generous whitespace
- Theme-aware: backgrounds, foregrounds, borders, and description text follow VS Code theme tokens; works with Light, Dark, and High Contrast themes
- Plan snapshot, per-window usage cards with progress bars, and a Plan Limits reference that highlights your current tier
- PAYG credit tile showing your pay-as-you-go account balance, with amber/red warnings when the balance runs low or reaches zero
- Compact status bar summary with severity-aware codicon and background color; full detail in the tooltip
- Secure API key storage through VS Code `SecretStorage`
- Manual refresh command for immediate sync
- Automatic refresh on a configurable timer, when the dashboard becomes visible again, and when the VS Code window regains focus
- Live `refresh in Ns` countdown driven by your `refreshIntervalSeconds` setting
- Cached state restore on side-panel reopen — no `Loading…` flash, no flicker on every tick
- Accessible by default: ARIA progressbar/alert/status semantics, `prefers-reduced-motion` support, and visible focus rings
- Hardened webview CSP with a per-load nonce and scoped `font-src`/`img-src`

## 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.
## What You Get

See the [changelog](CHANGELOG.md) for the complete release history.
- A focused sidebar dashboard for billing-cycle, rolling four-hour, and daily request usage
- Plan context and PAYG credit at a glance, with visible low- and no-credit warnings
- An optional status bar summary for quick checks without opening the dashboard
- Manual and automatic refresh when the timer fires, the view reopens, or VS Code regains focus
- Native Light, Dark, and High Contrast theme support with keyboard and screen-reader accessibility
- Read-only account access, secure API-key storage, no local usage history, and a locked-down webview

## Commands

Expand All @@ -78,7 +68,7 @@ The sidebar dashboard includes:
- a compact header with sync state and actions
- a plan snapshot with the most relevant subscription figures, including your pay-as-you-go credit balance
- stacked usage cards optimized for narrow Activity Bar layouts
- a `Plan Limits` reference section that uses pricing data when available without exposing a raw quota payload table
- a `Plan Limits` reference section with public plan prices and PAYG discounts; current-plan limits come from your account and unavailable values display as `--`

The dashboard refreshes when you run the refresh command, on the configured refresh interval, when the dashboard becomes visible again, and when VS Code regains window focus.

Expand All @@ -96,11 +86,20 @@ Settings changes for refresh interval and status bar visibility apply immediatel
- Your Chutes API key is stored using VS Code `SecretStorage`.
- The extension uses the key only to request your own usage data.
- The extension does not keep a local history of usage data.
- The webview persists only whether the Plan Limits section is collapsed, never an account snapshot.
- On uninstall, the extension performs best-effort cleanup of its local extension storage.

## Latest Changes

- `Unreleased` clears dependency advisories, removes persisted usage snapshots and the unrelated pricing request, tightens the webview boundary, and improves accessibility.
- `0.5.4` trims dashboard state, removes dead code, synchronizes release metadata, and expands the project documentation.
- `0.5.3` strengthens webview security, repository quality gates, cross-platform cleanup, and API compatibility.

See the [changelog](CHANGELOG.md) for the complete release history and the [latest GitHub release](https://github.com/TheStreamCode/chutes-usage-vscode/releases/latest) for downloadable artifacts.

## Development

Requires Node.js `22` (see `.nvmrc`) and `npm`. The lockfile is authoritative — do not switch package manager.
Requires Node.js `22.17.0` (see `.nvmrc`) and `npm`. The lockfile is authoritative — do not switch package manager.

```bash
npm ci # install the locked dependencies
Expand Down Expand Up @@ -135,6 +134,8 @@ out/ build output (generated, not committed)
- [User guide](docs/user-guide.md)
- [Troubleshooting](docs/troubleshooting.md)
- [Documentation index](docs/README.md)
- [Questions and community support](https://github.com/TheStreamCode/chutes-usage-vscode/discussions)
- [Bug reports and feature requests](https://github.com/TheStreamCode/chutes-usage-vscode/issues)
- [Contributing](CONTRIBUTING.md)
- [Support](SUPPORT.md)
- [Security](SECURITY.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ The extension currently relies on these endpoints for user-facing usage data:
- `GET /users/me/quota_usage/me`
- `GET /users/me/quota_usage/{chute_id}`
- `GET /invocations/stats/llm`
- `GET /pricing`
- `GET /users/me`

Some endpoints are used as fallbacks when the primary payload is incomplete or delayed.
The Plan Limits link opens the public `https://chutes.ai/pricing` page; the extension does not fetch or parse that page as account data.

## API Compatibility

Expand Down
8 changes: 4 additions & 4 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ It includes:
- a header with connection status and actions
- a plan snapshot with key subscription details, including your pay-as-you-go credit balance
- stacked usage cards for the main billing windows
- a `Plan Limits` section with monthly cap, daily request, burst, and PAYG discount references, using live pricing data when available
- a `Plan Limits` section with public plan prices and PAYG discounts; monthly, daily, and burst limits are filled only for your current plan when the account API provides them

Depending on the current state, the dashboard shows onboarding, loading, ready, or error content.

Expand Down Expand Up @@ -67,7 +67,7 @@ The extension refreshes data in three ways:

Use `chutesUsageVscode.refreshIntervalSeconds` to control the refresh interval.

When you reopen the dashboard after a recent refresh, the latest snapshot is shown immediately and then refreshed again in the background.
While the extension host remains active, reopening the dashboard can reuse its in-memory snapshot before refreshing. The webview itself persists only whether the Plan Limits section is collapsed and never stores usage or account values.

## Usage Data Shown

Expand All @@ -77,8 +77,8 @@ Depending on the API responses available for your account, the extension can dis
- 4-hour rolling window usage
- daily request usage
- plan information such as subscription price or caps when available
- your pay-as-you-go credit balance (the account credit that funds requests beyond your subscription caps), highlighted in amber when it runs low and in red when it reaches zero
- a built-in subscription limits reference based on pricing data when available, with packaged Plus and Pro defaults as a fallback
- your pay-as-you-go credit balance (the account credit that funds requests beyond your subscription caps), with visible `Low credit` or `No credit` text and warning colors when applicable
- a Plus and Pro reference using public prices and PAYG discounts; other limits show `--` unless they are verified from the current account

## Managing Your API Key

Expand Down
Binary file modified media/screenshot-chutes-usage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 26 additions & 6 deletions src/services/ChutesApiClient.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { API_BASE_URL } from '../constants'
import type { JsonContainer, JsonObject } from '../types'

const QUOTA_USAGE_CONCURRENCY = 5

export class ChutesApiClient {
public constructor(private readonly apiKey: string) {}

// Fetch all user-facing dashboard endpoints needed for the first extension version.
public async getDashboardPayload(): Promise<{ subscriptionUsage: JsonObject; quotas: JsonContainer; quotaUsageMe: JsonContainer | null; quotaUsageFallback: JsonContainer | null; invocationStatsLlm: JsonContainer | null; pricing: JsonContainer | null; me: JsonContainer | null }> {
const [subscriptionUsage, quotas, pricing, quotaUsageMe, invocationStatsLlm, me] = await Promise.all([
public async getDashboardPayload(): Promise<{ subscriptionUsage: JsonObject; quotas: JsonContainer; quotaUsageMe: JsonContainer | null; quotaUsageFallback: JsonContainer | null; invocationStatsLlm: JsonContainer | null; me: JsonContainer | null }> {
const [subscriptionUsage, quotas, quotaUsageMe, invocationStatsLlm, me] = await Promise.all([
this.getJsonContainer('/users/me/subscription_usage'),
this.getJsonContainer('/users/me/quotas'),
this.getJsonContainer('/pricing').catch(() => null),
this.getJsonContainer('/users/me/quota_usage/me').catch(() => null),
this.getJsonContainer('/invocations/stats/llm').catch(() => null),
this.getJsonContainer('/users/me').catch(() => null)
Expand All @@ -20,7 +21,7 @@ export class ChutesApiClient {
throw new Error('Unexpected API response shape for /users/me/subscription_usage')
}

return { subscriptionUsage, quotas, quotaUsageMe, quotaUsageFallback, invocationStatsLlm, pricing, me }
return { subscriptionUsage, quotas, quotaUsageMe, quotaUsageFallback, invocationStatsLlm, me }
}

// Execute one authenticated GET request and return a JSON object or array payload.
Expand Down Expand Up @@ -60,11 +61,11 @@ export class ChutesApiClient {
return null
}

const entries = await Promise.all(chuteIds.map(async (chuteId) => {
const entries = await mapWithConcurrency(chuteIds, QUOTA_USAGE_CONCURRENCY, async (chuteId) => {
const path = `/users/me/quota_usage/${encodePathSegment(chuteId)}`
const payload = await this.getJsonContainer(path).catch(() => null)
return payload === null ? null : [chuteId, payload] as const
}))
})

const validEntries = entries.filter((entry): entry is readonly [string, JsonContainer] => entry !== null)
if (validEntries.length === 0) {
Expand All @@ -75,6 +76,25 @@ export class ChutesApiClient {
}
}

async function mapWithConcurrency<T, R>(items: readonly T[], concurrency: number, mapper: (item: T) => Promise<R>): Promise<R[]> {
const results = new Array<R>(items.length)
let nextIndex = 0
const workerCount = Math.min(concurrency, items.length)

const workers = Array.from({ length: workerCount }, async () => {
while (nextIndex < items.length) {
const index = nextIndex++
const item = items[index]
if (item !== undefined) {
results[index] = await mapper(item)
}
}
})

await Promise.all(workers)
return results
}

function isJsonObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
Expand Down
Loading