From a34bc622dea456a73b60ed04f8ec692bd1811dba Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 24 Mar 2026 18:12:59 +0000 Subject: [PATCH 01/11] Context window bugs --- .../components/ContextUsageIndicator.tsx | 4 ---- .../sessions/components/buildConversationItems.ts | 2 ++ .../session-update/CompactBoundaryView.tsx | 10 +++++++++- .../session-update/SessionUpdateView.tsx | 2 ++ packages/agent/src/adapters/claude/claude-agent.ts | 10 +++------- .../src/adapters/claude/conversion/sdk-to-acp.ts | 3 ++- .../agent/src/adapters/claude/session/models.ts | 14 +------------- 7 files changed, 19 insertions(+), 26 deletions(-) diff --git a/apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx b/apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx index 99e121a1e..553a0b862 100644 --- a/apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx +++ b/apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx @@ -1,8 +1,6 @@ import { Text, Tooltip } from "@radix-ui/themes"; import { useContextUsageForTask } from "@renderer/features/sessions/hooks/useSession"; -const CONTEXT_WARNING_THRESHOLD_PCT = 40; - interface ContextUsageIndicatorProps { taskId?: string; } @@ -13,8 +11,6 @@ export function ContextUsageIndicator({ taskId }: ContextUsageIndicatorProps) { const percent = Math.round((contextUsage.used / contextUsage.size) * 100); - if (percent < CONTEXT_WARNING_THRESHOLD_PCT) return null; - return ( 0 + ? Math.round((preTokens / contextSize) * 100) + : null; return ( @@ -27,7 +33,9 @@ export function CompactBoundaryView({ {trigger} - (~{tokensK}K tokens summarized) + {percent !== null + ? `(${percent}% of context · ~${tokensK}K tokens summarized)` + : `(~${tokensK}K tokens summarized)`} diff --git a/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx b/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx index 5a34c5b21..51fa6112e 100644 --- a/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx +++ b/apps/code/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx @@ -23,6 +23,7 @@ export type RenderItem = sessionUpdate: "compact_boundary"; trigger: "manual" | "auto"; preTokens: number; + contextSize?: number; } | { sessionUpdate: "status"; @@ -101,6 +102,7 @@ export const SessionUpdateView = memo(function SessionUpdateView({ ); case "status": diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 092f4fdc9..35b1faa2a 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -595,8 +595,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { async unstable_setSessionModel( params: SetSessionModelRequest, ): Promise { - const sdkModelId = toSdkModelId(params.modelId); - await this.session.query.setModel(sdkModelId); + await this.session.query.setModel(params.modelId); this.session.modelId = params.modelId; this.rebuildEffortConfigOption(params.modelId); await this.updateConfigOption("model", params.modelId); @@ -894,11 +893,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const resolvedModelId = settingsModel || modelOptions.currentModelId; session.modelId = resolvedModelId; - if (!isResume) { - const resolvedSdkModel = toSdkModelId(resolvedModelId); - if (resolvedSdkModel !== DEFAULT_MODEL) { - await this.session.query.setModel(resolvedSdkModel); - } + if (!isResume && resolvedModelId !== DEFAULT_MODEL) { + await this.session.query.setModel(resolvedModelId); } const availableModes = getAvailableModes(); diff --git a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts index 4d0f65d00..938e7704c 100644 --- a/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts +++ b/packages/agent/src/adapters/claude/conversion/sdk-to-acp.ts @@ -544,7 +544,7 @@ export async function handleSystemMessage( message: Extract, context: MessageHandlerContext, ): Promise { - const { sessionId, client, logger } = context; + const { session, sessionId, client, logger } = context; switch (message.subtype) { case "init": @@ -554,6 +554,7 @@ export async function handleSystemMessage( sessionId, trigger: message.compact_metadata.trigger, preTokens: message.compact_metadata.pre_tokens, + contextSize: session.contextSize, }); break; case "hook_response": diff --git a/packages/agent/src/adapters/claude/session/models.ts b/packages/agent/src/adapters/claude/session/models.ts index 034cf46c1..6aeba6824 100644 --- a/packages/agent/src/adapters/claude/session/models.ts +++ b/packages/agent/src/adapters/claude/session/models.ts @@ -1,16 +1,4 @@ -export const DEFAULT_MODEL = "opus"; - -const GATEWAY_TO_SDK_MODEL: Record = { - "claude-opus-4-5": "opus", - "claude-opus-4-6": "opus", - "claude-sonnet-4-5": "sonnet", - "claude-sonnet-4-6": "sonnet", - "claude-haiku-4-5": "haiku", -}; - -export function toSdkModelId(modelId: string): string { - return GATEWAY_TO_SDK_MODEL[modelId] ?? modelId; -} +export const DEFAULT_MODEL = "claude-opus-4-6"; const MODELS_WITH_1M_CONTEXT = new Set([ "claude-opus-4-6", From ac0a321807e6aa03207505b5ea6c0657be06ad48 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 24 Mar 2026 18:22:40 +0000 Subject: [PATCH 02/11] new usage indicator --- .../components/ContextUsageIndicator.tsx | 37 --------- .../components/EditorToolbar.tsx | 2 - .../components/ContextUsageIndicator.tsx | 82 +++++++++++++++++++ .../sessions/components/ConversationView.tsx | 3 + .../sessions/components/SessionFooter.tsx | 74 +++++++++++------ .../components/buildConversationItems.ts | 1 + .../sessions/hooks/useContextUsage.ts | 59 +++++++++++++ .../agent/src/adapters/claude/claude-agent.ts | 30 ++++++- packages/agent/src/adapters/claude/types.ts | 2 + 9 files changed, 222 insertions(+), 68 deletions(-) delete mode 100644 apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx create mode 100644 apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx create mode 100644 apps/code/src/renderer/features/sessions/hooks/useContextUsage.ts diff --git a/apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx b/apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx deleted file mode 100644 index 553a0b862..000000000 --- a/apps/code/src/renderer/features/message-editor/components/ContextUsageIndicator.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Text, Tooltip } from "@radix-ui/themes"; -import { useContextUsageForTask } from "@renderer/features/sessions/hooks/useSession"; - -interface ContextUsageIndicatorProps { - taskId?: string; -} - -export function ContextUsageIndicator({ taskId }: ContextUsageIndicatorProps) { - const contextUsage = useContextUsageForTask(taskId); - if (!contextUsage || contextUsage.size <= 0) return null; - - const percent = Math.round((contextUsage.used / contextUsage.size) * 100); - - return ( - - - {percent}% - - - ); -} - -function getContextColor(percent: number): string { - if (percent >= 80) return "var(--red-9)"; - if (percent >= 50) return "var(--yellow-11)"; - return "var(--green-9)"; -} diff --git a/apps/code/src/renderer/features/message-editor/components/EditorToolbar.tsx b/apps/code/src/renderer/features/message-editor/components/EditorToolbar.tsx index 8149a0b0d..2f5efb8eb 100644 --- a/apps/code/src/renderer/features/message-editor/components/EditorToolbar.tsx +++ b/apps/code/src/renderer/features/message-editor/components/EditorToolbar.tsx @@ -2,7 +2,6 @@ import { ModelSelector } from "@features/sessions/components/ModelSelector"; import { Flex } from "@radix-ui/themes"; import type { FileAttachment, MentionChip } from "../utils/content"; import { AttachmentMenu } from "./AttachmentMenu"; -import { ContextUsageIndicator } from "./ContextUsageIndicator"; interface EditorToolbarProps { disabled?: boolean; @@ -43,7 +42,6 @@ export function EditorToolbar({ {!hideSelectors && ( )} - ); } diff --git a/apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx b/apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx new file mode 100644 index 000000000..dc859afea --- /dev/null +++ b/apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx @@ -0,0 +1,82 @@ +import { Tooltip } from "@components/ui/Tooltip"; +import type { ContextUsage } from "@features/sessions/hooks/useContextUsage"; +import { Flex, Text } from "@radix-ui/themes"; + +function formatTokensCompact(tokens: number): string { + if (tokens >= 1_000_000) { + return `${(tokens / 1_000_000).toFixed(1)}M`; + } + return `${Math.round(tokens / 1000)}K`; +} + +function formatTokensFull(tokens: number): string { + return tokens.toLocaleString(); +} + +function getUsageColor(percentage: number): string { + if (percentage >= 90) return "var(--red-9)"; + if (percentage >= 75) return "var(--orange-9)"; + if (percentage >= 50) return "var(--amber-9)"; + return "var(--green-9)"; +} + +const CIRCLE_SIZE = 20; +const STROKE_WIDTH = 2.5; +const RADIUS = (CIRCLE_SIZE - STROKE_WIDTH) / 2; +const CIRCUMFERENCE = 2 * Math.PI * RADIUS; + +interface ContextUsageIndicatorProps { + usage: ContextUsage | null; +} + +export function ContextUsageIndicator({ usage }: ContextUsageIndicatorProps) { + if (!usage) return null; + + const { used, size, percentage, cost } = usage; + const strokeDashoffset = CIRCUMFERENCE - (percentage / 100) * CIRCUMFERENCE; + const color = getUsageColor(percentage); + + const tooltipLines = [ + `Context: ${formatTokensFull(used)} / ${formatTokensFull(size)} tokens (${percentage}%)`, + ]; + if (cost) { + tooltipLines.push(`Cost: $${cost.amount.toFixed(2)}`); + } + + return ( + + + + + + + + {formatTokensCompact(used)}/{formatTokensCompact(size)} + + + + ); +} diff --git a/apps/code/src/renderer/features/sessions/components/ConversationView.tsx b/apps/code/src/renderer/features/sessions/components/ConversationView.tsx index 5ef58cb37..403e013c9 100644 --- a/apps/code/src/renderer/features/sessions/components/ConversationView.tsx +++ b/apps/code/src/renderer/features/sessions/components/ConversationView.tsx @@ -1,3 +1,4 @@ +import { useContextUsage } from "@features/sessions/hooks/useContextUsage"; import { sessionStoreSetters, useOptimisticItemsForTask, @@ -52,6 +53,7 @@ export function ConversationView({ const agentLogsEnabled = useFeatureFlag("posthog-code-background-agent-logs"); const debugLogsCloudRuns = useSettingsStore((s) => s.debugLogsCloudRuns); const showDebugLogs = agentLogsEnabled && debugLogsCloudRuns; + const contextUsage = useContextUsage(events); const { items: conversationItems, @@ -229,6 +231,7 @@ export function ConversationView({ hasPendingPermission={pendingPermissionsCount > 0} pausedDurationMs={pausedDurationMs} isCompacting={isCompacting} + usage={contextUsage} /> } diff --git a/apps/code/src/renderer/features/sessions/components/SessionFooter.tsx b/apps/code/src/renderer/features/sessions/components/SessionFooter.tsx index 4f22a07bd..f32068d56 100644 --- a/apps/code/src/renderer/features/sessions/components/SessionFooter.tsx +++ b/apps/code/src/renderer/features/sessions/components/SessionFooter.tsx @@ -1,6 +1,8 @@ +import type { ContextUsage } from "@features/sessions/hooks/useContextUsage"; import { Pause } from "@phosphor-icons/react"; import { Box, Flex, Text } from "@radix-ui/themes"; +import { ContextUsageIndicator } from "./ContextUsageIndicator"; import { formatDuration, GeneratingIndicator } from "./GeneratingIndicator"; interface SessionFooterProps { @@ -12,6 +14,7 @@ interface SessionFooterProps { hasPendingPermission?: boolean; pausedDurationMs?: number; isCompacting?: boolean; + usage?: ContextUsage | null; } export function SessionFooter({ @@ -23,20 +26,23 @@ export function SessionFooter({ hasPendingPermission = false, pausedDurationMs, isCompacting = false, + usage, }: SessionFooterProps) { if (isPromptPending && !isCompacting) { - // Show static "waiting" state when permission is pending if (hasPendingPermission) { return ( - - - Awaiting permission... + + + + Awaiting permission... + + ); @@ -44,16 +50,19 @@ export function SessionFooter({ return ( - - - {queuedCount > 0 && ( - - ({queuedCount} queued) - - )} + + + + {queuedCount > 0 && ( + + ({queuedCount} queued) + + )} + + ); @@ -69,13 +78,26 @@ export function SessionFooter({ ) { return ( - - Generated in {formatDuration(lastGenerationDuration)} - + + + Generated in {formatDuration(lastGenerationDuration)} + + + + + ); + } + + if (usage) { + return ( + + + + ); } diff --git a/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts b/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts index 4196ac542..7c0fa9a4b 100644 --- a/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts +++ b/apps/code/src/renderer/features/sessions/components/buildConversationItems.ts @@ -551,6 +551,7 @@ function processSessionUpdate(b: ItemBuilder, update: SessionUpdate) { case "plan": case "available_commands_update": case "config_option_update": + case "usage_update": break; default: { diff --git a/apps/code/src/renderer/features/sessions/hooks/useContextUsage.ts b/apps/code/src/renderer/features/sessions/hooks/useContextUsage.ts new file mode 100644 index 000000000..b340e9abb --- /dev/null +++ b/apps/code/src/renderer/features/sessions/hooks/useContextUsage.ts @@ -0,0 +1,59 @@ +import type { AcpMessage } from "@shared/types/session-events"; +import { useMemo } from "react"; + +export interface ContextUsage { + used: number; + size: number; + percentage: number; + cost: { amount: number; currency: string } | null; +} + +/** + * Extract the latest context window usage from session events. + * Scans backwards to find the most recent usage_update notification. + * Re-derives on each new event, giving live updates during streaming. + */ +export function useContextUsage(events: AcpMessage[]): ContextUsage | null { + return useMemo(() => extractContextUsage(events), [events]); +} + +export function extractContextUsage(events: AcpMessage[]): ContextUsage | null { + for (let i = events.length - 1; i >= 0; i--) { + const msg = events[i].message; + if ( + "method" in msg && + msg.method === "session/update" && + !("id" in msg) && + "params" in msg + ) { + const params = msg.params as + | { + update?: { + sessionUpdate?: string; + used?: number; + size?: number; + cost?: { amount: number; currency: string } | null; + }; + } + | undefined; + const update = params?.update; + if ( + update?.sessionUpdate === "usage_update" && + typeof update.used === "number" && + typeof update.size === "number" + ) { + const percentage = + update.size > 0 + ? Math.min(100, Math.round((update.used / update.size) * 100)) + : 0; + return { + used: update.used, + size: update.size, + percentage, + cost: update.cost ?? null, + }; + } + } + } + return null; +} diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 35b1faa2a..fccf66753 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -323,6 +323,12 @@ export class ClaudeAcpAgent extends BaseAcpAgent { this.session.promptRunning = true; let handedOff = false; let lastAssistantTotalUsage: number | null = null; + if (this.session.lastContextWindowSize == null) { + this.session.lastContextWindowSize = getDefaultContextWindow( + this.session.modelId ?? "", + ); + } + let lastContextWindowSize = this.session.lastContextWindowSize; const supportsTerminalOutput = ( @@ -397,12 +403,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const contextWindows = Object.values(message.modelUsage).map( (m) => m.contextWindow, ); - const contextWindowSize = + lastContextWindowSize = contextWindows.length > 0 ? Math.min(...contextWindows) : getDefaultContextWindow(this.session.modelId ?? ""); + this.session.lastContextWindowSize = lastContextWindowSize; - this.session.contextSize = contextWindowSize; + this.session.contextSize = lastContextWindowSize; if (lastAssistantTotalUsage !== null) { this.session.contextUsed = lastAssistantTotalUsage; } @@ -414,7 +421,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { update: { sessionUpdate: "usage_update", used: lastAssistantTotalUsage, - size: contextWindowSize, + size: lastContextWindowSize, cost: { amount: message.total_cost_usd, currency: "USD", @@ -524,6 +531,16 @@ export class ClaudeAcpAgent extends BaseAcpAgent { usage.output_tokens + usage.cache_read_input_tokens + usage.cache_creation_input_tokens; + + await this.client.sessionUpdate({ + sessionId: params.sessionId, + update: { + sessionUpdate: "usage_update", + used: lastAssistantTotalUsage, + size: lastContextWindowSize, + cost: null, + }, + }); } const result = await handleUserAssistantMessage(message, context); @@ -597,6 +614,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent { ): Promise { await this.session.query.setModel(params.modelId); this.session.modelId = params.modelId; + this.session.lastContextWindowSize = getDefaultContextWindow( + params.modelId, + ); this.rebuildEffortConfigOption(params.modelId); await this.updateConfigOption("model", params.modelId); return {}; @@ -673,6 +693,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const sdkModelId = toSdkModelId(resolvedValue); await this.session.query.setModel(sdkModelId); this.session.modelId = resolvedValue; + this.session.lastContextWindowSize = getDefaultContextWindow( + resolvedValue, + ); this.rebuildEffortConfigOption(resolvedValue); } else if (params.configId === "effort") { const newEffort = resolvedValue as EffortLevel; @@ -892,6 +915,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const modelOptions = await this.getModelConfigOptions(); const resolvedModelId = settingsModel || modelOptions.currentModelId; session.modelId = resolvedModelId; + session.lastContextWindowSize = getDefaultContextWindow(resolvedModelId); if (!isResume && resolvedModelId !== DEFAULT_MODEL) { await this.session.query.setModel(resolvedModelId); diff --git a/packages/agent/src/adapters/claude/types.ts b/packages/agent/src/adapters/claude/types.ts index 453c1a44f..6dfc6277c 100644 --- a/packages/agent/src/adapters/claude/types.ts +++ b/packages/agent/src/adapters/claude/types.ts @@ -57,6 +57,8 @@ export type Session = BaseSession & { contextUsed?: number; /** Context window size in tokens */ contextSize?: number; + /** Persists across prompt() calls so SDK-reported values survive turn boundaries */ + lastContextWindowSize?: number; promptRunning: boolean; pendingMessages: Map; nextPendingOrder: number; From 4baa0d8501e03b8d84abc5c308902bd85a6e94f6 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 24 Mar 2026 18:28:28 +0000 Subject: [PATCH 03/11] Update claude-agent.ts --- packages/agent/src/adapters/claude/claude-agent.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index fccf66753..6f31f59ec 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -399,14 +399,11 @@ export class ClaudeAcpAgent extends BaseAcpAgent { this.session.accumulatedUsage.cachedWriteTokens += message.usage.cache_creation_input_tokens; - // Calculate context window size from modelUsage (minimum across all models used) - const contextWindows = Object.values(message.modelUsage).map( - (m) => m.contextWindow, + // Use our model-aware default — the SDK binary may underreport + // context window size (e.g. 200k for models that support 1M). + lastContextWindowSize = getDefaultContextWindow( + this.session.modelId ?? "", ); - lastContextWindowSize = - contextWindows.length > 0 - ? Math.min(...contextWindows) - : getDefaultContextWindow(this.session.modelId ?? ""); this.session.lastContextWindowSize = lastContextWindowSize; this.session.contextSize = lastContextWindowSize; From 704f5b6b15b6d6e17845e1f0628aed6ceca8f50b Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 24 Mar 2026 23:12:30 +0000 Subject: [PATCH 04/11] Update ContextUsageIndicator.tsx --- .../sessions/components/ContextUsageIndicator.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx b/apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx index dc859afea..ce1411a1d 100644 --- a/apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx +++ b/apps/code/src/renderer/features/sessions/components/ContextUsageIndicator.tsx @@ -32,19 +32,15 @@ interface ContextUsageIndicatorProps { export function ContextUsageIndicator({ usage }: ContextUsageIndicatorProps) { if (!usage) return null; - const { used, size, percentage, cost } = usage; + const { used, size, percentage } = usage; const strokeDashoffset = CIRCUMFERENCE - (percentage / 100) * CIRCUMFERENCE; const color = getUsageColor(percentage); - const tooltipLines = [ - `Context: ${formatTokensFull(used)} / ${formatTokensFull(size)} tokens (${percentage}%)`, - ]; - if (cost) { - tooltipLines.push(`Cost: $${cost.amount.toFixed(2)}`); - } - return ( - + Date: Wed, 25 Mar 2026 07:16:02 +0000 Subject: [PATCH 05/11] Update claude-agent.ts --- packages/agent/src/adapters/claude/claude-agent.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 6f31f59ec..2299c3314 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -399,11 +399,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent { this.session.accumulatedUsage.cachedWriteTokens += message.usage.cache_creation_input_tokens; - // Use our model-aware default — the SDK binary may underreport - // context window size (e.g. 200k for models that support 1M). - lastContextWindowSize = getDefaultContextWindow( - this.session.modelId ?? "", + // Use SDK-reported context window size, fall back to our default + const contextWindows = Object.values(message.modelUsage).map( + (m) => m.contextWindow, ); + lastContextWindowSize = + contextWindows.length > 0 + ? Math.min(...contextWindows) + : getDefaultContextWindow(this.session.modelId ?? ""); this.session.lastContextWindowSize = lastContextWindowSize; this.session.contextSize = lastContextWindowSize; From bb4d316fb8235bb7a097b3dddb6f41626291e7f2 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 25 Mar 2026 07:33:47 +0000 Subject: [PATCH 06/11] updates --- packages/agent/src/adapters/base-acp-agent.ts | 13 ++++++-- .../agent/src/adapters/claude/claude-agent.ts | 32 +++++++++++++------ .../src/adapters/claude/session/models.ts | 4 --- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/packages/agent/src/adapters/base-acp-agent.ts b/packages/agent/src/adapters/base-acp-agent.ts index 93db4e3ee..792efe416 100644 --- a/packages/agent/src/adapters/base-acp-agent.ts +++ b/packages/agent/src/adapters/base-acp-agent.ts @@ -20,6 +20,7 @@ import { DEFAULT_GATEWAY_MODEL, fetchGatewayModels, formatGatewayModelName, + type GatewayModel, isAnthropicModel, } from "../gateway-models"; import { Logger } from "../utils/logger"; @@ -33,6 +34,8 @@ export interface BaseSession { settingsManager: SettingsManager; } +const DEFAULT_CONTEXT_WINDOW = 200_000; + export abstract class BaseAcpAgent implements Agent { abstract readonly adapterName: string; protected session!: BaseSession; @@ -40,6 +43,7 @@ export abstract class BaseAcpAgent implements Agent { client: AgentSideConnection; logger: Logger; fileContentCache: { [key: string]: string } = {}; + protected gatewayModels: GatewayModel[] = []; constructor(client: AgentSideConnection) { this.client = client; @@ -119,9 +123,9 @@ export abstract class BaseAcpAgent implements Agent { currentModelId: string; options: SessionConfigSelectOption[]; }> { - const gatewayModels = await fetchGatewayModels(); + this.gatewayModels = await fetchGatewayModels(); - const options = gatewayModels + const options = this.gatewayModels .filter((model) => isAnthropicModel(model)) .map((model) => ({ value: model.id, @@ -150,4 +154,9 @@ export abstract class BaseAcpAgent implements Agent { return { currentModelId, options }; } + + getContextWindowForModel(modelId: string): number { + const match = this.gatewayModels.find((m) => m.id === modelId); + return match?.context_window ?? DEFAULT_CONTEXT_WINDOW; + } } diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 2299c3314..fded551ff 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -64,7 +64,6 @@ import { getAvailableSlashCommands } from "./session/commands"; import { parseMcpServers } from "./session/mcp-config"; import { DEFAULT_MODEL, - getDefaultContextWindow, getEffortOptions, resolveModelPreference, toSdkModelId, @@ -324,9 +323,13 @@ export class ClaudeAcpAgent extends BaseAcpAgent { let handedOff = false; let lastAssistantTotalUsage: number | null = null; if (this.session.lastContextWindowSize == null) { - this.session.lastContextWindowSize = getDefaultContextWindow( + this.session.lastContextWindowSize = this.getContextWindowForModel( this.session.modelId ?? "", ); + this.logger.debug("Initial context window size from gateway", { + modelId: this.session.modelId, + contextWindowSize: this.session.lastContextWindowSize, + }); } let lastContextWindowSize = this.session.lastContextWindowSize; @@ -399,15 +402,23 @@ export class ClaudeAcpAgent extends BaseAcpAgent { this.session.accumulatedUsage.cachedWriteTokens += message.usage.cache_creation_input_tokens; - // Use SDK-reported context window size, fall back to our default + // SDK can underreport context window (e.g. 200k for 1M models). + // Use SDK value only if it's larger than what gateway reported. const contextWindows = Object.values(message.modelUsage).map( (m) => m.contextWindow, ); - lastContextWindowSize = - contextWindows.length > 0 - ? Math.min(...contextWindows) - : getDefaultContextWindow(this.session.modelId ?? ""); + if (contextWindows.length > 0) { + const sdkContextWindow = Math.min(...contextWindows); + if (sdkContextWindow > lastContextWindowSize) { + lastContextWindowSize = sdkContextWindow; + } + } this.session.lastContextWindowSize = lastContextWindowSize; + this.logger.debug("Context window size from result", { + sdkReported: contextWindows, + resolved: lastContextWindowSize, + modelId: this.session.modelId, + }); this.session.contextSize = lastContextWindowSize; if (lastAssistantTotalUsage !== null) { @@ -614,7 +625,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { ): Promise { await this.session.query.setModel(params.modelId); this.session.modelId = params.modelId; - this.session.lastContextWindowSize = getDefaultContextWindow( + this.session.lastContextWindowSize = this.getContextWindowForModel( params.modelId, ); this.rebuildEffortConfigOption(params.modelId); @@ -693,7 +704,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const sdkModelId = toSdkModelId(resolvedValue); await this.session.query.setModel(sdkModelId); this.session.modelId = resolvedValue; - this.session.lastContextWindowSize = getDefaultContextWindow( + this.session.lastContextWindowSize = this.getContextWindowForModel( resolvedValue, ); this.rebuildEffortConfigOption(resolvedValue); @@ -915,7 +926,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const modelOptions = await this.getModelConfigOptions(); const resolvedModelId = settingsModel || modelOptions.currentModelId; session.modelId = resolvedModelId; - session.lastContextWindowSize = getDefaultContextWindow(resolvedModelId); + session.lastContextWindowSize = + this.getContextWindowForModel(resolvedModelId); if (!isResume && resolvedModelId !== DEFAULT_MODEL) { await this.session.query.setModel(resolvedModelId); diff --git a/packages/agent/src/adapters/claude/session/models.ts b/packages/agent/src/adapters/claude/session/models.ts index 6aeba6824..b8cfd36eb 100644 --- a/packages/agent/src/adapters/claude/session/models.ts +++ b/packages/agent/src/adapters/claude/session/models.ts @@ -9,10 +9,6 @@ export function supports1MContext(modelId: string): boolean { return MODELS_WITH_1M_CONTEXT.has(modelId); } -export function getDefaultContextWindow(modelId: string): number { - return supports1MContext(modelId) ? 1_000_000 : 200_000; -} - const MODELS_WITH_EFFORT = new Set([ "claude-opus-4-5", "claude-opus-4-6", From 61d0fcbb7eb3529f09626291cfe823671e979885 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 25 Mar 2026 07:38:11 +0000 Subject: [PATCH 07/11] Update claude-agent.ts --- packages/agent/src/adapters/claude/claude-agent.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index fded551ff..a0252f042 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -538,10 +538,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { cache_creation_input_tokens: number; }; lastAssistantTotalUsage = - usage.input_tokens + - usage.output_tokens + - usage.cache_read_input_tokens + - usage.cache_creation_input_tokens; + usage.input_tokens + usage.cache_read_input_tokens; await this.client.sessionUpdate({ sessionId: params.sessionId, From 8b671bf54c43251eb5e778b7eb481bcf93ef9f68 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 25 Mar 2026 09:02:41 +0000 Subject: [PATCH 08/11] Update claude-agent.ts --- packages/agent/src/adapters/claude/claude-agent.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index a0252f042..33bbe9f93 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -538,7 +538,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent { cache_creation_input_tokens: number; }; lastAssistantTotalUsage = - usage.input_tokens + usage.cache_read_input_tokens; + usage.input_tokens + + usage.cache_read_input_tokens + + usage.cache_creation_input_tokens; await this.client.sessionUpdate({ sessionId: params.sessionId, From 178ac2da2409d62f241a2453fc87576a3b0b1bbe Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 25 Mar 2026 09:41:52 +0000 Subject: [PATCH 09/11] restore toSdkModelId to fix compaction loop --- packages/agent/src/adapters/claude/claude-agent.ts | 7 ++++--- .../agent/src/adapters/claude/session/models.ts | 14 +++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 33bbe9f93..7317e0c4b 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -622,7 +622,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent { async unstable_setSessionModel( params: SetSessionModelRequest, ): Promise { - await this.session.query.setModel(params.modelId); + await this.session.query.setModel(toSdkModelId(params.modelId)); this.session.modelId = params.modelId; this.session.lastContextWindowSize = this.getContextWindowForModel( params.modelId, @@ -928,8 +928,9 @@ export class ClaudeAcpAgent extends BaseAcpAgent { session.lastContextWindowSize = this.getContextWindowForModel(resolvedModelId); - if (!isResume && resolvedModelId !== DEFAULT_MODEL) { - await this.session.query.setModel(resolvedModelId); + const resolvedSdkModel = toSdkModelId(resolvedModelId); + if (!isResume && resolvedSdkModel !== DEFAULT_MODEL) { + await this.session.query.setModel(resolvedSdkModel); } const availableModes = getAvailableModes(); diff --git a/packages/agent/src/adapters/claude/session/models.ts b/packages/agent/src/adapters/claude/session/models.ts index b8cfd36eb..b683bf88e 100644 --- a/packages/agent/src/adapters/claude/session/models.ts +++ b/packages/agent/src/adapters/claude/session/models.ts @@ -1,4 +1,16 @@ -export const DEFAULT_MODEL = "claude-opus-4-6"; +export const DEFAULT_MODEL = "opus"; + +const GATEWAY_TO_SDK_MODEL: Record = { + "claude-opus-4-5": "opus", + "claude-opus-4-6": "opus", + "claude-sonnet-4-5": "sonnet", + "claude-sonnet-4-6": "sonnet", + "claude-haiku-4-5": "haiku", +}; + +export function toSdkModelId(modelId: string): string { + return GATEWAY_TO_SDK_MODEL[modelId] ?? modelId; +} const MODELS_WITH_1M_CONTEXT = new Set([ "claude-opus-4-6", From 4ff10a65ecda49e9238bcb72970fad0151c42243 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 25 Mar 2026 14:28:03 +0000 Subject: [PATCH 10/11] lint --- packages/agent/src/adapters/claude/claude-agent.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/agent/src/adapters/claude/claude-agent.ts b/packages/agent/src/adapters/claude/claude-agent.ts index 7317e0c4b..92f9ddf1a 100644 --- a/packages/agent/src/adapters/claude/claude-agent.ts +++ b/packages/agent/src/adapters/claude/claude-agent.ts @@ -703,9 +703,8 @@ export class ClaudeAcpAgent extends BaseAcpAgent { const sdkModelId = toSdkModelId(resolvedValue); await this.session.query.setModel(sdkModelId); this.session.modelId = resolvedValue; - this.session.lastContextWindowSize = this.getContextWindowForModel( - resolvedValue, - ); + this.session.lastContextWindowSize = + this.getContextWindowForModel(resolvedValue); this.rebuildEffortConfigOption(resolvedValue); } else if (params.configId === "effort") { const newEffort = resolvedValue as EffortLevel; From 168d89a631b45560492523e71598f3723ca99e54 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 25 Mar 2026 17:36:58 +0000 Subject: [PATCH 11/11] Update useSession.ts --- .../features/sessions/hooks/useSession.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/apps/code/src/renderer/features/sessions/hooks/useSession.ts b/apps/code/src/renderer/features/sessions/hooks/useSession.ts index fa6f7cae0..246b2d0a1 100644 --- a/apps/code/src/renderer/features/sessions/hooks/useSession.ts +++ b/apps/code/src/renderer/features/sessions/hooks/useSession.ts @@ -147,25 +147,6 @@ export const useThoughtLevelConfigOptionForTask = ( return useConfigOptionForTask(taskId, "thought_level"); }; -/** Get context window usage for a task (used / size) */ -export const useContextUsageForTask = ( - taskId: string | undefined, -): { used: number; size: number } | undefined => { - return useSessionStore((s) => { - if (!taskId) return undefined; - const taskRunId = s.taskIdIndex[taskId]; - if (!taskRunId) return undefined; - const session = s.sessions[taskRunId]; - if ( - session?.contextUsed === undefined || - session?.contextSize === undefined - ) { - return undefined; - } - return { used: session.contextUsed, size: session.contextSize }; - }); -}; - /** Get the adapter type for a task */ export const useAdapterForTask = ( taskId: string | undefined,