diff --git a/README.md b/README.md
index 9f4d0bc..8bbcd6e 100644
--- a/README.md
+++ b/README.md
@@ -73,8 +73,8 @@ Open any session and click the **Context / 上下文** tab:
| Card | The question it answers |
| --- | --- |
-| **Context Stats** | Turns, steps, human inputs, live tool calls, the session's cache-hit rate — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; DeepSeek peak/off-peak aware). |
-| **Token Stats** | Where the billed tokens went — the same total as the chat stats line under the composer, split by composition (system, tools, messages…) with the provider-exact output closing the ring. |
+| **Context Stats** | Turns, steps, human inputs, live tool calls — plus a cost estimated from the models.dev list prices (hover the `?` for per-1M rates; DeepSeek peak/off-peak aware). |
+| **Token Stats** | Where the billed tokens went — the same total as the chat stats line under the composer, split by composition (system, tools, messages…) with the provider-exact output closing the ring, and the session's cache-hit rate in the corner. |
| **Timing Stats** | How active time split across model calls, tool runs, and overhead. |
| **Current Context** | What's in the window *right now*. |
| **Context Trend** | Every request's size — and its story. |
diff --git a/docs/context-stats.png b/docs/context-stats.png
index 60c4186..1e91c99 100644
Binary files a/docs/context-stats.png and b/docs/context-stats.png differ
diff --git a/src/client/components/contextView.tsx b/src/client/components/contextView.tsx
index bc6118b..7398ad5 100644
--- a/src/client/components/contextView.tsx
+++ b/src/client/components/contextView.tsx
@@ -94,7 +94,7 @@ export function makeContextView(
// no longer mirrors it. Absent → derived fallback.
const pressure = projectionOf(props, 'contextPressure', contextPressureOf)
// Official token-meter `tokenUsage` projection — the same data the chat stats line below the input box reads for its '缓存命中' figure, so
- // the stats board's cache-hit cell reuses it verbatim; absent → the cell drops to a dash instead of estimating.
+ // the Token card's figures reuse it verbatim; absent → the card drops them instead of estimating.
const usage = projectionOf(props, 'tokenUsage', tokenUsageOf)
// Official token-meter `contextBreakdown` projection — the exact rows the chat ring's click-open panel shows, so the overview legend
// reads identically by construction; absent → the fold's own same-estimator sums inside headlineOf.
@@ -591,7 +591,7 @@ export function makeContextView(
pair in a narrow pane at the cards' min-width floors. */}
{inSidebar ? null : (
diff --git a/src/client/components/statsContext.tsx b/src/client/components/statsContext.tsx
index 03eaf14..223ed96 100644
--- a/src/client/components/statsContext.tsx
+++ b/src/client/components/statsContext.tsx
@@ -1,16 +1,15 @@
/**
* The Context card: what the session's context IS and how it evolved — a
- * seven-cell grid of the session's shape (turns / steps / human inputs /
- * live tool calls), the whole-session cache-hit rate, and the cost estimate
- * at two scopes: the family total (the current agent plus every subagent
- * session) and the subagents' own share.
+ * six-cell grid of the session's shape (turns / steps / human inputs /
+ * live tool calls) and the cost estimate at two scopes: the family total
+ * (the current agent plus every subagent session) and the subagents' own
+ * share.
* Count figures only: nothing here is part of a spendable whole, so no pie —
- * proportions live in the composition card, and the context-event tallies
- * live on the events card's kind filters (contextView.tsx). The cache-hit
- * cell reads the official `tokenUsage` projection — the same source and
- * formula as the harness chat stats line under the composer, shown with one
- * decimal — and dashes until a provider reports usage. The cost cells price
- * the host-folded cumulative billed totals (complete session logs, never
+ * proportions live in the composition card, the context-event tallies live
+ * on the events card's kind filters (contextView.tsx), and the token figures
+ * (the session's cache-hit share among them) live on the Token card. The
+ * cost cells price the host-folded cumulative billed totals (complete
+ * session logs, never
* trimmed; the subagents' usage folds out of the session-list snapshot,
* `makeSubagentCost` below) from the models.dev price book (modelPrices.ts)
* in the locale's currency; their hover bubbles (a '?' marker + styled DOM
@@ -26,15 +25,14 @@
*/
import { useEffect, useMemo, useState, type ReactElement, type ReactNode } from 'react'
-import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TimelineCounts, TokenUsage } from '../../shared/types'
+import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TimelineCounts } from '../../shared/types'
import { estimateSessionCost, formatCost, formatPriceRate, mergeCostUsage, priceFaceOf, toCurrency } from '../cost'
import type { CostCurrency, ModelBook, PriceFace } from '../cost'
import { sessionsFaceOf, subagentCostFoldOf } from '../agentTree'
import type { AgentHeads } from '../agentHeads'
import { useSessionsSnapshot } from '../agentHeads'
-import { cacheHitPercent } from '../format'
import { useModelPrices } from '../modelPrices'
-import { asRecord, numOf, type ClientCtx } from '../services'
+import { asRecord, type ClientCtx } from '../services'
import { isDeepSeekProvider } from '../../shared/providers'
import type { ViewKit } from '../viewkit'
@@ -139,8 +137,6 @@ export function makeStatsContext(
humanInputs?: number
/** Tool calls with a result live in the current context (absent on older hosts). */
toolCalls?: number
- /** The official tokenUsage projection — the cache-hit cell's source (null until a provider reports). */
- usage: TokenUsage | null
cost?: SessionCostUsage
locale: string
/** The current session id, anchoring the subagent-cost fold (absent = nothing to fold). */
@@ -151,7 +147,6 @@ export function makeStatsContext(
counts: TimelineCounts
humanInputs?: number
toolCalls?: number
- usage: TokenUsage | null
cost?: SessionCostUsage
locale: string
sessionId?: string
@@ -223,14 +218,6 @@ export function makeStatsContext(
...notes(subDeepseek),
subUnpriced ? {t('stats.costUnavailable')} : null,
]
- // The harness chat stats line's own formula, shown two decimals deep:
- // prompt-side cache reads over the whole billed input (output excluded),
- // dashed until reported.
- const hit = props.usage === null ? null
- : cacheHitPercent(
- numOf(props.usage.cacheReadTokens),
- numOf(props.usage.uncachedInputTokens) + numOf(props.usage.cacheReadTokens) + numOf(props.usage.cacheWriteTokens),
- )
const cell = (label: string, value: string | number, tip?: ReactNode, href?: string): ReactElement => {
// The framed cell body, as a div — or as an anchor opening the models.dev
// provider listing in a new tab when the caller hands a destination (the
@@ -269,7 +256,6 @@ export function makeStatsContext(
{cell(t('stats.steps'), props.counts.steps)}
{cell(t('stats.humanInputs'), props.humanInputs ?? 0, t('stats.humanInputsTip'))}
{cell(t('stats.toolCalls'), props.toolCalls ?? 0)}
- {cell(t('stats.cacheHit'), hit === null ? '—' : `${hit}%`, t('stats.cacheHitTip'))}
{cell(t('stats.cost'), cost === null ? '—' : formatCost(cost, currency), costTip, costHref)}
{cell(t('stats.subCost'), subCost === null ? '—' : formatCost(subCost, currency), subTip)}
diff --git a/src/client/format.ts b/src/client/format.ts
index f0846c7..cfa5fa1 100644
--- a/src/client/format.ts
+++ b/src/client/format.ts
@@ -20,17 +20,15 @@ export function fmtBytes(n: number | null | undefined): string {
/**
* Cache-hit share of billed prompt-side input (`reads` over `billed`),
- * TRUNCATED to `decimals` places (cut, not round) — same formula as the
- * harness chat stats line's '缓存命中' figure and the stats board's cell
- * (which shows two decimals). Null when nothing was billed. The 1e-9 epsilon
- * absorbs only float noise (integer token counts never sit that close to a
- * boundary).
+ * TRUNCATED to two decimals (cut, not round) — same formula as the harness
+ * chat stats line's '缓存命中' figure and the token card's corner. Null when
+ * nothing was billed. The 1e-9 epsilon absorbs only float noise (integer
+ * token counts never sit that close to a boundary).
*/
-export function cacheHitPercent(reads: number, billed: number, decimals = 2): string | null {
+export function cacheHitPercent(reads: number, billed: number): string | null {
if (!(billed > 0)) return null
- const factor = 10 ** decimals
- const scaled = Math.trunc((reads / billed) * 100 * factor + 1e-9)
- return `${Math.floor(scaled / factor)}.${String(scaled % factor).padStart(decimals, '0')}`
+ const scaled = Math.trunc((reads / billed) * 100 * 100 + 1e-9)
+ return `${Math.floor(scaled / 100)}.${String(scaled % 100).padStart(2, '0')}`
}
/** One shared formatter: building an Intl formatter per call costs ~50x the format itself (issue #116). */
diff --git a/src/client/services.ts b/src/client/services.ts
index 7916c92..30e5336 100644
--- a/src/client/services.ts
+++ b/src/client/services.ts
@@ -545,10 +545,10 @@ export function contextBreakdownOf(value: unknown): ContextBreakdown | null {
* Narrow a delivered projection value to the official token-meter
* `tokenUsage` projection (durable cumulative provider usage). Absent key or
* value = the meter's projection is not composed (or no request has reported
- * usage yet) — callers drop the cache-hit cell to a dash. The wire schema is
- * strict with all four buckets REQUIRED (dsh token-meter's projectionSchema),
- * so a partial/corrupt value degrades the whole value to null instead of
- * undercounting the billed total.
+ * usage yet) — callers drop the affected figures instead of estimating. The
+ * wire schema is strict with all four buckets REQUIRED (dsh token-meter's
+ * projectionSchema), so a partial/corrupt value degrades the whole value to
+ * null instead of undercounting the billed total.
*/
export function tokenUsageOf(value: unknown): TokenUsage | null {
const data = asRecord(value)
diff --git a/src/client/styles/stats.css b/src/client/styles/stats.css
index 754a4e4..cef0389 100644
--- a/src/client/styles/stats.css
+++ b/src/client/styles/stats.css
@@ -10,7 +10,7 @@
card it visually is, with the pointer as the only affordance. */
a.lc-stat { color: inherit; text-decoration: none; cursor: pointer; }
a.lc-stat:hover { border-color: var(--dsw-alias-label-secondary); }
-/* The 5-cell stats grid shares a half-width head-row card with a sibling, so its cells are moderate: tighter padding plus a corner-pinned '?' (out of the label's line) keep the longest labels on one line. */
+/* The stats grid shares a half-width head-row card with a sibling, so its cells are moderate: tighter padding plus a corner-pinned '?' (out of the label's line) keep the longest labels on one line. */
.lc-stats .lc-stat { padding: 6px 3px; }
.lc-stats .lc-stat-q { position: absolute; top: 3px; right: 4px; margin: 0; }
.lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
diff --git a/tests/client/components/contextView.spec.ts b/tests/client/components/contextView.spec.ts
index 9715d84..d603a75 100644
--- a/tests/client/components/contextView.spec.ts
+++ b/tests/client/components/contextView.spec.ts
@@ -195,11 +195,11 @@ describe('ContextView — projection guards', () => {
assert.ok(text(m.container).includes(DICT_EN['overview.used']))
// The Token card's center is the chat stats line's whole-session billed
// total off the official tokenUsage projection (100 + 200 + 0 + 50), and
- // the Context card's cache-hit cell shows the line's own rate two decimals
- // deep (200 / 300, truncated).
+ // its corner shows the line's own cache-hit rate two decimals deep
+ // (200 / 300, truncated).
const tokensCard = queryAll(m.container, '.lc-head > .lc-col-donut')[0]
assert.equal(query(tokensCard, '.lc-donut-center b').textContent, '350')
- assert.ok(text(m.container).includes('66.66%'))
+ assert.equal(query(tokensCard, '.lc-card-rate').textContent, 'Cache Hit 66.66%')
assert.ok(text(m.container).includes('m-only'))
await m.unmount()
})
diff --git a/tests/client/components/statsContext.spec.ts b/tests/client/components/statsContext.spec.ts
index 4333b7f..118fd7b 100644
--- a/tests/client/components/statsContext.spec.ts
+++ b/tests/client/components/statsContext.spec.ts
@@ -1,9 +1,8 @@
// StatsContext (src/client/components/statsContext.tsx) rendered with real
-// React: the seven-cell grid — session shape with the whole-session
-// human-input tally, the chat-line cache-hit cell with its whole-session
-// share tip, the family-scope priced cost cell (current agent + subagents)
-// with its per-model rate tooltip, and the subagents' own share — in both
-// locales, against an injected model-price
+// React: the six-cell grid — session shape with the whole-session
+// human-input tally, the family-scope priced cost cell (current agent +
+// subagents) with its per-model rate tooltip, and the subagents' own share —
+// in both locales, against an injected model-price
// book (the store never reaches the network). The context-event tallies live
// on the events card's kind filters (contextView.spec.ts); `countsOfRecords`
// still derives every count the split generation's wire head carries, pinned
@@ -15,7 +14,7 @@ import { afterEach, describe, test, vi, beforeEach } from 'vitest'
import { countsOfRecords, makeStatsContext, makeSubagentCost } from '../../../src/client/components/statsContext'
import { makeAgentHeads } from '../../../src/client/agentHeads'
import { resetModelPrices, setModelPricesLoader } from '../../../src/client/modelPrices'
-import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage, TokenUsage } from '../../../src/shared/types'
+import type { ContextEventRecord, ContextTimeline, RequestRecord, SessionCostUsage } from '../../../src/shared/types'
import { TestClientCtx, asClientCtx } from '../helpers/harness'
import { flush, makeKit, mount, queryAll, text } from '../helpers/kit'
@@ -34,8 +33,6 @@ const PROVIDERS = {
const COST: SessionCostUsage = {
'deepseek-official': { 'deepseek-v4-flash': { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0 } } },
}
-// Prompt-side billed input 300 (100 uncached + 200 read) → hit 66.66% truncated.
-const USAGE: TokenUsage = { uncachedInputTokens: 100, outputTokens: 50, cacheReadTokens: 200, cacheWriteTokens: 0 }
function req(turn?: number): RequestRecord {
return {
@@ -82,60 +79,46 @@ describe('countsOfRecords (the inline generation derivation)', () => {
})
describe('StatsContext', () => {
- test('folds the seven-cell grid: shape stats, the cache-hit cell, and the two cost cells', async () => {
+ test('folds the six-cell grid: the shape stats and the two cost cells', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 3, steps: 4, injects: 3, compactions: 2, prunes: 1 },
humanInputs: 7,
toolCalls: 3,
- usage: USAGE,
cost: COST,
locale: 'en',
}))
await flush()
assert.ok(text(m.container).includes('Context Stats'))
const { labels, values } = cells(m.container)
- assert.equal(labels.length, 7)
- assert.deepEqual(labels, ['Turns', 'Steps', 'Human Inputs?', 'Tool Calls', 'Cache Hit?', 'Cost?', 'Subagent Cost?'])
+ assert.equal(labels.length, 6)
+ assert.deepEqual(labels, ['Turns', 'Steps', 'Human Inputs?', 'Tool Calls', 'Cost?', 'Subagent Cost?'])
// 1M uncached input at the doubled peak miss rate (2 × $0.15); no subagent usage → the sub cell dashes.
- assert.deepEqual(values, ['3', '4', '7', '3', '66.66%', '$0.30', '—'])
+ assert.deepEqual(values, ['3', '4', '7', '3', '$0.30', '—'])
await m.unmount()
})
- test('absent counters, usage, and cost degrade to zeros and the dash', async () => {
+ test('absent counters and cost degrade to zeros and the dash', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
}))
await flush()
- assert.deepEqual(cells(m.container).values, ['0', '0', '0', '0', '—', '—', '—'])
+ assert.deepEqual(cells(m.container).values, ['0', '0', '0', '0', '—', '—'])
await m.unmount()
- // A usage report with nothing billed prompt-side dashes the hit too.
- const zero: TokenUsage = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
- const m2 = await mount(h(StatsContext, {
- counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: zero,
- locale: 'en',
- }))
- await flush()
- assert.deepEqual(cells(m2.container).values, ['0', '0', '0', '0', '—', '—', '—'])
- await m2.unmount()
})
test('the cost bubble lists the billed models with their billed rates', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: COST,
locale: 'en',
}))
await flush()
- assert.equal(queryAll(m.container, '.lc-stat-tip').length, 4)
- assert.equal(queryAll(m.container, '.lc-stat-q').length, 4)
+ assert.equal(queryAll(m.container, '.lc-stat-tip').length, 3)
+ assert.equal(queryAll(m.container, '.lc-stat-q').length, 3)
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
assert.ok(tips[0].includes('question answerings'), 'the human-inputs tip explains its tally')
- assert.ok(tips[1].includes('Cumulative cache-read'), 'the cache-hit tip names the whole-session share')
- const costTip = tips[2]
+ const costTip = tips[1]
assert.ok(costTip.includes('this agent and all its subagents'), 'the cost tip names the family scope')
assert.ok(costTip.includes('Per-1M-token rates:'))
assert.ok(costTip.includes('Priced as listed on models.dev for deepseek · deepseek-v4-flash.'), 'the listing line names the resolved registry face')
@@ -146,19 +129,18 @@ describe('StatsContext', () => {
assert.ok(costTip.includes('Output $0.60'))
assert.ok(costTip.includes('peak windows'), 'a DeepSeek session explains the peak/off-peak scheme')
assert.ok(!costTip.includes('|'), 'no peak|off pairs — the footnotes carry the scheme')
- assert.ok(tips[3].includes('every subagent session'), 'the sub cell explains its own scope')
+ assert.ok(tips[2].includes('every subagent session'), 'the sub cell explains its own scope')
await m.unmount()
})
test('a non-DeepSeek session never sees DeepSeek-specific notes', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: { 'zai-coding-cn': { 'glm-5.3-flash': { peak: { uncached: 1_000_000, cacheRead: 0, cacheWrite: 0, output: 0 } } } },
locale: 'en',
}))
await flush()
- const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
+ const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
assert.ok(costTip.includes('Priced as listed on models.dev for zhipuai · glm-5.3-flash.'))
assert.ok(!costTip.includes('DeepSeek'), 'the DeepSeek scheme note stays out of other providers’ bubbles')
// glm-5.3-flash lists cache_write at 0 — the zero band drops from the table.
@@ -170,7 +152,6 @@ describe('StatsContext', () => {
test('the cost cell links to the models.dev provider listing when one provider priced the scope', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: COST,
locale: 'en',
}))
@@ -191,7 +172,6 @@ describe('StatsContext', () => {
}
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: two,
locale: 'en',
}))
@@ -207,12 +187,11 @@ describe('StatsContext', () => {
}
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: two,
locale: 'en',
}))
await flush()
- const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
+ const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
assert.ok(costTip.includes('for deepseek · deepseek-v4-flash.'))
assert.ok(costTip.includes('for zhipuai · glm-5.3-flash.'))
// 1M × $0.15 × 2 (the DeepSeek peak) + 2M × $0.075 = $0.45.
@@ -223,18 +202,16 @@ describe('StatsContext', () => {
test('the zh locale localizes labels and prices the cost in CNY at 1 CNY = 0.15 USD', async () => {
const m = await mount(h(StatsContextZh, {
counts: { turns: 1, steps: 1, injects: 0, compactions: 1, prunes: 0 },
- usage: USAGE,
cost: COST,
locale: 'zh',
}))
await flush()
assert.ok(text(m.container).includes('上下文统计'))
const { labels, values } = cells(m.container)
- assert.deepEqual(labels, ['轮次', '步数', '用户输入?', '工具调用', '缓存命中?', '费用?', '子 Agent 费用?'])
+ assert.deepEqual(labels, ['轮次', '步数', '用户输入?', '工具调用', '费用?', '子 Agent 费用?'])
// $0.30 / 0.15 = ¥2; the rates convert through the same fixed rate.
- assert.deepEqual(values, ['1', '1', '0', '0', '66.66%', '¥2.00', '—'])
- assert.ok(text(queryAll(m.container, '.lc-stat-tip')[1]).includes('整个会话累计'), 'the cache-hit tip localizes too')
- const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
+ assert.deepEqual(values, ['1', '1', '0', '0', '¥2.00', '—'])
+ const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
assert.ok(costTip.includes('含当前智能体及所有子智能体'), 'the cost tip names the family scope too')
assert.ok(costTip.includes('每百万 Token 价格'))
assert.ok(costTip.includes('按 deepseek · deepseek-v4-flash 在 models.dev 的刊登价格如上。'))
@@ -244,15 +221,14 @@ describe('StatsContext', () => {
assert.ok(costTip.includes('未缓存输入 ¥1.00'))
assert.ok(costTip.includes('缓存写入 ¥1.00'))
assert.ok(costTip.includes('输出 ¥4.00'))
- assert.ok(text(queryAll(m.container, '.lc-stat-tip')[3]).includes('子 Agent 会话'), 'the sub tip localizes too')
+ assert.ok(text(queryAll(m.container, '.lc-stat-tip')[2]).includes('子 Agent 会话'), 'the sub tip localizes too')
await m.unmount()
})
- test('usage with no book yet stays a dash until the fetch lands', async () => {
+ test('a book that has not landed yet keeps the cost cell dashed', async () => {
setModelPricesLoader(() => new Promise(() => {}))
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: USAGE,
cost: COST,
locale: 'en',
}))
@@ -266,13 +242,12 @@ describe('StatsContext', () => {
setModelPricesLoader(() => Promise.reject(new Error('down')))
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: USAGE,
cost: COST,
locale: 'en',
}))
await flush()
assert.ok(cells(m.container).values.at(-2) === '—')
- const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
+ const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
assert.ok(costTip.includes('unavailable'))
assert.ok(!costTip.includes('Per-1M-token rates'))
await m.unmount()
@@ -289,14 +264,13 @@ describe('StatsContext', () => {
}
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: split,
locale: 'en',
}))
await flush()
// 1M at the doubled $0.3 peak miss rate + 2M at the $0.15 off-peak (book) rate.
assert.ok(cells(m.container).values.at(-2) === '$0.60')
- const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
+ const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
assert.ok(costTip.includes('Cache input $0.00'), 'the table lists the book rates once, whatever the billed buckets')
assert.ok(costTip.includes('Uncached input $0.15'))
assert.ok(costTip.includes('peak windows'), 'the peak-window footnote explains the bucket split')
@@ -307,13 +281,12 @@ describe('StatsContext', () => {
test('a session whose models the book cannot price notes the outage too', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: USAGE,
cost: { 'future-provider': { 'mystery-model': { peak: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } },
locale: 'en',
}))
await flush()
assert.ok(cells(m.container).values.at(-2) === '—')
- assert.ok(text(queryAll(m.container, '.lc-stat-tip')[2]).includes('unavailable'))
+ assert.ok(text(queryAll(m.container, '.lc-stat-tip')[1]).includes('unavailable'))
await m.unmount()
})
@@ -329,12 +302,11 @@ describe('StatsContext', () => {
} as unknown as SessionCostUsage
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: USAGE,
cost: hostile,
locale: 'en',
}))
await flush()
- const costTip = text(queryAll(m.container, '.lc-stat-tip')[2])
+ const costTip = text(queryAll(m.container, '.lc-stat-tip')[1])
assert.ok(costTip.includes('glm-5.3-flash'))
assert.ok(!costTip.includes('junk'))
// No row may show the peak|off-peak pair — that is DeepSeek's alone.
@@ -355,7 +327,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
const Stats = makeStatsContext(kit, () => SUB)
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: COST,
locale: 'en',
}))
@@ -364,10 +335,10 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
assert.deepEqual(cells(m.container).values.slice(-2), ['$0.45', '$0.15'])
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
// The rate table covers BOTH sides' models in either tip.
- assert.ok(tips[2].includes('for deepseek · deepseek-v4-flash.'))
- assert.ok(tips[2].includes('for zhipuai · glm-5.3-flash.'))
- assert.ok(tips[3].includes('every subagent session'))
- assert.ok(tips[3].includes('for zhipuai · glm-5.3-flash.'), 'the sub tip carries its own price table')
+ assert.ok(tips[1].includes('for deepseek · deepseek-v4-flash.'))
+ assert.ok(tips[1].includes('for zhipuai · glm-5.3-flash.'))
+ assert.ok(tips[2].includes('every subagent session'))
+ assert.ok(tips[2].includes('for zhipuai · glm-5.3-flash.'), 'the sub tip carries its own price table')
await m.unmount()
})
@@ -375,7 +346,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
const Stats = makeStatsContext(kit, () => SUB)
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
}))
await flush()
@@ -390,15 +360,14 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
const Stats = makeStatsContext(kit, () => deepseekSub)
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
}))
await flush()
// The off bucket bills at book: 1M × $0.15.
assert.deepEqual(cells(m.container).values.slice(-2), ['$0.15', '$0.15'])
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
- assert.ok(tips[2].includes('peak windows'))
- assert.ok(tips[3].includes('peak windows'), 'the sub tip explains the scheme its own figure rides')
+ assert.ok(tips[1].includes('peak windows'))
+ assert.ok(tips[2].includes('peak windows'), 'the sub tip explains the scheme its own figure rides')
await m.unmount()
})
@@ -406,7 +375,6 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
const Stats = makeStatsContext(kit, () => ({ future: { 'mystery-model': { peak: { uncached: 1, cacheRead: 0, cacheWrite: 0, output: 0 } } } }))
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
cost: COST,
locale: 'en',
}))
@@ -416,14 +384,13 @@ describe('StatsContext — the subagent-cost cell (injected seat)', () => {
// total — the sub cell dashes.
assert.deepEqual(cells(m.container).values.slice(-2), ['$0.30', '—'])
const tips = queryAll(m.container, '.lc-stat-tip').map(el => text(el))
- assert.ok(tips[3].includes('unavailable'))
+ assert.ok(tips[2].includes('unavailable'))
await m.unmount()
})
test('a null seat keeps both cells dashed with no outage notes', async () => {
const m = await mount(h(StatsContext, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
}))
await flush()
@@ -498,7 +465,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
const Stats = makeStatsContext(kit, seat)
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
sessionId: 'root',
}))
@@ -552,7 +518,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
const Stats = makeStatsContext(kit, seat)
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
sessionId: 'root',
}))
@@ -588,7 +553,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
const Stats = makeStatsContext(kit, seat)
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
sessionId: 'root',
}))
@@ -614,7 +578,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
const Stats = makeStatsContext(kit, makeSubagentCost(asClientCtx(ctx), makeAgentHeads(asClientCtx(ctx))))
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
sessionId: 'root',
}))
@@ -634,7 +597,6 @@ describe('StatsContext — the real subagent-cost seat (makeSubagentCost)', () =
const Stats = makeStatsContext(kit, makeSubagentCost(asClientCtx(ctx), makeAgentHeads(asClientCtx(ctx))))
const m = await mount(h(Stats, {
counts: { turns: 0, steps: 0, injects: 0, compactions: 0, prunes: 0 },
- usage: null,
locale: 'en',
}))
await flush()
diff --git a/tests/client/format.spec.ts b/tests/client/format.spec.ts
index a1e0746..fe68330 100644
--- a/tests/client/format.spec.ts
+++ b/tests/client/format.spec.ts
@@ -60,13 +60,6 @@ describe('cacheHitPercent', () => {
assert.equal(cacheHitPercent(82499, 83000), '99.39')
assert.equal(cacheHitPercent(83000, 83000), '100.00')
})
-
- test('the stats board cell truncates to one decimal by the same formula', () => {
- assert.equal(cacheHitPercent(2, 3, 1), '66.6')
- assert.equal(cacheHitPercent(1, 3, 1), '33.3')
- assert.equal(cacheHitPercent(82499, 83000, 1), '99.3')
- assert.equal(cacheHitPercent(83000, 83000, 1), '100.0')
- })
})
describe('fmtTime', () => {