fix(tui): total root session family cost

This commit is contained in:
Dax Raad 2026-07-10 19:50:50 -04:00
parent 6b3c4f5839
commit 0eaa75ec0f
5 changed files with 35 additions and 7 deletions

View file

@ -290,7 +290,7 @@ export function Prompt(props: PromptProps) {
.list(session.location)
?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
const cost = session.cost
const cost = data.session.cost(props.sessionID)
return {
context: pct ? `${Locale.number(tokens)} (${pct})` : Locale.number(tokens),
cost: cost > 0 ? money.format(cost) : undefined,

View file

@ -805,6 +805,15 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
family(sessionID: string) {
return store.session.family[resolveRoot(sessionID)] ?? []
},
cost(sessionID: string) {
const session = store.session.info[sessionID]
if (!session) return 0
if (session.parentID) return session.cost
return (store.session.family[sessionID] ?? [sessionID]).reduce(
(total, id) => total + (store.session.info[id]?.cost ?? 0),
0,
)
},
status(sessionID: string) {
return store.session.status[sessionID] ?? "idle"
},

View file

@ -16,7 +16,7 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
const theme = () => props.api.theme.current
const msg = createMemo(() => data.session.message.list(props.session_id))
const session = createMemo(() => data.session.get(props.session_id))
const cost = createMemo(() => session()?.cost ?? 0)
const cost = createMemo(() => data.session.cost(props.session_id))
const state = createMemo(() => {
const last = lastAssistantWithUsage(msg(), session()?.revert?.messageID)

View file

@ -33,7 +33,7 @@ export function SubagentFooter() {
.model.list(current.location)
?.find((model) => model.providerID === last.model.providerID && model.id === last.model.id)
const pct = model?.limit.context ? `${Math.round((tokens / model.limit.context) * 100)}%` : undefined
const cost = current.cost
const cost = data.session.cost(route.sessionID)
const money = new Intl.NumberFormat("en-US", {
style: "currency",

View file

@ -2148,12 +2148,12 @@ test("projects live instruction updates with their message ID", async () => {
}
})
function sessionInfo(id: string, parentID: string | undefined) {
function sessionInfo(id: string, parentID: string | undefined, cost = 0) {
return {
id,
parentID,
projectID: "proj_test",
cost: 0,
cost,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
title: id,
@ -2164,10 +2164,11 @@ function sessionInfo(id: string, parentID: string | undefined) {
// Mounts a DataProvider whose `/api/session/:id` responses are driven by the
// given parent map (sessionID -> parentID). Roots omit the entry. Reused across
// the family-index tests below.
async function mountData(parents: Record<string, string>) {
async function mountData(parents: Record<string, string>, costs: Record<string, number> = {}) {
const calls = createFetch((url) => {
const match = url.pathname.match(/^\/api\/session\/([^/]+)$/)
if (match && match[1] !== "active") return json({ data: sessionInfo(match[1], parents[match[1]]) })
if (match && match[1] !== "active")
return json({ data: sessionInfo(match[1], parents[match[1]], costs[match[1]]) })
})
let data!: ReturnType<typeof useData>
let ready!: () => void
@ -2235,6 +2236,24 @@ test("indexes arbitrarily deep nesting under a single root", async () => {
}
})
test("totals family cost for roots and keeps subagent cost scoped", async () => {
const { data, app } = await mountData(
{ grandchild: "child", child: "root" },
{ root: 1, child: 2, grandchild: 3 },
)
try {
await data.session.refresh("grandchild")
await data.session.refresh("child")
await data.session.refresh("root")
expect(data.session.cost("root")).toBe(6)
expect(data.session.cost("child")).toBe(2)
expect(data.session.cost("grandchild")).toBe(3)
} finally {
app.renderer.destroy()
}
})
test("re-registering an existing session is idempotent", async () => {
const { data, app } = await mountData({ grandchild: "child", child: "root" })
try {