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', () => {