summaryrefslogtreecommitdiffhomepage
path: root/src/app
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-14 17:04:35 +0900
committerAdam Malczewski <[email protected]>2026-10-14 17:04:35 +0900
commit10e78065201c8387494b8aafabb987da87183b3a (patch)
treef038f368459acbd3b817974100fb4bd4817ecbff /src/app
parent551a7a050e8cd25e795026a1060c9d5b5ea028bf (diff)
downloaddispatch-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.ts6
-rw-r--r--src/app/store.svelte.ts29
-rw-r--r--src/app/views/AccountUsagePane.svelte16
-rw-r--r--src/app/views/ExtensionsPane.svelte2
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,