refactor(tui): simplify turn usage reduction (#38514)

This commit is contained in:
James Long 2026-07-23 11:22:00 -04:00 committed by GitHub
commit 833dd2ed7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 53 deletions

View file

@ -1036,16 +1036,6 @@ type SessionRowViewProps = {
} }
function SessionRowView(props: SessionRowViewProps) { function SessionRowView(props: SessionRowViewProps) {
const config = useConfig()
const hidden = () => props.row.type === "turn-usage" && config.data.debug?.turn_tokens !== true
return (
<Show when={!hidden()}>
<SessionRowContent row={props.row} message={props.message} boundaryID={props.boundaryID} />
</Show>
)
}
function SessionRowContent(props: SessionRowViewProps) {
return ( return (
<box id={props.boundaryID} marginTop={1} flexShrink={0}> <box id={props.boundaryID} marginTop={1} flexShrink={0}>
<Switch> <Switch>

View file

@ -1,6 +1,7 @@
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { createEffect, on, onCleanup, type Accessor } from "solid-js" import { createEffect, on, onCleanup, type Accessor } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store" import { createStore, produce, reconcile } from "solid-js/store"
import { useConfig } from "../../config"
import { useData } from "../../context/data" import { useData } from "../../context/data"
import { useClient } from "../../context/client" import { useClient } from "../../context/client"
@ -32,14 +33,20 @@ export type SessionRow =
export function createSessionRows(sessionID: Accessor<string>) { export function createSessionRows(sessionID: Accessor<string>) {
const data = useData() const data = useData()
const client = useClient() const client = useClient()
const config = useConfig()
const [rows, setRows] = createStore<SessionRow[]>([]) const [rows, setRows] = createStore<SessionRow[]>([])
const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID
const turnTokens = () => config.data.debug?.turn_tokens === true
function reduce() { function reduce() {
const messages = data.session.message.list(sessionID()) const messages = data.session.message.list(sessionID())
const inputs = new Set(data.session.input.list(sessionID())) const inputs = new Set(data.session.input.list(sessionID()))
const boundary = revertBoundary() const boundary = revertBoundary()
const rows = reduceSessionRows(boundary ? messages.filter((message) => message.id < boundary) : messages, inputs) const rows = reduceSessionRows(
boundary ? messages.filter((message) => message.id < boundary) : messages,
inputs,
turnTokens(),
)
partitionPending(rows, pendingPermissions()) partitionPending(rows, pendingPermissions())
const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID)) const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID))
rows.splice( rows.splice(
@ -129,23 +136,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
) )
createEffect( createEffect(
on( on(turnTokens, () => setRows(reconcile(reduce()))),
() =>
data.session.message.list(sessionID()).flatMap((message) =>
message.type === "assistant"
? [
{
id: message.id,
finish: message.finish,
error: message.error,
retry: message.retry,
tokens: message.tokens,
},
]
: [],
),
() => setRows(reconcile(reduce())),
),
) )
const appendMessage = (messageID: string) => const appendMessage = (messageID: string) =>
@ -267,9 +258,12 @@ export function createSessionRows(sessionID: Accessor<string>) {
data.on("session.step.ended", (event) => { data.on("session.step.ended", (event) => {
if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return
appendFooter(event.data.assistantMessageID) appendFooter(event.data.assistantMessageID)
if (turnTokens()) setRows(reconcile(reduce()))
}), }),
data.on("session.step.failed", (event) => { data.on("session.step.failed", (event) => {
if (event.data.sessionID === sessionID()) appendFooter(event.data.assistantMessageID) if (event.data.sessionID !== sessionID()) return
appendFooter(event.data.assistantMessageID)
if (turnTokens()) setRows(reconcile(reduce()))
}), }),
] ]
onCleanup(() => subscriptions.forEach((unsubscribe) => unsubscribe())) onCleanup(() => subscriptions.forEach((unsubscribe) => unsubscribe()))
@ -277,14 +271,17 @@ export function createSessionRows(sessionID: Accessor<string>) {
return rows return rows
} }
export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set<string>()) { export function reduceSessionRows(
messages: SessionMessageInfo[],
inputs = new Set<string>(),
turnTokens = false,
) {
const isInput = (message: SessionMessageInfo) => inputs.has(message.id) const isInput = (message: SessionMessageInfo) => inputs.has(message.id)
const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running")
const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs])
const steps: string[] = [] const usage = turnTokens
let previousCacheRead: number | undefined ? { steps: [] as SessionMessageAssistant[], previousTurnCacheRead: undefined as number | undefined }
let turnPreviousCacheRead: number | undefined : undefined
let measured = false
return [ return [
...messages.filter((message) => !pending.has(message.id)), ...messages.filter((message) => !pending.has(message.id)),
...pendingCompactions, ...pendingCompactions,
@ -296,12 +293,7 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
rows.push({ type: "message", messageID: message.id }) rows.push({ type: "message", messageID: message.id })
return rows return rows
} }
if (steps.length === 0) turnPreviousCacheRead = previousCacheRead usage?.steps.push(message)
steps.push(message.id)
if (message.tokens && tokenTotal(message.tokens) > 0) {
previousCacheRead = message.tokens.cache.read
measured = true
}
const ordinals = { text: 0, reasoning: 0 } const ordinals = { text: 0, reasoning: 0 }
message.content.forEach((part) => { message.content.forEach((part) => {
const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}` const partID = part.type === "tool" ? part.id : `${part.type}:${ordinals[part.type]++}`
@ -313,21 +305,31 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S
completePrevious(rows) completePrevious(rows)
rows.push({ type: "assistant-footer", messageID: message.id }) rows.push({ type: "assistant-footer", messageID: message.id })
} }
if (terminal) { if (terminal && usage) {
if (measured) const stepsWithUsage = usage.steps.filter(hasTokenUsage)
const last = stepsWithUsage.at(-1)
if (last) {
rows.push({ rows.push({
type: "turn-usage", type: "turn-usage",
messageIDs: [...steps], messageIDs: stepsWithUsage.map((step) => step.id),
...(turnPreviousCacheRead === undefined ? {} : { previousCacheRead: turnPreviousCacheRead }), ...(usage.previousTurnCacheRead === undefined
? {}
: { previousCacheRead: usage.previousTurnCacheRead }),
}) })
steps.length = 0 usage.previousTurnCacheRead = last.tokens.cache.read
turnPreviousCacheRead = undefined }
measured = false usage.steps.length = 0
} }
return rows return rows
}, []) }, [])
} }
function hasTokenUsage(
message: SessionMessageAssistant,
): message is SessionMessageAssistant & { tokens: NonNullable<SessionMessageAssistant["tokens"]> } {
return message.tokens !== undefined && tokenTotal(message.tokens) > 0
}
function tokenTotal(tokens: NonNullable<SessionMessageAssistant["tokens"]>) { function tokenTotal(tokens: NonNullable<SessionMessageAssistant["tokens"]>) {
return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write
} }

View file

@ -5,12 +5,14 @@ import type { OpenCodeEvent } from "@opencode-ai/client"
import { SessionMessage } from "@opencode-ai/core/session/message" import { SessionMessage } from "@opencode-ai/core/session/message"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { createEffect, onMount, type ParentProps } from "solid-js" import { createEffect, onMount, type ParentProps } from "solid-js"
import { ConfigProvider } from "../../../src/config"
import { ClientProvider, useClient } from "../../../src/context/client" import { ClientProvider, useClient } from "../../../src/context/client"
import { DataProvider as DataProviderBase, useData } from "../../../src/context/data" import { DataProvider as DataProviderBase, useData } from "../../../src/context/data"
import { LocationProvider, useLocation } from "../../../src/context/location" import { LocationProvider, useLocation } from "../../../src/context/location"
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows" import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client" import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client"
import { TestTuiContexts } from "../../fixture/tui-environment" import { TestTuiContexts } from "../../fixture/tui-environment"
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [ const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [
{ {
@ -32,14 +34,18 @@ function emitEvent(events: ReturnType<typeof createEventStream>, event: OpenCode
events.emit({ ...event, location: { directory } }) events.emit({ ...event, location: { directory } })
} }
const config = createTuiResolvedConfig()
function DataProvider(props: ParentProps) { function DataProvider(props: ParentProps) {
return ( return (
<DataProviderBase> <ConfigProvider config={config}>
<LocationProvider> <DataProviderBase>
<SyncLocation /> <LocationProvider>
{props.children} <SyncLocation />
</LocationProvider> {props.children}
</DataProviderBase> </LocationProvider>
</DataProviderBase>
</ConfigProvider>
) )
} }