/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/app/src/components/session/session-context-tab.tsx
343 строки
12 KB
Luke Parker
fix(app): align context tokens with usage (#36015)
10 июл 2026, 02:12
Не верифицирован
10 июл 2026, 02:12
b94e55c
Код
Авторство
О чём код?
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" import type { JSX } from "solid-js" import { useSync } from "@/context/sync" import { checksum } from "@opencode-ai/core/util/encode" import { findLast } from "@opencode-ai/core/util/array" import { same } from "@/utils/same" import { Icon } from "@opencode-ai/ui/icon" import { Accordion } from "@opencode-ai/ui/accordion" import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { File } from "@opencode-ai/session-ui/file" import { Markdown } from "@opencode-ai/session-ui/markdown" import { ScrollView } from "@opencode-ai/ui/scroll-view" import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client" import { useLanguage } from "@/context/language" import { useProviders } from "@/hooks/use-providers" import { useSDK } from "@/context/sdk" import { useSessionLayout } from "@/pages/session/session-layout" import { getSessionContext } from "./session-context-metrics" import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown" import { createSessionContextFormatter } from "./session-context-format" const BREAKDOWN_COLOR: Record<SessionContextBreakdownKey, string> = { system: "var(--syntax-info)", user: "var(--syntax-success)", assistant: "var(--syntax-property)", tool: "var(--syntax-warning)", other: "var(--syntax-comment)", } function Stat(props: { label: string; value: JSX.Element }) { return ( <div class="flex flex-col gap-1"> <div class="text-12-regular text-text-weak">{props.label}</div> <div class="text-12-medium text-text-strong">{props.value}</div> </div> ) } function RawMessageContent(props: { message: Message; getParts: (id: string) => Part[]; onRendered: () => void }) { const file = createMemo(() => { const parts = props.getParts(props.message.id) const contents = JSON.stringify({ message: props.message, parts }, null, 2) return { name: `${props.message.role}-${props.message.id}.json`, contents, cacheKey: checksum(contents), } }) return ( <File mode="text" file={file()} overflow="wrap" class="select-text" onRendered={() => requestAnimationFrame(props.onRendered)} /> ) } function RawMessage(props: { message: Message getParts: (id: string) => Part[] onRendered: () => void time: (value: number | undefined) => string }) { return ( <Accordion.Item value={props.message.id}> <StickyAccordionHeader> <Accordion.Trigger> <div class="flex items-center justify-between gap-2 w-full"> <div class="min-w-0 truncate"> {props.message.role} <span class="text-text-base">• {props.message.id}</span> </div> <div class="flex items-center gap-3"> <div class="shrink-0 text-12-regular text-text-weak">{props.time(props.message.time.created)}</div> <Icon name="chevron-grabber-vertical" size="small" class="shrink-0 text-text-weak" /> </div> </div> </Accordion.Trigger> </StickyAccordionHeader> <Accordion.Content class="bg-background-base"> <div class="p-3"> <RawMessageContent message={props.message} getParts={props.getParts} onRendered={props.onRendered} /> </div> </Accordion.Content> </Accordion.Item> ) } const emptyMessages: Message[] = [] const emptyUserMessages: UserMessage[] = [] export function SessionContextTab() { const sync = useSync() const language = useLanguage() const sdk = useSDK() const providers = useProviders(() => sdk().directory) const { params, view } = useSessionLayout() const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) const messages = createMemo( () => { const id = params.id if (!id) return emptyMessages return (sync().data.message[id] ?? []) as Message[] }, emptyMessages, { equals: same }, ) const userMessages = createMemo( () => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages, { equals: same }, ) const visibleUserMessages = createMemo( () => { const revert = info()?.revert?.messageID if (!revert) return userMessages() return userMessages().filter((m) => m.id < revert) }, emptyUserMessages, { equals: same }, ) const usd = createMemo( () => new Intl.NumberFormat(language.intl(), { style: "currency", currency: "USD", }), ) const ctx = createMemo(() => getSessionContext(messages(), [...providers.all().values()])) const formatter = createMemo(() => createSessionContextFormatter(language.intl())) const cost = createMemo(() => { return usd().format(info()?.cost ?? 0) }) const counts = createMemo(() => { const all = messages() const user = all.reduce((count, x) => count + (x.role === "user" ? 1 : 0), 0) const assistant = all.reduce((count, x) => count + (x.role === "assistant" ? 1 : 0), 0) return { all: all.length, user, assistant, } }) const systemPrompt = createMemo(() => { const msg = findLast(visibleUserMessages(), (m) => !!m.system) const system = msg?.system if (!system) return const trimmed = system.trim() if (!trimmed) return return trimmed }) const providerLabel = createMemo(() => { const c = ctx() if (!c) return "—" return c.providerLabel }) const modelLabel = createMemo(() => { const c = ctx() if (!c) return "—" return c.modelLabel }) const breakdown = createMemo( on( () => [ctx()?.message.id, ctx()?.input, messages().length, systemPrompt()], () => { const c = ctx() if (!c?.input) return [] return estimateSessionContextBreakdown({ messages: messages(), parts: sync().data.part as Record<string, Part[] | undefined>, input: c.input, systemPrompt: systemPrompt(), }) }, ), ) const breakdownLabel = (key: SessionContextBreakdownKey) => { if (key === "system") return language.t("context.breakdown.system") if (key === "user") return language.t("context.breakdown.user") if (key === "assistant") return language.t("context.breakdown.assistant") if (key === "tool") return language.t("context.breakdown.tool") return language.t("context.breakdown.other") } const stats = [ { label: "context.stats.session", value: () => info()?.title ?? params.id ?? "—" }, { label: "context.stats.messages", value: () => counts().all.toLocaleString(language.intl()) }, { label: "context.stats.provider", value: providerLabel }, { label: "context.stats.model", value: modelLabel }, { label: "context.stats.limit", value: () => formatter().number(ctx()?.limit) }, { label: "context.stats.totalTokens", value: () => formatter().number(ctx()?.total) }, { label: "context.stats.usage", value: () => formatter().percent(ctx()?.usage) }, { label: "context.stats.inputTokens", value: () => formatter().number(ctx()?.input) }, { label: "context.stats.outputTokens", value: () => formatter().number(ctx()?.message.tokens.output) }, { label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.message.tokens.reasoning) }, { label: "context.stats.cacheTokens", value: () => `${formatter().number(ctx()?.message.tokens.cache.read)} / ${formatter().number(ctx()?.message.tokens.cache.write)}`, }, { label: "context.stats.userMessages", value: () => counts().user.toLocaleString(language.intl()) }, { label: "context.stats.assistantMessages", value: () => counts().assistant.toLocaleString(language.intl()) }, { label: "context.stats.totalCost", value: cost }, { label: "context.stats.sessionCreated", value: () => formatter().time(info()?.time.created) }, { label: "context.stats.lastActivity", value: () => formatter().time(ctx()?.message.time.created) }, ] satisfies { label: string; value: () => JSX.Element }[] let scroll: HTMLDivElement | undefined let frame: number | undefined let pending: { x: number; y: number } | undefined const getParts = (id: string) => (sync().data.part[id] ?? []) as Part[] const restoreScroll = () => { const el = scroll if (!el) return const s = view().scroll("context") if (!s) return if (el.scrollTop !== s.y) el.scrollTop = s.y if (el.scrollLeft !== s.x) el.scrollLeft = s.x } const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { pending = { x: event.currentTarget.scrollLeft, y: event.currentTarget.scrollTop, } if (frame !== undefined) return frame = requestAnimationFrame(() => { frame = undefined const next = pending pending = undefined if (!next) return view().setScroll("context", next) }) } createEffect( on( () => messages().length, () => { requestAnimationFrame(restoreScroll) }, { defer: true }, ), ) onCleanup(() => { if (frame === undefined) return cancelAnimationFrame(frame) }) return ( <ScrollView class="@container h-full" viewportRef={(el) => { scroll = el restoreScroll() }} onScroll={handleScroll} > <div class="px-6 pt-4 pb-10 flex flex-col gap-10"> <div class="grid grid-cols-1 @[32rem]:grid-cols-2 gap-4"> <For each={stats}> {(stat) => <Stat label={language.t(stat.label as Parameters<typeof language.t>[0])} value={stat.value()} />} </For> </div> <Show when={breakdown().length > 0}> <div class="flex flex-col gap-2"> <div class="text-12-regular text-text-weak">{language.t("context.breakdown.title")}</div> <div class="h-2 w-full rounded-full bg-surface-base overflow-hidden flex"> <For each={breakdown()}> {(segment) => ( <div class="h-full" style={{ width: `${segment.width}%`, "background-color": BREAKDOWN_COLOR[segment.key], }} /> )} </For> </div> <div class="flex flex-wrap gap-x-3 gap-y-1"> <For each={breakdown()}> {(segment) => ( <div class="flex items-center gap-1 text-11-regular text-text-weak"> <div class="size-2 rounded-sm" style={{ "background-color": BREAKDOWN_COLOR[segment.key] }} /> <div>{breakdownLabel(segment.key)}</div> <div class="text-text-weaker">{segment.percent.toLocaleString(language.intl())}%</div> </div> )} </For> </div> <div class="hidden text-11-regular text-text-weaker">{language.t("context.breakdown.note")}</div> </div> </Show> <Show when={systemPrompt()}> {(prompt) => ( <div class="flex flex-col gap-2"> <div class="text-12-regular text-text-weak">{language.t("context.systemPrompt.title")}</div> <div class="border border-border-base rounded-md bg-surface-base px-3 py-2"> <Markdown text={prompt()} class="text-12-regular" /> </div> </div> )} </Show> <div class="flex flex-col gap-2"> <div class="text-12-regular text-text-weak">{language.t("context.rawMessages.title")}</div> <Accordion multiple> <For each={messages()}> {(message) => ( <RawMessage message={message} getParts={getParts} onRendered={restoreScroll} time={formatter().time} /> )} </For> </Accordion> </div> </div> </ScrollView> ) }