Use session usage totals in TUI

This commit is contained in:
Dax Raad
2026-05-10 00:04:55 -04:00
parent 3d6e04d18b
commit 4a0c53df08
7 changed files with 49 additions and 4 deletions
@@ -337,6 +337,7 @@ export function Prompt(props: PromptProps) {
const usage = createMemo(() => {
if (!props.sessionID) return
const session = sync.session.get(props.sessionID)
const msg = sync.data.message[props.sessionID] ?? []
const last = msg.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
if (!last) return
@@ -347,7 +348,7 @@ export function Prompt(props: PromptProps) {
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)
const cost = session?.cost ?? 0
return {
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
cost: cost > 0 ? money.format(cost) : undefined,
@@ -131,6 +131,34 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
}
function partUsage(part: Part) {
if (part.type !== "step-finish") return
return { cost: part.cost, tokens: part.tokens }
}
function applySessionUsage(sessionID: string, part: Part, sign = 1) {
const usage = partUsage(part)
if (!usage) return
const result = Binary.search(store.session, sessionID, (s) => s.id)
if (!result.found) return
setStore(
"session",
result.index,
produce((draft) => {
draft.cost = (draft.cost ?? 0) + usage.cost * sign
draft.tokens = {
input: (draft.tokens?.input ?? 0) + usage.tokens.input * sign,
output: (draft.tokens?.output ?? 0) + usage.tokens.output * sign,
reasoning: (draft.tokens?.reasoning ?? 0) + usage.tokens.reasoning * sign,
cache: {
read: (draft.tokens?.cache.read ?? 0) + usage.tokens.cache.read * sign,
write: (draft.tokens?.cache.write ?? 0) + usage.tokens.cache.write * sign,
},
}
}),
)
}
event.subscribe((event) => {
switch (event.type) {
case "server.instance.disposed":
@@ -294,6 +322,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const messages = store.message[event.properties.sessionID]
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
for (const part of store.part[event.properties.messageID] ?? []) {
applySessionUsage(event.properties.sessionID, part, -1)
}
setStore(
"message",
event.properties.sessionID,
@@ -307,14 +338,18 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
case "message.part.updated": {
const parts = store.part[event.properties.part.messageID]
if (!parts) {
applySessionUsage(event.properties.part.sessionID, event.properties.part)
setStore("part", event.properties.part.messageID, [event.properties.part])
break
}
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) {
applySessionUsage(event.properties.part.sessionID, parts[result.index], -1)
applySessionUsage(event.properties.part.sessionID, event.properties.part)
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
break
}
applySessionUsage(event.properties.part.sessionID, event.properties.part)
setStore(
"part",
event.properties.part.messageID,
@@ -346,7 +381,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
case "message.part.removed": {
const parts = store.part[event.properties.messageID]
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found)
if (result.found) {
applySessionUsage(event.properties.sessionID, parts[result.index], -1)
setStore(
"part",
event.properties.messageID,
@@ -354,6 +390,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
draft.splice(result.index, 1)
}),
)
}
break
}
@@ -13,7 +13,8 @@ const money = new Intl.NumberFormat("en-US", {
function View(props: { api: TuiPluginApi; session_id: string }) {
const theme = () => props.api.theme.current
const msg = createMemo(() => props.api.state.session.messages(props.session_id))
const cost = createMemo(() => msg().reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0))
const session = createMemo(() => props.api.state.session.get(props.session_id))
const cost = createMemo(() => session()?.cost ?? 0)
const state = createMemo(() => {
const last = msg().findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
@@ -147,6 +147,9 @@ function stateApi(sync: ReturnType<typeof useSync>): TuiPluginApi["state"] {
count() {
return sync.data.session.length
},
get(sessionID) {
return sync.session.get(sessionID)
},
diff(sessionID) {
return (sync.data.session_diff[sessionID] ?? []).flatMap((item) =>
item.file === undefined ? [] : [{ ...item, file: item.file }],
@@ -42,7 +42,7 @@ export function SubagentFooter() {
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)
const cost = session()?.cost ?? 0
const money = new Intl.NumberFormat("en-US", {
style: "currency",
@@ -292,6 +292,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi {
},
session: {
count: opts.state?.session?.count ?? (() => 0),
get: opts.state?.session?.get ?? (() => undefined),
diff: opts.state?.session?.diff ?? (() => []),
todo: opts.state?.session?.todo ?? (() => []),
messages: opts.state?.session?.messages ?? (() => []),
+2
View File
@@ -11,6 +11,7 @@ import type {
Provider,
PermissionRequest,
QuestionRequest,
Session,
SessionStatus,
TextPart,
Config as SdkConfig,
@@ -310,6 +311,7 @@ export type TuiState = {
readonly vcs: { branch?: string } | undefined
session: {
count: () => number
get: (sessionID: string) => Session | undefined
diff: (sessionID: string) => ReadonlyArray<TuiSidebarFileItem>
todo: (sessionID: string) => ReadonlyArray<TuiSidebarTodoItem>
messages: (sessionID: string) => ReadonlyArray<Message>