diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index e07947ba1c4..9b03a404225 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1036,16 +1036,6 @@ type SessionRowViewProps = { } function SessionRowView(props: SessionRowViewProps) { - const config = useConfig() - const hidden = () => props.row.type === "turn-usage" && config.data.debug?.turn_tokens !== true - return ( - - - - ) -} - -function SessionRowContent(props: SessionRowViewProps) { return ( diff --git a/packages/tui/src/routes/session/rows.ts b/packages/tui/src/routes/session/rows.ts index 837725faf82..c6b0d744c91 100644 --- a/packages/tui/src/routes/session/rows.ts +++ b/packages/tui/src/routes/session/rows.ts @@ -1,6 +1,7 @@ import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client" import { createEffect, on, onCleanup, type Accessor } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" +import { useConfig } from "../../config" import { useData } from "../../context/data" import { useClient } from "../../context/client" @@ -32,14 +33,20 @@ export type SessionRow = export function createSessionRows(sessionID: Accessor) { const data = useData() const client = useClient() + const config = useConfig() const [rows, setRows] = createStore([]) const revertBoundary = () => data.session.get(sessionID())?.revert?.messageID + const turnTokens = () => config.data.debug?.turn_tokens === true function reduce() { const messages = data.session.message.list(sessionID()) const inputs = new Set(data.session.input.list(sessionID())) 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()) const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID)) rows.splice( @@ -129,23 +136,7 @@ export function createSessionRows(sessionID: Accessor) { ) createEffect( - on( - () => - 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())), - ), + on(turnTokens, () => setRows(reconcile(reduce()))), ) const appendMessage = (messageID: string) => @@ -267,9 +258,12 @@ export function createSessionRows(sessionID: Accessor) { data.on("session.step.ended", (event) => { if (event.data.sessionID !== sessionID() || ["tool-calls", "unknown"].includes(event.data.finish)) return appendFooter(event.data.assistantMessageID) + if (turnTokens()) setRows(reconcile(reduce())) }), 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())) @@ -277,14 +271,17 @@ export function createSessionRows(sessionID: Accessor) { return rows } -export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new Set()) { +export function reduceSessionRows( + messages: SessionMessageInfo[], + inputs = new Set(), + turnTokens = false, +) { const isInput = (message: SessionMessageInfo) => inputs.has(message.id) const pendingCompactions = messages.filter((message) => message.type === "compaction" && message.status === "running") const pending = new Set([...pendingCompactions.map((message) => message.id), ...inputs]) - const steps: string[] = [] - let previousCacheRead: number | undefined - let turnPreviousCacheRead: number | undefined - let measured = false + const usage = turnTokens + ? { steps: [] as SessionMessageAssistant[], previousTurnCacheRead: undefined as number | undefined } + : undefined return [ ...messages.filter((message) => !pending.has(message.id)), ...pendingCompactions, @@ -296,12 +293,7 @@ export function reduceSessionRows(messages: SessionMessageInfo[], inputs = new S rows.push({ type: "message", messageID: message.id }) return rows } - if (steps.length === 0) turnPreviousCacheRead = previousCacheRead - steps.push(message.id) - if (message.tokens && tokenTotal(message.tokens) > 0) { - previousCacheRead = message.tokens.cache.read - measured = true - } + usage?.steps.push(message) const ordinals = { text: 0, reasoning: 0 } message.content.forEach((part) => { 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) rows.push({ type: "assistant-footer", messageID: message.id }) } - if (terminal) { - if (measured) + if (terminal && usage) { + const stepsWithUsage = usage.steps.filter(hasTokenUsage) + const last = stepsWithUsage.at(-1) + if (last) { rows.push({ type: "turn-usage", - messageIDs: [...steps], - ...(turnPreviousCacheRead === undefined ? {} : { previousCacheRead: turnPreviousCacheRead }), + messageIDs: stepsWithUsage.map((step) => step.id), + ...(usage.previousTurnCacheRead === undefined + ? {} + : { previousCacheRead: usage.previousTurnCacheRead }), }) - steps.length = 0 - turnPreviousCacheRead = undefined - measured = false + usage.previousTurnCacheRead = last.tokens.cache.read + } + usage.steps.length = 0 } return rows }, []) } +function hasTokenUsage( + message: SessionMessageAssistant, +): message is SessionMessageAssistant & { tokens: NonNullable } { + return message.tokens !== undefined && tokenTotal(message.tokens) > 0 +} + function tokenTotal(tokens: NonNullable) { return tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 88b2175f261..e7e6146f4b5 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -5,12 +5,14 @@ import type { OpenCodeEvent } from "@opencode-ai/client" import { SessionMessage } from "@opencode-ai/core/session/message" import { EventV2 } from "@opencode-ai/core/event" import { createEffect, onMount, type ParentProps } from "solid-js" +import { ConfigProvider } from "../../../src/config" import { ClientProvider, useClient } from "../../../src/context/client" import { DataProvider as DataProviderBase, useData } from "../../../src/context/data" import { LocationProvider, useLocation } from "../../../src/context/location" import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows" import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client" import { TestTuiContexts } from "../../fixture/tui-environment" +import { createTuiResolvedConfig } from "../../fixture/tui-runtime" const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [ { @@ -32,14 +34,18 @@ function emitEvent(events: ReturnType, event: OpenCode events.emit({ ...event, location: { directory } }) } +const config = createTuiResolvedConfig() + function DataProvider(props: ParentProps) { return ( - - - - {props.children} - - + + + + + {props.children} + + + ) }