fix(ui): stop the context meter from counting every internal round-trip

The token breakdown of an assistant message accumulates across every API
round-trip inside the turn: each tool call re-reads the whole cached
prompt, so input/cache.read add up to several times the context window.
Every context-usage surface summed those fields, which is why the meter
could read 330% of a 1M window whose real fill was 232,872 tokens
(23.3%), and why reopening an older session jumps the readout (#2562).

The server reports the final round-trip's window as tokens.total
(optional in the message schema; opencode 1.18.18 returns it, verified
against its live /session/:id/message API). Prefer it everywhere the
window fill is displayed and fall back to summing only when the server
did not send it: contextTokensFromBreakdown in tokenUtils now owns that
rule, and the context store extractor, sync store getter, work status
panel, context sidebar, VS Code layout, mini chat, and mobile metadata
all use it instead of their own inline sums.

Fixes #2562
This commit is contained in:
dibanez
2026-08-15 21:46:49 +02:00
parent e3094ee676
commit 9e1a9b59b1
11 changed files with 156 additions and 25 deletions
+4 -3
View File
@@ -86,6 +86,7 @@ import { getRuntimeKey } from "@/lib/runtime-switch"
import { clearLastActiveSession, persistLastActiveSession, readLastActiveSession } from "./last-session-cache"
import { persistWorktreeTopology, readPersistedWorktreeTopology } from "./worktree-topology-cache"
import { rememberRuntimeLiveStatus } from "./runtime-live-memory"
import { contextTokensFromBreakdown } from "@/stores/utils/tokenUtils"
export type { AttachedFile }
@@ -1173,7 +1174,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
const messages = getSyncMessages(sessionId)
if (messages.length === 0) return null
type AssistantTokens = { input: number; output: number; reasoning: number; cache: { read: number; write: number } }
type AssistantTokens = { total?: number; input: number; output: number; reasoning: number; cache: { read: number; write: number } }
let lastTokens: AssistantTokens | undefined
let lastMessageId: string | undefined
for (let i = messages.length - 1; i >= 0; i--) {
@@ -1181,7 +1182,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
if (msg.role !== "assistant") continue
const tokens = (msg as { tokens?: AssistantTokens }).tokens
if (!tokens) continue
const total = tokens.input + tokens.output + tokens.reasoning + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0)
const total = contextTokensFromBreakdown(tokens)
if (total > 0) {
lastTokens = tokens
lastMessageId = msg.id
@@ -1191,7 +1192,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
if (!lastTokens) return null
const totalTokens = lastTokens.input + lastTokens.output + lastTokens.reasoning + (lastTokens.cache?.read ?? 0) + (lastTokens.cache?.write ?? 0)
const totalTokens = contextTokensFromBreakdown(lastTokens)
const thresholdLimit = contextLimit > 0 ? contextLimit : 200000
const percentage = contextLimit > 0 ? Math.round((totalTokens / contextLimit) * 100) : 0
const normalizedOutput = outputLimit > 0 ? Math.round((lastTokens.output / outputLimit) * 100) : undefined