diff options
| author | Adam Malczewski <[email protected]> | 2026-10-14 17:04:35 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-14 17:04:35 +0900 |
| commit | 10e78065201c8387494b8aafabb987da87183b3a (patch) | |
| tree | f038f368459acbd3b817974100fb4bd4817ecbff /src/app | |
| parent | 551a7a050e8cd25e795026a1060c9d5b5ea028bf (diff) | |
| download | dispatch-web-10e78065201c8387494b8aafabb987da87183b3a.tar.gz dispatch-web-10e78065201c8387494b8aafabb987da87183b3a.zip | |
feat(account-usage): sidebar Usage view with Claude/Codex quota bars + on-track pace markers
Consumes GET /usage (transport-contract 0.25.0). DaisyUI progress bars per quota
window with a dot at the elapsed fraction of the window (the 'on track' level),
ahead/under-pace labels, reset countdowns, stale/unavailable/not-configured states.
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/register-views.ts | 6 | ||||
| -rw-r--r-- | src/app/store.svelte.ts | 29 | ||||
| -rw-r--r-- | src/app/views/AccountUsagePane.svelte | 16 | ||||
| -rw-r--r-- | src/app/views/ExtensionsPane.svelte | 2 |
4 files changed, 51 insertions, 2 deletions
diff --git a/src/app/register-views.ts b/src/app/register-views.ts index 5b462fe..05afd89 100644 --- a/src/app/register-views.ts +++ b/src/app/register-views.ts @@ -9,7 +9,7 @@ * * `chat` / `composer` / `message-queue` are CONTENT views (decision 5): they * register with `panelEligible: false` so they never appear in the panel - * dropdowns. All 14 sidebar views keep the eligible default. + * dropdowns. All 15 sidebar views keep the eligible default. * * Imported ONCE by App.svelte's module script — before any tile renders. */ @@ -17,6 +17,7 @@ import type { Component } from "svelte"; import { registerView, type ViewComponent, type ViewRegistration } from "../features/layout"; import type { AppViewPorts } from "./view-ports"; +import AccountUsagePane from "./views/AccountUsagePane.svelte"; import CacheWarmingPane from "./views/CacheWarmingPane.svelte"; import ChatPane from "./views/ChatPane.svelte"; import CompactionPane from "./views/CompactionPane.svelte"; @@ -50,7 +51,7 @@ function asView(component: WrapperComponent): ViewComponent { export function registerBuiltinViews(): void { const views: readonly ViewRegistration[] = [ - // --- sidebar views (panel-eligible by default — the 14 old viewKinds) + // --- sidebar views (panel-eligible by default — the 14 old viewKinds + account-usage) { id: "tabs", label: "Tabs", component: asView(TabsPane) }, { id: "model", label: "Model", component: asView(ModelPane) }, { id: "named-agents", label: "Named Agents", component: asView(NamedAgentsPane) }, @@ -63,6 +64,7 @@ export function registerBuiltinViews(): void { { id: "vision", label: "Vision", component: asView(VisionPane) }, { id: "heartbeat", label: "Heartbeat", component: asView(HeartbeatPane) }, { id: "concurrency", label: "Concurrency", component: asView(ConcurrencyPane) }, + { id: "account-usage", label: "Usage", component: asView(AccountUsagePane) }, { id: "system-prompt", label: "System Prompt", component: asView(SystemPromptPane) }, { id: "settings", label: "Settings", component: asView(SettingsPane) }, // --- content views (never in the panel dropdowns — decision 5) diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index afccced..7433f81 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -56,6 +56,8 @@ import { subscribe as protocolSubscribe, unsubscribe as protocolUnsubscribe, } from "../core/protocol"; +import type { AccountUsageResult } from "../features/account-usage"; +import { normalizeAccountUsage } from "../features/account-usage"; import type { ChatStore, HistorySync, MetricsSync } from "../features/chat"; import { createChatStore, migrateLegacyModelName } from "../features/chat"; import type { SetThinkingRequest, ThinkingResponse } from "../features/chat/reasoning-effort"; @@ -535,6 +537,13 @@ export interface AppStore { */ concurrencyStatus(): Promise<ConcurrencyStatusResult>; /** + * Load Claude + Codex subscription usage (`GET /usage`). Served from the + * backend's cache (5 min TTL); `refresh: true` sends `?refresh=1`, which the + * server still rate-limits (min interval + failure cooldown). A 503 (the + * `account-usage` extension isn't loaded) returns `unavailable: true`. + */ + accountUsage(opts?: { readonly refresh?: boolean }): Promise<AccountUsageResult>; + /** * Load the GLOBAL search-backend selection * (`GET /search/config`): which engine backs the `web_search` tool's search * mode. Not workspace-scoped — the selection is global. `unavailable: true` @@ -2584,6 +2593,26 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { } }, + async accountUsage(opts?: { readonly refresh?: boolean }): Promise<AccountUsageResult> { + try { + const res = await fetchImpl(`${httpBase}/usage${opts?.refresh ? "?refresh=1" : ""}`); + if (!res.ok) { + const errBody = (await res.json().catch(() => null)) as { error?: string } | null; + return { + ok: false, + error: errBody?.error ?? `Account usage failed (HTTP ${res.status})`, + ...(res.status === 503 ? { unavailable: true } : {}), + }; + } + return { ok: true, usage: normalizeAccountUsage(await res.json(), Date.now()) }; + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : "Account usage request failed", + }; + } + }, + async concurrencyStatus(): Promise<ConcurrencyStatusResult> { try { const res = await fetchImpl(`${httpBase}/concurrency/status`); diff --git a/src/app/views/AccountUsagePane.svelte b/src/app/views/AccountUsagePane.svelte new file mode 100644 index 0000000..4315d4c --- /dev/null +++ b/src/app/views/AccountUsagePane.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + /** + * The `account-usage` view — Claude + Codex subscription usage bars with + * on-track pace markers. GLOBAL (not workspace/conversation scoped), so it + * stays mounted across tab switches (no {#key}). + */ + import { AccountUsageView, type LoadAccountUsage } from "../../features/account-usage"; + import type { AppViewPorts } from "../view-ports"; + + let { ports }: { ports: AppViewPorts } = $props(); + const store = $derived(ports.store); + + const loadAccountUsage: LoadAccountUsage = (opts) => store.accountUsage(opts); +</script> + +<AccountUsageView load={loadAccountUsage} /> diff --git a/src/app/views/ExtensionsPane.svelte b/src/app/views/ExtensionsPane.svelte index 6afc2bf..d5baa39 100644 --- a/src/app/views/ExtensionsPane.svelte +++ b/src/app/views/ExtensionsPane.svelte @@ -6,6 +6,7 @@ * surface is a SEPARATE, backend-owned list. Unkeyed (like today). */ import Table from "../../components/Table.svelte"; + import { manifest as accountUsageManifest } from "../../features/account-usage"; import { manifest as cacheWarmingManifest } from "../../features/cache-warming"; import { manifest as chatManifest } from "../../features/chat"; import { manifest as concurrencyManifest } from "../../features/concurrency"; @@ -59,6 +60,7 @@ systemPromptManifest, heartbeatManifest, concurrencyManifest, + accountUsageManifest, visionManifest, namedAgentsManifest, searchSettingsManifest, |
