Propagate session context usage percent

This commit is contained in:
Shantur Rathore
2025-11-13 11:17:26 +00:00
parent 041dfc6824
commit f42d0a285d
3 changed files with 56 additions and 134 deletions

View File

@@ -302,19 +302,14 @@ const ContextUsagePanel: Component<{ instanceId: string; sessionId: string }> =
contextWindow: 0, contextWindow: 0,
isSubscriptionModel: false, isSubscriptionModel: false,
contextUsageTokens: 0, contextUsageTokens: 0,
contextUsagePercent: null,
}, },
) )
const tokens = createMemo(() => info().tokens) const tokens = createMemo(() => info().tokens)
const contextUsageTokens = createMemo(() => info().contextUsageTokens ?? 0) const contextUsageTokens = createMemo(() => info().contextUsageTokens ?? 0)
const contextWindow = createMemo(() => info().contextWindow) const contextWindow = createMemo(() => info().contextWindow)
const percentage = createMemo(() => { const contextUsagePercent = createMemo(() => info().contextUsagePercent)
const windowSize = contextWindow()
if (!windowSize || windowSize <= 0) return null
const usage = contextUsageTokens()
const percent = Math.round((usage / windowSize) * 100)
return Math.min(100, Math.max(0, percent))
})
const costLabel = createMemo(() => { const costLabel = createMemo(() => {
if (info().isSubscriptionModel || info().cost <= 0) return "Included in plan" if (info().isSubscriptionModel || info().cost <= 0) return "Included in plan"
@@ -333,7 +328,7 @@ const ContextUsagePanel: Component<{ instanceId: string; sessionId: string }> =
<div class="mt-4"> <div class="mt-4">
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<div class="text-xs font-semibold text-primary/70 uppercase tracking-wide">Context window usage</div> <div class="text-xs font-semibold text-primary/70 uppercase tracking-wide">Context window usage</div>
<div class="text-sm font-medium text-primary">{percentage() !== null ? `${percentage()}%` : "--"}</div> <div class="text-sm font-medium text-primary">{contextUsagePercent() !== null ? `${contextUsagePercent()}%` : "--"}</div>
</div> </div>
<div class="text-sm text-primary/90"> <div class="text-sm text-primary/90">
{contextWindow() {contextWindow()
@@ -344,7 +339,7 @@ const ContextUsagePanel: Component<{ instanceId: string; sessionId: string }> =
<div class="mt-3 h-1.5 rounded-full bg-base relative overflow-hidden"> <div class="mt-3 h-1.5 rounded-full bg-base relative overflow-hidden">
<div <div
class="absolute inset-y-0 left-0 rounded-full bg-accent-primary transition-[width]" class="absolute inset-y-0 left-0 rounded-full bg-accent-primary transition-[width]"
style={{ width: percentage() === null ? "0%" : `${percentage()}%` }} style={{ width: contextUsagePercent() === null ? "0%" : `${contextUsagePercent()}%` }}
/> />
</div> </div>
</div> </div>

View File

@@ -31,14 +31,7 @@ import ToolCall from "./tool-call"
import { sseManager } from "../lib/sse-manager" import { sseManager } from "../lib/sse-manager"
import Kbd from "./kbd" import Kbd from "./kbd"
import { preferences } from "../stores/preferences" import { preferences } from "../stores/preferences"
import { import { getSessionInfo, computeDisplayParts, sessions, setActiveSession, setActiveParentSession } from "../stores/sessions"
providers,
getSessionInfo,
computeDisplayParts,
sessions,
setActiveSession,
setActiveParentSession,
} from "../stores/sessions"
import { setActiveInstanceId } from "../stores/instances" import { setActiveInstanceId } from "../stores/instances"
const SCROLL_OFFSET = 64 const SCROLL_OFFSET = 64
@@ -76,75 +69,6 @@ function navigateToTaskSession(location: TaskSessionLocation) {
} }
} }
// Calculate session tokens and cost from messagesInfo (matches TUI logic)
function calculateSessionInfo(messagesInfo?: Map<string, MessageInfo>, instanceId?: string) {
if (!messagesInfo || messagesInfo.size === 0)
return { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false }
let tokens = 0
let cost = 0
let contextWindow = 0
let modelID = ""
let providerID = ""
let isSubscriptionModel = false
// Go backwards through messages to find the last relevant assistant message (like TUI)
const messageArray = Array.from(messagesInfo.values()).reverse()
for (const info of messageArray) {
if (!info) continue
if (info.role === "assistant" && info.tokens) {
const usage = info.tokens
if (usage.output && usage.output > 0) {
if (info.summary) {
// If summary message, only count output tokens and stop (like TUI)
tokens = usage.output || 0
cost = info.cost || 0
} else {
// Regular message - count all token types (like TUI)
tokens =
(usage.input || 0) +
(usage.cache?.read || 0) +
(usage.cache?.write || 0) +
(usage.output || 0) +
(usage.reasoning || 0)
cost = info.cost || 0
}
// Get model info for context window and subscription check
modelID = info.modelID || ""
providerID = info.providerID || ""
isSubscriptionModel = cost === 0
break
}
}
}
// Try to get context window from providers
if (instanceId && modelID && providerID) {
const instanceProviders = providers().get(instanceId) || []
console.log("[calculateSessionInfo] instanceProviders:", instanceProviders)
console.log("[calculateSessionInfo] looking for providerID:", providerID, "modelID:", modelID)
const provider = instanceProviders.find((p) => p.id === providerID)
console.log("[calculateSessionInfo] found provider:", provider)
if (provider) {
const model = provider.models.find((m) => m.id === modelID)
console.log("[calculateSessionInfo] found model:", model)
if (model?.limit?.context) {
contextWindow = model.limit.context
}
// Check if it's a subscription model (cost is 0 for both input and output)
if (model?.cost?.input === 0 && model?.cost?.output === 0) {
isSubscriptionModel = true
}
}
}
return { tokens, cost, contextWindow, isSubscriptionModel }
}
// Format tokens like TUI (e.g., "110K", "1.2M") // Format tokens like TUI (e.g., "110K", "1.2M")
function formatTokens(tokens: number): string { function formatTokens(tokens: number): string {
if (tokens >= 1000000) { if (tokens >= 1000000) {
@@ -156,16 +80,15 @@ function formatTokens(tokens: number): string {
} }
// Format session info for the session view header // Format session info for the session view header
function formatSessionInfo(tokens: number, _cost: number, contextWindow: number, _isSubscriptionModel: boolean): string { function formatSessionInfo(usageTokens: number, contextWindow: number, usagePercent: number | null): string {
const tokensStr = formatTokens(tokens)
if (contextWindow > 0) { if (contextWindow > 0) {
const windowStr = formatTokens(contextWindow) const windowStr = formatTokens(contextWindow)
const percentage = Math.min(100, Math.max(0, Math.round((tokens / contextWindow) * 100))) const usageStr = formatTokens(usageTokens)
return `${tokensStr} of ${windowStr} (${percentage}%)` const percent = usagePercent ?? Math.min(100, Math.max(0, Math.round((usageTokens / contextWindow) * 100)))
return `${usageStr} of ${windowStr} (${percent}%)`
} }
return tokensStr return formatTokens(usageTokens)
} }
interface MessageStreamProps { interface MessageStreamProps {
@@ -274,30 +197,20 @@ export default function MessageStream(props: MessageStreamProps) {
return `${toolPart.id}:${version}:${status}` return `${toolPart.id}:${version}:${status}`
} }
const sessionInfo = createMemo(() => { const sessionInfo = createMemo(() =>
return ( getSessionInfo(props.instanceId, props.sessionId) ?? {
getSessionInfo(props.instanceId, props.sessionId) || {
tokens: 0,
cost: 0,
contextWindow: 0,
isSubscriptionModel: false,
}
)
})
const formattedSessionInfo = createMemo(() => {
const sessionInfo = getSessionInfo(props.instanceId, props.sessionId) || {
tokens: 0, tokens: 0,
cost: 0, cost: 0,
contextWindow: 0, contextWindow: 0,
isSubscriptionModel: false, isSubscriptionModel: false,
} contextUsageTokens: 0,
return formatSessionInfo( contextUsagePercent: null,
sessionInfo.tokens, },
sessionInfo.cost, )
sessionInfo.contextWindow,
sessionInfo.isSubscriptionModel, const formattedSessionInfo = createMemo(() => {
) const info = sessionInfo()
return formatSessionInfo(info.contextUsageTokens, info.contextWindow, info.contextUsagePercent)
}) })
function isNearBottom(element: HTMLDivElement, offset = SCROLL_OFFSET) { function isNearBottom(element: HTMLDivElement, offset = SCROLL_OFFSET) {

View File

@@ -34,6 +34,7 @@ interface SessionInfo {
contextWindow: number contextWindow: number
isSubscriptionModel: boolean isSubscriptionModel: boolean
contextUsageTokens: number contextUsageTokens: number
contextUsagePercent: number | null
} }
interface SessionForkResponse { interface SessionForkResponse {
@@ -552,8 +553,9 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
let isSubscriptionModel = false let isSubscriptionModel = false
let modelID = "" let modelID = ""
let providerID = "" let providerID = ""
let inputTokensForUsage = 0 let actualUsageTokens = 0
let cacheReadTokensForUsage = 0 let contextUsagePercent: number | null = null
let hasContextUsage = false
// Calculate from last assistant message in this session only // Calculate from last assistant message in this session only
if (session.messagesInfo.size > 0) { if (session.messagesInfo.size > 0) {
@@ -565,23 +567,23 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
const usage = info.tokens const usage = info.tokens
if (usage.output > 0) { if (usage.output > 0) {
const inputTokens = usage.input || 0
const reasoningTokens = usage.reasoning || 0
const cacheReadTokens = usage.cache?.read || 0
const cacheWriteTokens = usage.cache?.write || 0
const outputTokens = usage.output || 0
if (info.summary) { if (info.summary) {
// If summary message, only count output tokens and stop (like TUI) // If summary message, only count output tokens and stop (like TUI)
tokens = usage.output || 0 tokens = outputTokens
cost = info.cost || 0
} else { } else {
// Regular message - count all token types (like TUI) // Regular message - count all token types (like TUI)
tokens = tokens = inputTokens + cacheReadTokens + cacheWriteTokens + outputTokens + reasoningTokens
(usage.input || 0) +
(usage.cache?.read || 0) +
(usage.cache?.write || 0) +
(usage.output || 0) +
(usage.reasoning || 0)
cost = info.cost || 0
} }
inputTokensForUsage = usage.input || 0 cost = info.cost || 0
cacheReadTokensForUsage = usage.cache?.read || 0 actualUsageTokens = tokens
hasContextUsage = inputTokens + cacheReadTokens + cacheWriteTokens > 0
// Get model info identifiers for context lookups // Get model info identifiers for context lookups
modelID = info.modelID || "" modelID = info.modelID || ""
@@ -625,7 +627,20 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
} }
} }
const contextUsageTokens = inputTokensForUsage + cacheReadTokensForUsage + modelOutputLimit const outputBudget = Math.min(modelOutputLimit, DEFAULT_MODEL_OUTPUT_LIMIT)
let contextUsageTokens = 0
if (hasContextUsage && actualUsageTokens > 0) {
contextUsageTokens = actualUsageTokens + outputBudget
if (contextWindow > 0) {
const percent = Math.round((contextUsageTokens / contextWindow) * 100)
contextUsagePercent = Math.min(100, Math.max(0, percent))
} else {
contextUsagePercent = null
}
} else {
contextUsagePercent = contextWindow > 0 ? 0 : null
}
setSessionInfoByInstance((prev) => { setSessionInfoByInstance((prev) => {
const next = new Map(prev) const next = new Map(prev)
@@ -636,6 +651,7 @@ function updateSessionInfo(instanceId: string, sessionId: string) {
contextWindow, contextWindow,
isSubscriptionModel, isSubscriptionModel,
contextUsageTokens, contextUsageTokens,
contextUsagePercent,
}) })
next.set(instanceId, instanceInfo) next.set(instanceId, instanceInfo)
return next return next
@@ -708,11 +724,8 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId) const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId) const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
const initialContextWindow = initialModel?.limit?.context ?? 0 const initialContextWindow = initialModel?.limit?.context ?? 0
const initialOutputLimit =
initialModel?.limit?.output && initialModel.limit.output > 0
? initialModel.limit.output
: DEFAULT_MODEL_OUTPUT_LIMIT
const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0 const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
const initialContextPercent = initialContextWindow > 0 ? 0 : null
setSessionInfoByInstance((prev) => { setSessionInfoByInstance((prev) => {
const next = new Map(prev) const next = new Map(prev)
@@ -722,7 +735,8 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
cost: 0, cost: 0,
contextWindow: initialContextWindow, contextWindow: initialContextWindow,
isSubscriptionModel: Boolean(initialSubscriptionModel), isSubscriptionModel: Boolean(initialSubscriptionModel),
contextUsageTokens: initialOutputLimit, contextUsageTokens: 0,
contextUsagePercent: initialContextPercent,
}) })
next.set(instanceId, instanceInfo) next.set(instanceId, instanceInfo)
return next return next
@@ -810,9 +824,8 @@ async function forkSession(
const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId) const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId) const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
const forkContextWindow = forkModel?.limit?.context ?? 0 const forkContextWindow = forkModel?.limit?.context ?? 0
const forkOutputLimit =
forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0 const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
const forkContextPercent = forkContextWindow > 0 ? 0 : null
setSessionInfoByInstance((prev) => { setSessionInfoByInstance((prev) => {
const next = new Map(prev) const next = new Map(prev)
@@ -822,7 +835,8 @@ async function forkSession(
cost: 0, cost: 0,
contextWindow: forkContextWindow, contextWindow: forkContextWindow,
isSubscriptionModel: Boolean(forkSubscriptionModel), isSubscriptionModel: Boolean(forkSubscriptionModel),
contextUsageTokens: forkOutputLimit, contextUsageTokens: 0,
contextUsagePercent: forkContextPercent,
}) })
next.set(instanceId, instanceInfo) next.set(instanceId, instanceInfo)
return next return next