summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/session/session-context-tab.tsx
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-12 09:49:14 -0600
committerGitHub <[email protected]>2026-02-12 09:49:14 -0600
commitff4414bb152acfddb5c0eb073c38bedc1df4ae14 (patch)
tree78381c67d21ef6f089647f6b19e7aa2976840dbc /packages/app/src/components/session/session-context-tab.tsx
parent56ad2db02055955f926fda0e4a89055b22ead6f9 (diff)
downloadopencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.tar.gz
opencode-ff4414bb152acfddb5c0eb073c38bedc1df4ae14.zip
chore: refactor packages/app files (#13236)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Frank <[email protected]>
Diffstat (limited to 'packages/app/src/components/session/session-context-tab.tsx')
-rw-r--r--packages/app/src/components/session/session-context-tab.tsx288
1 files changed, 103 insertions, 185 deletions
diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx
index 8aae44863..eb5b4197d 100644
--- a/packages/app/src/components/session/session-context-tab.tsx
+++ b/packages/app/src/components/session/session-context-tab.tsx
@@ -1,7 +1,6 @@
import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js"
import type { JSX } from "solid-js"
import { useParams } from "@solidjs/router"
-import { DateTime } from "luxon"
import { useSync } from "@/context/sync"
import { useLayout } from "@/context/layout"
import { checksum } from "@opencode-ai/util/encode"
@@ -14,6 +13,8 @@ import { Markdown } from "@opencode-ai/ui/markdown"
import type { Message, Part, UserMessage } from "@opencode-ai/sdk/v2/client"
import { useLanguage } from "@/context/language"
import { getSessionContextMetrics } from "./session-context-metrics"
+import { estimateSessionContextBreakdown, type SessionContextBreakdownKey } from "./session-context-breakdown"
+import { createSessionContextFormatter } from "./session-context-format"
interface SessionContextTabProps {
messages: () => Message[]
@@ -22,6 +23,74 @@ interface SessionContextTabProps {
info: () => ReturnType<ReturnType<typeof useSync>["session"]["get"]>
}
+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 (
+ <Code
+ 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>
+ )
+}
+
export function SessionContextTab(props: SessionContextTabProps) {
const params = useParams()
const sync = useSync()
@@ -37,6 +106,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
const metrics = createMemo(() => getSessionContextMetrics(props.messages(), sync.data.provider.all))
const ctx = createMemo(() => metrics().context)
+ const formatter = createMemo(() => createSessionContextFormatter(language.locale()))
const cost = createMemo(() => {
return usd().format(metrics().totalCost)
@@ -62,23 +132,6 @@ export function SessionContextTab(props: SessionContextTabProps) {
return trimmed
})
- const number = (value: number | null | undefined) => {
- if (value === undefined) return "—"
- if (value === null) return "—"
- return value.toLocaleString(language.locale())
- }
-
- const percent = (value: number | null | undefined) => {
- if (value === undefined) return "—"
- if (value === null) return "—"
- return value.toLocaleString(language.locale()) + "%"
- }
-
- const time = (value: number | undefined) => {
- if (!value) return "—"
- return DateTime.fromMillis(value).setLocale(language.locale()).toLocaleString(DateTime.DATETIME_MED)
- }
-
const providerLabel = createMemo(() => {
const c = ctx()
if (!c) return "—"
@@ -96,122 +149,23 @@ export function SessionContextTab(props: SessionContextTabProps) {
() => [ctx()?.message.id, ctx()?.input, props.messages().length, systemPrompt()],
() => {
const c = ctx()
- if (!c) return []
- const input = c.input
- if (!input) return []
-
- const out = {
- system: systemPrompt()?.length ?? 0,
- user: 0,
- assistant: 0,
- tool: 0,
- }
-
- for (const msg of props.messages()) {
- const parts = (sync.data.part[msg.id] ?? []) as Part[]
-
- if (msg.role === "user") {
- for (const part of parts) {
- if (part.type === "text") out.user += part.text.length
- if (part.type === "file") out.user += part.source?.text.value.length ?? 0
- if (part.type === "agent") out.user += part.source?.value.length ?? 0
- }
- continue
- }
-
- if (msg.role === "assistant") {
- for (const part of parts) {
- if (part.type === "text") out.assistant += part.text.length
- if (part.type === "reasoning") out.assistant += part.text.length
- if (part.type === "tool") {
- out.tool += Object.keys(part.state.input).length * 16
- if (part.state.status === "pending") out.tool += part.state.raw.length
- if (part.state.status === "completed") out.tool += part.state.output.length
- if (part.state.status === "error") out.tool += part.state.error.length
- }
- }
- }
- }
-
- const estimateTokens = (chars: number) => Math.ceil(chars / 4)
- const system = estimateTokens(out.system)
- const user = estimateTokens(out.user)
- const assistant = estimateTokens(out.assistant)
- const tool = estimateTokens(out.tool)
- const estimated = system + user + assistant + tool
-
- const pct = (tokens: number) => (tokens / input) * 100
- const pctLabel = (tokens: number) => (Math.round(pct(tokens) * 10) / 10).toString() + "%"
-
- const build = (tokens: { system: number; user: number; assistant: number; tool: number; other: number }) => {
- return [
- {
- key: "system",
- label: language.t("context.breakdown.system"),
- tokens: tokens.system,
- width: pct(tokens.system),
- percent: pctLabel(tokens.system),
- color: "var(--syntax-info)",
- },
- {
- key: "user",
- label: language.t("context.breakdown.user"),
- tokens: tokens.user,
- width: pct(tokens.user),
- percent: pctLabel(tokens.user),
- color: "var(--syntax-success)",
- },
- {
- key: "assistant",
- label: language.t("context.breakdown.assistant"),
- tokens: tokens.assistant,
- width: pct(tokens.assistant),
- percent: pctLabel(tokens.assistant),
- color: "var(--syntax-property)",
- },
- {
- key: "tool",
- label: language.t("context.breakdown.tool"),
- tokens: tokens.tool,
- width: pct(tokens.tool),
- percent: pctLabel(tokens.tool),
- color: "var(--syntax-warning)",
- },
- {
- key: "other",
- label: language.t("context.breakdown.other"),
- tokens: tokens.other,
- width: pct(tokens.other),
- percent: pctLabel(tokens.other),
- color: "var(--syntax-comment)",
- },
- ].filter((x) => x.tokens > 0)
- }
-
- if (estimated <= input) {
- return build({ system, user, assistant, tool, other: input - estimated })
- }
-
- const scale = input / estimated
- const scaled = {
- system: Math.floor(system * scale),
- user: Math.floor(user * scale),
- assistant: Math.floor(assistant * scale),
- tool: Math.floor(tool * scale),
- }
- const scaledTotal = scaled.system + scaled.user + scaled.assistant + scaled.tool
- return build({ ...scaled, other: Math.max(0, input - scaledTotal) })
+ if (!c?.input) return []
+ return estimateSessionContextBreakdown({
+ messages: props.messages(),
+ parts: sync.data.part as Record<string, Part[] | undefined>,
+ input: c.input,
+ systemPrompt: systemPrompt(),
+ })
},
),
)
- function Stat(statProps: { label: string; value: JSX.Element }) {
- return (
- <div class="flex flex-col gap-1">
- <div class="text-12-regular text-text-weak">{statProps.label}</div>
- <div class="text-12-medium text-text-strong">{statProps.value}</div>
- </div>
- )
+ 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 = createMemo(() => {
@@ -222,15 +176,15 @@ export function SessionContextTab(props: SessionContextTabProps) {
{ label: language.t("context.stats.messages"), value: count.all.toLocaleString(language.locale()) },
{ label: language.t("context.stats.provider"), value: providerLabel() },
{ label: language.t("context.stats.model"), value: modelLabel() },
- { label: language.t("context.stats.limit"), value: number(c?.limit) },
- { label: language.t("context.stats.totalTokens"), value: number(c?.total) },
- { label: language.t("context.stats.usage"), value: percent(c?.usage) },
- { label: language.t("context.stats.inputTokens"), value: number(c?.input) },
- { label: language.t("context.stats.outputTokens"), value: number(c?.output) },
- { label: language.t("context.stats.reasoningTokens"), value: number(c?.reasoning) },
+ { label: language.t("context.stats.limit"), value: formatter().number(c?.limit) },
+ { label: language.t("context.stats.totalTokens"), value: formatter().number(c?.total) },
+ { label: language.t("context.stats.usage"), value: formatter().percent(c?.usage) },
+ { label: language.t("context.stats.inputTokens"), value: formatter().number(c?.input) },
+ { label: language.t("context.stats.outputTokens"), value: formatter().number(c?.output) },
+ { label: language.t("context.stats.reasoningTokens"), value: formatter().number(c?.reasoning) },
{
label: language.t("context.stats.cacheTokens"),
- value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
+ value: `${formatter().number(c?.cacheRead)} / ${formatter().number(c?.cacheWrite)}`,
},
{ label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
{
@@ -238,55 +192,15 @@ export function SessionContextTab(props: SessionContextTabProps) {
value: count.assistant.toLocaleString(language.locale()),
},
{ label: language.t("context.stats.totalCost"), value: cost() },
- { label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
- { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },
+ { label: language.t("context.stats.sessionCreated"), value: formatter().time(props.info()?.time.created) },
+ { label: language.t("context.stats.lastActivity"), value: formatter().time(c?.message.time.created) },
] satisfies { label: string; value: JSX.Element }[]
})
- function RawMessageContent(msgProps: { message: Message }) {
- const file = createMemo(() => {
- const parts = (sync.data.part[msgProps.message.id] ?? []) as Part[]
- const contents = JSON.stringify({ message: msgProps.message, parts }, null, 2)
- return {
- name: `${msgProps.message.role}-${msgProps.message.id}.json`,
- contents,
- cacheKey: checksum(contents),
- }
- })
-
- return (
- <Code file={file()} overflow="wrap" class="select-text" onRendered={() => requestAnimationFrame(restoreScroll)} />
- )
- }
-
- function RawMessage(msgProps: { message: Message }) {
- return (
- <Accordion.Item value={msgProps.message.id}>
- <StickyAccordionHeader>
- <Accordion.Trigger>
- <div class="flex items-center justify-between gap-2 w-full">
- <div class="min-w-0 truncate">
- {msgProps.message.role} <span class="text-text-base">• {msgProps.message.id}</span>
- </div>
- <div class="flex items-center gap-3">
- <div class="shrink-0 text-12-regular text-text-weak">{time(msgProps.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={msgProps.message} />
- </div>
- </Accordion.Content>
- </Accordion.Item>
- )
- }
-
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
@@ -356,7 +270,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
class="h-full"
style={{
width: `${segment.width}%`,
- "background-color": segment.color,
+ "background-color": BREAKDOWN_COLOR[segment.key],
}}
/>
)}
@@ -366,9 +280,9 @@ export function SessionContextTab(props: SessionContextTabProps) {
<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": segment.color }} />
- <div>{segment.label}</div>
- <div class="text-text-weaker">{segment.percent}</div>
+ <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.locale())}%</div>
</div>
)}
</For>
@@ -391,7 +305,11 @@ export function SessionContextTab(props: SessionContextTabProps) {
<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={props.messages()}>{(message) => <RawMessage message={message} />}</For>
+ <For each={props.messages()}>
+ {(message) => (
+ <RawMessage message={message} getParts={getParts} onRendered={restoreScroll} time={formatter().time} />
+ )}
+ </For>
</Accordion>
</div>
</div>