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/util/encode" import { findLast } from "@opencode-ai/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/ui/file" import { Markdown } from "@opencode-ai/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 { useSessionLayout } from "@/pages/session/session-layout" import { getSessionContextMetrics } from "./session-context-metrics" import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown" import { createSessionContextFormatter } from "./session-context-format" const BREAKDOWN_COLOR: Record = { 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 (
{props.label}
{props.value}
) } 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 ( requestAnimationFrame(props.onRendered)} /> ) } function RawMessage(props: { message: Message getParts: (id: string) => Part[] onRendered: () => void time: (value: number | undefined) => string }) { return (
{props.message.role} • {props.message.id}
{props.time(props.message.time.created)}
) } const emptyMessages: Message[] = [] const emptyUserMessages: UserMessage[] = [] export function SessionContextTab() { const sync = useSync() const language = useLanguage() 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 metrics = createMemo(() => getSessionContextMetrics(messages(), sync.data.provider.all)) const ctx = createMemo(() => metrics().context) const formatter = createMemo(() => createSessionContextFormatter(language.intl())) const cost = createMemo(() => { return usd().format(metrics().totalCost) }) 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, 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()?.output) }, { label: "context.stats.reasoningTokens", value: () => formatter().number(ctx()?.reasoning) }, { label: "context.stats.cacheTokens", value: () => `${formatter().number(ctx()?.cacheRead)} / ${formatter().number(ctx()?.cacheWrite)}`, }, { 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 ( { scroll = el restoreScroll() }} onScroll={handleScroll} >
{(stat) => [0])} value={stat.value()} />}
0}>
{language.t("context.breakdown.title")}
{(segment) => (
)}
{(segment) => (
{breakdownLabel(segment.key)}
{segment.percent.toLocaleString(language.intl())}%
)}
{(prompt) => (
{language.t("context.systemPrompt.title")}
)}
{language.t("context.rawMessages.title")}
{(message) => ( )}
) }