diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 13:25:23 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 13:25:23 +0900 |
| commit | 6433cc42de1ceca7210e2b64ad3b98b3a5ce7d02 (patch) | |
| tree | 78b30dedd471ab76177b3631a956ab160615e303 /packages/frontend/src/lib/components | |
| parent | 3f629a8469fe483243671e1ca15582a111e96541 (diff) | |
| download | dispatch-6433cc42de1ceca7210e2b64ad3b98b3a5ce7d02.tar.gz dispatch-6433cc42de1ceca7210e2b64ad3b98b3a5ce7d02.zip | |
feat(context-window): show current/max context usage per tab/model
Add a 'Context Window' sidebar view showing the live context occupancy
(latest request's input+output) against the model's maximum context
window, resolved dynamically from the models.dev catalog.
- core: models.dev catalog module (resolveContextLimit) with disk cache,
TTL, stale-fallback + offline penalty memo; null for unknown models.
- api: GET /models/context-limit?provider=&modelId=.
- frontend: ContextWindowPanel + computeContextUsage helper; App resolves
+ caches the active model's max (anthropic/opencode-anthropic only);
percent shown to 2 decimals; degrades to bare token count when max
unknown.
- tests: core catalog (13), api route (3), frontend helper (6).
Diffstat (limited to 'packages/frontend/src/lib/components')
| -rw-r--r-- | packages/frontend/src/lib/components/ContextWindowPanel.svelte | 85 | ||||
| -rw-r--r-- | packages/frontend/src/lib/components/SidebarPanel.svelte | 11 |
2 files changed, 96 insertions, 0 deletions
diff --git a/packages/frontend/src/lib/components/ContextWindowPanel.svelte b/packages/frontend/src/lib/components/ContextWindowPanel.svelte new file mode 100644 index 0000000..6c7de05 --- /dev/null +++ b/packages/frontend/src/lib/components/ContextWindowPanel.svelte @@ -0,0 +1,85 @@ +<script lang="ts"> +import { computeContextUsage } from "../context-window.js"; +import type { CacheStats } from "../types.js"; + +const { + cacheStats = null, + contextLimit = null, + tabTitle = null, + modelId = null, +}: { + cacheStats?: CacheStats | null; + contextLimit?: number | null; + tabTitle?: string | null; + modelId?: string | null; +} = $props(); + +const usage = $derived(computeContextUsage(cacheStats, contextLimit)); + +// As the window fills, escalate color: calm → warning → danger. +function fillClass(pct: number): string { + if (pct >= 90) return "progress-error"; + if (pct >= 70) return "progress-warning"; + return "progress-success"; +} + +function fmt(n: number): string { + return n.toLocaleString(); +} + +const hasUsage = $derived((cacheStats?.last ?? null) !== null); +</script> + +<div class="flex flex-col gap-3 flex-1 min-h-0 overflow-y-auto"> + {#if !hasUsage} + <p class="text-xs text-base-content/50"> + No context data yet. Send a message — the current context size appears + here after the first response. + </p> + {:else} + <div class="bg-base-200 rounded-lg p-2"> + <div class="flex items-center gap-1.5 mb-2"> + <span class="text-xs font-semibold">Context Window</span> + {#if tabTitle} + <span class="badge badge-xs badge-ghost">{tabTitle}</span> + {/if} + {#if usage.percent !== null} + <span class="badge badge-xs ml-auto">{usage.percent.toFixed(2)}%</span> + {/if} + </div> + + <!-- Headline: current / max (or just current when max is unknown) --> + <div class="flex items-baseline gap-1.5"> + <span class="text-lg font-mono font-semibold">{fmt(usage.current)}</span> + {#if usage.max !== null} + <span class="text-xs text-base-content/50 font-mono">/ {fmt(usage.max)}</span> + {/if} + <span class="text-xs text-base-content/40 ml-1">tokens</span> + </div> + + {#if usage.percent !== null} + <progress + class="progress w-full h-2 mt-1.5 {fillClass(usage.percent)}" + value={usage.percent} + max="100" + ></progress> + {:else} + <p class="text-xs text-base-content/40 mt-1.5"> + Max context size unknown for this model. + </p> + {/if} + + {#if modelId} + <div class="text-xs text-base-content/40 mt-1.5 truncate" title={modelId}> + {modelId} + </div> + {/if} + </div> + + <p class="text-xs text-base-content/40"> + Current context = the most recent request's prompt + output (what the + model actually held in its window that turn). Grows as the conversation + gets longer. Resets on reload. + </p> + {/if} +</div> diff --git a/packages/frontend/src/lib/components/SidebarPanel.svelte b/packages/frontend/src/lib/components/SidebarPanel.svelte index 491b1bd..573a6fc 100644 --- a/packages/frontend/src/lib/components/SidebarPanel.svelte +++ b/packages/frontend/src/lib/components/SidebarPanel.svelte @@ -4,6 +4,7 @@ import type { CacheStats, KeyInfo, LogEntry, TaskItem } from "../types.js"; import CacheRatePanel from "./CacheRatePanel.svelte"; import ClaudeReset from "./ClaudeReset.svelte"; import ConfigPanel from "./ConfigPanel.svelte"; +import ContextWindowPanel from "./ContextWindowPanel.svelte"; import DebugPanel from "./DebugPanel.svelte"; import KeyUsage from "./KeyUsage.svelte"; import ModelSelector from "./ModelSelector.svelte"; @@ -27,6 +28,7 @@ const { tasks = [], cacheStats = null, cacheTabTitle = null, + contextLimit = null, permissionLog = [], apiBase = "", activeKeyId = null, @@ -47,6 +49,7 @@ const { tasks?: TaskItem[]; cacheStats?: CacheStats | null; cacheTabTitle?: string | null; + contextLimit?: number | null; permissionLog?: LogEntry[]; apiBase?: string; activeKeyId?: string | null; @@ -89,6 +92,7 @@ const viewOptions = [ "Chat Settings", "Key Usage", "Cache Rate", + "Context Window", "Claude Reset", "Model Status", "Tasks", @@ -170,6 +174,13 @@ function contentClass(_selected: string): string { <KeyUsage {keys} {apiBase} /> {:else if panel.selected === "Cache Rate"} <CacheRatePanel {cacheStats} tabTitle={cacheTabTitle} /> + {:else if panel.selected === "Context Window"} + <ContextWindowPanel + {cacheStats} + {contextLimit} + tabTitle={cacheTabTitle} + modelId={activeModelId} + /> {:else if panel.selected === "Claude Reset"} <ClaudeReset {apiBase} /> {:else if panel.selected === "Model Status"} |
