From 88f5de68d8467d6b9bb8f55c418bf5111bc01841 Mon Sep 17 00:00:00 2001 From: Tyler Gray Date: Fri, 10 Apr 2026 19:18:23 -0400 Subject: [PATCH] feat: make context window size model-aware The context usage ring and breakdown popover now dynamically adjust based on the selected model. Opus shows usage out of 1M tokens, Sonnet/Haiku out of 200k, etc. Previously hardcoded to 200k for all models. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/chat/Composer.test.tsx | 18 ++++----- src/components/chat/Composer.tsx | 9 +++-- .../chat/ContextBreakdownPopover.test.tsx | 38 +++++++++---------- .../chat/ContextBreakdownPopover.tsx | 14 +++---- src/components/chat/ContextUsageIndicator.tsx | 37 +++++++++--------- src/lib/models.test.ts | 29 ++++++++++++++ src/lib/models.ts | 16 ++++++++ 7 files changed, 105 insertions(+), 56 deletions(-) create mode 100644 src/lib/models.test.ts create mode 100644 src/lib/models.ts diff --git a/src/components/chat/Composer.test.tsx b/src/components/chat/Composer.test.tsx index 24a2f20..ca23e93 100644 --- a/src/components/chat/Composer.test.tsx +++ b/src/components/chat/Composer.test.tsx @@ -1134,7 +1134,7 @@ describe("Composer", () => { it("shows compact warning when context is >= 90% full", () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 190_000, totalOutputTokens: 0, totalCostUsd: 1.5, numTurns: 10 }, + "ws-1": { totalInputTokens: 950_000, totalOutputTokens: 0, totalCostUsd: 1.5, numTurns: 10 }, }, }); render(); @@ -1145,7 +1145,7 @@ describe("Composer", () => { const onSend = vi.fn(); useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 195_000, totalOutputTokens: 0, totalCostUsd: 2.0, numTurns: 15 }, + "ws-1": { totalInputTokens: 950_000, totalOutputTokens: 0, totalCostUsd: 2.0, numTurns: 15 }, }, }); const user = userEvent.setup(); @@ -1157,7 +1157,7 @@ describe("Composer", () => { it("does not show compact warning when context < 90%", () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 100_000, totalOutputTokens: 0, totalCostUsd: 0.5, numTurns: 5 }, + "ws-1": { totalInputTokens: 500_000, totalOutputTokens: 0, totalCostUsd: 0.5, numTurns: 5 }, }, }); render(); @@ -1167,7 +1167,7 @@ describe("Composer", () => { it("does not show compact warning when agent is running", () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 195_000, totalOutputTokens: 0, totalCostUsd: 2.0, numTurns: 15 }, + "ws-1": { totalInputTokens: 950_000, totalOutputTokens: 0, totalCostUsd: 2.0, numTurns: 15 }, }, }); render(); @@ -1177,7 +1177,7 @@ describe("Composer", () => { it("shows context usage ring when tokens > 0", () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 50_000, totalOutputTokens: 1000, totalCostUsd: 0.25, numTurns: 3 }, + "ws-1": { totalInputTokens: 250_000, totalOutputTokens: 1000, totalCostUsd: 0.25, numTurns: 3 }, }, }); render(); @@ -1187,7 +1187,7 @@ describe("Composer", () => { it("applies warning color to ring when context is 75-89% full", () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 160_000, totalOutputTokens: 0, totalCostUsd: 1.0, numTurns: 8 }, + "ws-1": { totalInputTokens: 800_000, totalOutputTokens: 0, totalCostUsd: 1.0, numTurns: 8 }, }, }); render(); @@ -1201,7 +1201,7 @@ describe("Composer", () => { it("shows tooltip with context details on hover", async () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 50_000, totalOutputTokens: 2000, totalCostUsd: 0.25, numTurns: 3 }, + "ws-1": { totalInputTokens: 250_000, totalOutputTokens: 2000, totalCostUsd: 0.25, numTurns: 3 }, }, }); const user = userEvent.setup(); @@ -1216,7 +1216,7 @@ describe("Composer", () => { it("shows cache read row in tooltip when cache tokens > 0", async () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 50_000, totalOutputTokens: 2000, totalCostUsd: 0.25, numTurns: 3, totalCacheReadTokens: 10_000 }, + "ws-1": { totalInputTokens: 250_000, totalOutputTokens: 2000, totalCostUsd: 0.25, numTurns: 3, totalCacheReadTokens: 10_000 }, }, }); const user = userEvent.setup(); @@ -1762,7 +1762,7 @@ describe("Composer", () => { it("hides context tooltip on mouse leave", async () => { useChatStore.setState({ sessionStats: { - "ws-1": { totalInputTokens: 50_000, totalOutputTokens: 1000, totalCostUsd: 0.25, numTurns: 3 }, + "ws-1": { totalInputTokens: 250_000, totalOutputTokens: 1000, totalCostUsd: 0.25, numTurns: 3 }, }, }); const user = userEvent.setup(); diff --git a/src/components/chat/Composer.tsx b/src/components/chat/Composer.tsx index 03b0510..46b88ff 100644 --- a/src/components/chat/Composer.tsx +++ b/src/components/chat/Composer.tsx @@ -12,7 +12,8 @@ import { useVoiceInput } from "../../hooks/useVoiceInput"; import { useToastStore } from "../../stores/toastStore"; import { ActionBar, ActionBarButton } from "./ActionBar"; import { QuestionCard } from "./QuestionCard"; -import { ContextUsageIndicator, CONTEXT_WINDOW_TOKENS } from "./ContextUsageIndicator"; +import { ContextUsageIndicator } from "./ContextUsageIndicator"; +import { getContextWindow } from "../../lib/models"; import { FileChipIcon } from "./FileChipIcon"; import { useFileDropHandler } from "../../hooks/useFileDropHandler"; import { useSlashCommandAutocomplete } from "../../hooks/useSlashCommandAutocomplete"; @@ -469,10 +470,10 @@ export function Composer({ contextId, contextType, agentStatus, onSend, onStop, )} {/* Context near-full warning with compact button */} - {sessionStats && sessionStats.totalInputTokens > 0 && (sessionStats.totalInputTokens / CONTEXT_WINDOW_TOKENS) >= 0.9 && agentStatus === "Idle" && ( + {sessionStats && sessionStats.totalInputTokens > 0 && (sessionStats.totalInputTokens / getContextWindow(selectedModel)) >= 0.9 && agentStatus === "Idle" && ( } - description={Context window is {Math.round((sessionStats.totalInputTokens / CONTEXT_WINDOW_TOKENS) * 100)}% full. Compact to free space.} + description={Context window is {Math.round((sessionStats.totalInputTokens / getContextWindow(selectedModel)) * 100)}% full. Compact to free space.} bgStyle={{ backgroundColor: "color-mix(in srgb, var(--error) 10%, transparent)" }} primaryAction={