feat(client): add a session coverage picker to Context Insights, defaulting to all

Archived sessions fold back into every figure the page derives off the
session list: rowsOfSnapshot now tags archived rows instead of dropping
them, and one coveredRowsOf pass scopes the KPI band, charts, heatmap,
skills, and the session grid together. A "Filter sessions" select next to
the list's search box offers dsh's own archived-filter set — All (show
archived) by default, Hide archived, Archived only. The Active Sessions
card follows the picked coverage (Sessions incl. the window's archived
count under All; Active/Archived Sessions over the full-list total under
the narrow scopes), archived cards wear an Archived tag and refuse to
open like dsh does, and kpisOf drops its now-unused listed param.
This commit is contained in:
bowenliang123 committed 2026-10-16 15:58:28 +08:00
1 parent ca5a3e8d3d
commit 7ae8de869d
11 files changed
+156 -33

No files matched your search

+3 -1
View File
@@ -36,12 +36,14 @@ export function makeOverviewCard(kit: ViewKit): (props: OverviewCardProps) => Re
<button
type="button"
className={'lc-ov-session' + (row.current ? ' lc-ov-session-current' : '')}
onClick={() => { props.onOpen(row.id) }}
aria-disabled={row.archived || undefined}
onClick={() => { if (!row.archived) props.onOpen(row.id) }}
>
<span className="lc-ov-session-head">
{row.running && <span className="lc-ov-running" title={t('ov.running')} />}
<span className="lc-ov-session-title" title={row.title}>{row.title}</span>
{row.current && <span className="lc-ov-current">{t('ov.current')}</span>}
{row.archived && <span className="lc-ov-archived" title={t('ov.card.archivedTip')}>{t('ov.card.archived')}</span>}
<span className="lc-ov-session-times">
<span className="lc-ov-session-time">{relativeTime(t, row.updatedAt, props.now)}</span>
{created !== undefined && <span className="lc-ov-session-created">{created}</span>}
+33 -10
View File
@@ -8,7 +8,10 @@
* <body>, because that container would otherwise capture their fixed positioning.
*
* The cluster holds ONE scope: a picked calendar range replaces the rolling preset and clicking a preset hands it
* back, so they never both claim it, and everything the presets scope reads that one resolved window. Below the KPI
* back, so they never both claim it, and everything the presets scope reads that one resolved window. The coverage
* picker sits at the sessions feed's head and defaults to All: archived sessions fold into the page's every figure
* unless the picker narrows them, and the Active Sessions card's label and sub-line follow the picked coverage.
* Below the KPI
* band and the usage chart, the heatmap pins both its column mates to a picked day, and a skill-card row click
* narrows the session column to that skill's loaders within the same scope. A session card jumps through the
* harness's own `openSessionVia`, whose navigation returns the center column to the conversation.
@@ -19,11 +22,11 @@ import { estimateSessionCost, formatCost, type CostCurrency, type ModelBook } fr
import { fmt } from '../format'
import { useModelPrices } from '../modelPrices'
import {
aggregateDays, dayRangeWindow, filterRows, groupCountsOf, inGroup, kpisOf,
aggregateDays, coveredRowsOf, dayRangeWindow, filterRows, groupCountsOf, inGroup, kpisOf,
pageOf, rangeWindowOf, refreshSessions, requestActivityBackfill, rowsOfSnapshot,
rowLoadedSkill, sessionGroupsOf, sessionsSnapshotOf, skillLoadsOf, sortRows,
UNGROUPED_KEY, workspacesSnapshotOf,
type DayRange, type OverviewRange, type OverviewRow, type OverviewSort, type SkillSort,
type DayRange, type OverviewCoverage, type OverviewRange, type OverviewRow, type OverviewSort, type SkillSort,
} from '../overview'
import { openSessionVia, type ClientCtx } from '../services'
import { readSkillCatalog } from '../skills'
@@ -52,6 +55,8 @@ export interface OverviewPanelProps {
const RANGES: readonly OverviewRange[] = ['today', '7d', '30d', 'all']
const SORTS: readonly OverviewSort[] = ['recent', 'tokens', 'context']
/** The coverage scopes the sessions feed serves, in menu order. */
const COVERAGE: readonly OverviewCoverage[] = ['all', 'active', 'archived']
/** The heatmap's depth metrics, in toggle order (steps is the default). */
const METRICS: readonly HeatMetric[] = ['sessions', 'steps']
@@ -85,6 +90,7 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
const wsSnapshot = workspacesSnapshotOf(props)
const [preset, setPreset] = useState<OverviewRange>('30d')
const [custom, setCustom] = useState<DayRange | null>(null)
const [coverage, setCoverage] = useState<OverviewCoverage>('all')
const [day, setDay] = useState<string | null>(null)
const [query, setQuery] = useState('')
const [group, setGroup] = useState<string | null>(null)
@@ -122,11 +128,12 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
}, [])
// Any filter change re-anchors the pager at the first page.
useEffect(() => { setPage(0) }, [preset, custom, day, query, group, skill, sort])
useEffect(() => { setPage(0) }, [preset, custom, coverage, day, query, group, skill, sort])
const currency = activeCurrency()
const now = Date.now()
const allRows = rows ?? []
const covered = coveredRowsOf(allRows, coverage)
// The scope window every figure below reads: the calendar's own picked
// range while one is set (a malformed key keeps the preset, never an unprovable window), else the preset's rolling floor.
const scope = (custom !== null ? dayRangeWindow(custom) : null) ?? rangeWindowOf(preset, now)
@@ -137,8 +144,8 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
const spanTo = scope.start === null ? null : dayKeyOf(scope.end ?? now)
const span = spanFrom !== null && spanTo !== null ? { from: spanFrom, to: spanTo } : null
// The scope scopes the KPI band, the composition donut, and the grid;
// the heatmap keeps its own fixed window over the whole list.
const ranged = filterRows(allRows, { scope, day: null, query: '' })
// the heatmap keeps its own fixed window over the coverage-selected list.
const ranged = filterRows(covered, { scope, day: null, query: '' })
// The group chips count the day/query-scoped rows BEFORE the group filter
// applies, so selecting a chip never collapses the row itself.
const scoped = filterRows(ranged, { scope, day, query })
@@ -156,13 +163,19 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
sort,
)
const paged = pageOf(visible, page)
const kpi = kpisOf(ranged, allRows.length, book, currency)
const days = aggregateDays(allRows, book, currency)
const kpi = kpisOf(ranged, book, currency)
const days = aggregateDays(covered, book, currency)
const openOne = (id: string): void => {
// The view owner's verb navigates the center column back to the
// conversation itself (selectPanel(null)), so the page needs no close.
openSessionVia(ctx, id)
}
// The sessions card mirrors its coverage: All prices in the window's
// archived share; the narrow scopes name what the full list holds.
const sessionsLabel = coverage === 'archived' ? t('ov.kpi.archived') : coverage === 'active' ? t('ov.kpi.sessions') : t('ov.kpi.allSessions')
const sessionsSub = coverage === 'all'
? t('ov.kpi.inclArchived', { n: ranged.filter(row => row.archived).length })
: t('ov.kpi.ofTotal', { n: allRows.length })
return (
<section className="lc-ov-page" aria-label={t('ov.title')} {...zoneProps}>
@@ -208,9 +221,9 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
<div className="lc-ov-first">
<div className="lc-ov-kpis">
<div className="lc-stat lc-ov-kpi">
<span className="lc-stat-label">{t('ov.kpi.sessions')}</span>
<span className="lc-stat-label">{sessionsLabel}</span>
<span className="lc-stat-value">{kpi.sessions}</span>
<span className="lc-stat-sub">{t('ov.kpi.ofTotal', { n: kpi.listed })}</span>
<span className="lc-stat-sub">{sessionsSub}</span>
</div>
<div className="lc-stat lc-ov-kpi">
<span className="lc-stat-label">{t('ov.kpi.tokens')}</span>
@@ -300,6 +313,16 @@ export function makeOverviewPanel(ctx: ClientCtx, kit: ViewKit): (props: Overvie
aria-label={t('ov.list.search')}
onChange={(ev) => { setQuery(ev.target.value) }}
/>
<select
className="lc-ov-cover"
value={coverage}
aria-label={t('ov.cover.label')}
onChange={(ev) => { setCoverage(ev.target.value as OverviewCoverage) }}
>
{COVERAGE.map(c => (
<option key={c} value={c}>{t('ov.cover.' + c)}</option>
))}
</select>
<div className="lc-gran" role="group" aria-label={t('ov.list.sortLabel')}>
{SORTS.map(s => (
<button
+9
View File
@@ -480,8 +480,11 @@ export const DICT_EN = {
'ov.date.wd.4': 'T',
'ov.date.wd.5': 'F',
'ov.date.wd.6': 'S',
'ov.kpi.allSessions': 'Sessions',
'ov.kpi.sessions': 'Active Sessions',
'ov.kpi.archived': 'Archived Sessions',
'ov.kpi.ofTotal': 'of {n} total',
'ov.kpi.inclArchived': 'incl. {n} archived',
'ov.kpi.tokens': 'Tokens Used',
'ov.kpi.sessionsSub': 'across {n} sessions',
'ov.kpi.pricedSub': '{n} of {total} sessions priced',
@@ -544,6 +547,10 @@ export const DICT_EN = {
'ov.skills.k.path': 'Path',
'ov.skills.copyPath': 'Click to copy the path',
'ov.skills.copied': 'Copied',
'ov.cover.label': 'Filter sessions',
'ov.cover.all': 'All (show archived)',
'ov.cover.active': 'Hide archived',
'ov.cover.archived': 'Archived only',
'ov.list.title': 'Sessions',
'ov.list.search': 'Search title / path / last message…',
'ov.list.sortLabel': 'Sort',
@@ -565,6 +572,8 @@ export const DICT_EN = {
'ov.running': 'Running',
'ov.current': 'current',
'ov.card.turns': '{n} turns · {s} steps',
'ov.card.archived': 'Archived',
'ov.card.archivedTip': 'Archived sessions cannot be opened. Unarchive it to view.',
'ov.card.teamUsage': 'Total Tokens',
'ov.card.teamCost': 'Total Est. Cost',
'ov.card.lastUser': 'Last message',
+9
View File
@@ -479,8 +479,11 @@ export const DICT_ZH = {
'ov.date.wd.4': '四',
'ov.date.wd.5': '五',
'ov.date.wd.6': '六',
'ov.kpi.allSessions': '全部会话',
'ov.kpi.sessions': '活跃会话',
'ov.kpi.archived': '已归档会话',
'ov.kpi.ofTotal': '共 {n} 个',
'ov.kpi.inclArchived': '其中已归档 {n} 个',
'ov.kpi.tokens': '累计消耗',
'ov.kpi.sessionsSub': '含 {n} 个会话',
'ov.kpi.pricedSub': '已计价 {n}/{total} 个会话',
@@ -543,6 +546,10 @@ export const DICT_ZH = {
'ov.skills.k.path': '路径',
'ov.skills.copyPath': '点击复制路径',
'ov.skills.copied': '已复制',
'ov.cover.label': '筛选会话',
'ov.cover.all': '全部会话(显示已归档)',
'ov.cover.active': '隐藏已归档',
'ov.cover.archived': '仅显示已归档',
'ov.list.title': '会话',
'ov.list.search': '搜索标题 / 目录 / 最后消息…',
'ov.list.sortLabel': '排序',
@@ -564,6 +571,8 @@ export const DICT_ZH = {
'ov.running': '运行中',
'ov.current': '当前',
'ov.card.turns': '{n} 轮 {s} 步',
'ov.card.archived': '已归档',
'ov.card.archivedTip': '已归档会话暂无法打开,取消归档后可查看。',
'ov.card.teamUsage': 'Token 总用量',
'ov.card.teamCost': '总预估费用',
'ov.card.lastUser': '最后消息',
+18 -7
View File
@@ -26,6 +26,8 @@ export interface OverviewRow {
updatedAt: number
running: boolean
current: boolean
/** Whether the workspace registry archived this session: the coverage picker's split line. */
archived: boolean
timeline: ContextTimeline | null
/** The TEAM's merged daily ledger: this session's own plus every descendant subagent's. */
activity: ContextActivity | null
@@ -131,7 +133,8 @@ export function inGroup(row: OverviewRow, group: string, groups: Record<string,
return group === UNGROUPED_KEY ? title === undefined : title === group
}
/** Fail open: an absent seat, a hostile shape, or a throwing accessor archives nothing, so filtering never empties the list. */
/** Fail open: an absent seat, a hostile shape, or a throwing accessor tags
* nothing, so the coverage split keeps every row in the pane's own scope. */
function archivedSetOf(workspaces: unknown): Set<string> {
try {
const value = asRecord(workspaces)?.archivedSessionIds
@@ -143,8 +146,9 @@ function archivedSetOf(workspaces: unknown): Set<string> {
}
/** Join the raw session-list snapshot into render-ready rows, or null when it
* is unusable (distinct from a real empty list). Blank placeholder and
* archived rows drop out; one throwing row costs just itself. */
* is unusable (distinct from a real empty list). Blank placeholder rows drop
* out, archived rows are tagged for the coverage picker, and one throwing row
* costs just itself. */
export function rowsOfSnapshot(snapshot: unknown, workspaces?: unknown): OverviewRow[] | null {
const state = asRecord(snapshot)
if (state === null) return null
@@ -155,7 +159,6 @@ export function rowsOfSnapshot(snapshot: unknown, workspaces?: unknown): Overvie
const current = typeof state.current === 'string' ? state.current : undefined
const rows: OverviewRow[] = []
for (const id of ids) {
if (archived.has(id)) continue
try {
const row = asRecord(byId[id])
if (row === null) continue
@@ -175,6 +178,7 @@ export function rowsOfSnapshot(snapshot: unknown, workspaces?: unknown): Overvie
updatedAt: typeof row.updatedAt === 'number' && Number.isFinite(row.updatedAt) ? row.updatedAt : 0,
running: row.running === true,
current: id === current,
archived: archived.has(id),
timeline: timelineOf(values?.contextTimeline),
activity: activityOf(values?.contextActivity),
family: [],
@@ -218,6 +222,16 @@ export function rowsOfSnapshot(snapshot: unknown, workspaces?: unknown): Overvie
return rows
}
/** The coverage picker's scopes: 'active' hides archived sessions, 'archived'
* lists only them, and 'all' folds both. */
export type OverviewCoverage = 'all' | 'active' | 'archived'
/** The coverage pass every page reader (KPI band, chart, heatmap, grid) shares:
* one list, so the cards can never disagree with the grid on which sessions count. */
export function coveredRowsOf(rows: readonly OverviewRow[], coverage: OverviewCoverage): readonly OverviewRow[] {
return coverage === 'all' ? rows : rows.filter(row => row.archived === (coverage === 'archived'))
}
/** Merge several sessions' daily ledgers into one family's: per day the tokens
* and requests sum, the pricing records merge, and the skill tables merge name by name (loads sum, last-load takes the max). */
function mergeActivity(members: (ContextActivity | null)[]): ContextActivity | null {
@@ -579,7 +593,6 @@ export function tokenPartsOf(rows: readonly OverviewRow[]): { parts: TokenPartTo
export interface OverviewKpis {
sessions: number
listed: number
tokens: number
turns: number
cost: number | null
@@ -596,7 +609,6 @@ export interface OverviewKpis {
export function kpisOf(
rows: readonly OverviewRow[],
listed: number,
book: ModelBook | null | undefined,
currency: CostCurrency,
): OverviewKpis {
@@ -623,7 +635,6 @@ export function kpisOf(
}
return {
sessions: rows.length,
listed,
tokens: totals?.total ?? 0,
turns,
cost: estimateSessionCost(usage, book, currency),
+3
View File
@@ -165,6 +165,8 @@ button.lc-heat-cell:hover { box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--col
.lc-ov-search { flex: 1 1 150px; min-width: 110px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); border-radius: 6px; padding: 4px 10px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-primary); outline: none; transition: border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
.lc-ov-search:focus { border-color: var(--dsw-alias-label-secondary); }
.lc-ov-search::placeholder { color: var(--dsw-alias-label-secondary); }
/* The coverage picker neighbors the search box; the search recipe keeps the two one height. */
.lc-ov-cover { flex: none; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); border-radius: 6px; padding: 4px 8px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-primary); }
.lc-ov-groups { display: flex; gap: 6px; flex-wrap: wrap; margin: -2px 0 10px; }
.lc-ov-chip { display: inline-flex; align-items: baseline; gap: 5px; border: 1px solid var(--dsw-alias-border-l2); background: transparent; color: var(--dsw-alias-label-secondary); border-radius: 999px; font: inherit; font-size: 11px; line-height: 1.5; padding: 2px 10px; cursor: pointer; max-width: 220px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), border-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out), background-color var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease-in-out); }
@@ -184,6 +186,7 @@ button.lc-heat-cell:hover { box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--col
/* The 2-line clamp keeps one long title from ballooning its row; the full text stays on the tooltip. */
.lc-ov-session-title { flex: 1 1 auto; min-width: 0; font-weight: 600; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow-wrap: anywhere; }
.lc-ov-current { flex: none; font-size: 10px; line-height: 1.5; color: var(--color-blue-500); border: 1px solid color-mix(in srgb, var(--color-blue-500) 45%, transparent); border-radius: 4px; padding: 0 4px; }
.lc-ov-archived { flex: none; font-size: 10px; line-height: 1.5; color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l2); border-radius: 4px; padding: 0 4px; }
.lc-ov-session-times { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; }
.lc-ov-session-time { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }
.lc-ov-session-created { color: var(--dsw-alias-label-secondary); font-size: 10px; line-height: 1.4; white-space: nowrap; font-variant-numeric: tabular-nums; }
@@ -17,6 +17,7 @@ function rowOf(over: Partial<OverviewRow> = {}): OverviewRow {
updatedAt: NOW - 3 * 3_600_000,
running: false,
current: false,
archived: false,
timeline: null,
activity: null,
family: [over.timeline ?? null],
+52 -6
View File
@@ -151,7 +151,7 @@ describe('OverviewPanel', () => {
assert.ok(query(m.container, '.lc-ov-first > .lc-card.lc-ov-usage') !== null, 'the usage chart rides the row')
// KPI band: 2 sessions in the 30d range, 1750 tokens billed, priced cost, cache hit.
const labels = queryAll(m.container, '.lc-stat-label').map(el => el.textContent)
assert.deepEqual(labels, ['Active Sessions', 'Tokens Used', 'Cost', 'Cache Hit', 'Tool Calls', 'Active Time'])
assert.deepEqual(labels, ['Sessions', 'Tokens Used', 'Cost', 'Cache Hit', 'Tool Calls', 'Active Time'])
const values = queryAll(m.container, '.lc-stat-value').map(el => el.textContent)
assert.equal(values[0], '2')
assert.equal(values[1], '1.8k')
@@ -189,16 +189,52 @@ describe('OverviewPanel', () => {
await m.unmount()
})
test('archived sessions drop from every band, not just the card grid', async () => {
test('the coverage picker folds archived sessions in by default and its scopes split the page', async () => {
const ctx = makeCtx()
const { m } = await openPanel(ctx, {
useWorkspaces: useHookOf({ ...workspacesSnapshotValue, archivedSessionIds: ['b'] }),
})
const values = queryAll(m.container, '.lc-stat-value').map(el => el.textContent)
assert.equal(values[0], '1', 'the archived session leaves the KPI band')
const cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
// Default 'all': the archived session folds back into every band, tagged on its card.
assert.deepEqual(queryAll(m.container, '.lc-stat-label')[0].textContent, 'Sessions')
assert.deepEqual(queryAll(m.container, '.lc-stat-sub')[0].textContent, 'incl. 1 archived')
let cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 2)
const betaCard = cards.find(c => text(c).includes('beta session'))
assert.ok(betaCard !== undefined && text(betaCard).includes('Archived'), 'the archived card carries the tag')
const select = query<HTMLSelectElement>(m.container, 'select.lc-ov-cover')
assert.deepEqual([...select.options].map(o => o.textContent), ['All (show archived)', 'Hide archived', 'Archived only'])
// 'active' restores the lighter list; the sub-line names what the full list holds.
await actSelect(select, 'active')
assert.deepEqual(queryAll(m.container, '.lc-stat-label')[0].textContent, 'Active Sessions')
assert.deepEqual(queryAll(m.container, '.lc-stat-sub')[0].textContent, 'of 3 total')
assert.equal(queryAll(m.container, '.lc-stat-value')[0].textContent, '1', 'the archived session leaves the KPI band')
cards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(cards.length, 1)
assert.ok(text(cards[0]).includes('alpha session'))
// 'archived' flips the split: only the tagged session in range stays.
await actSelect(select, 'archived')
assert.deepEqual(queryAll(m.container, '.lc-stat-label')[0].textContent, 'Archived Sessions')
assert.deepEqual(queryAll(m.container, '.lc-stat-sub')[0].textContent, 'of 3 total')
assert.equal(queryAll(m.container, '.lc-stat-value')[0].textContent, '1')
const archCards = queryAll(m.container, '.lc-ov-grid > .lc-ov-session')
assert.equal(archCards.length, 1)
assert.ok(text(archCards[0]).includes('beta session'))
await m.unmount()
})
test('an archived card click opens nothing (dsh refuses archived sessions)', async () => {
const opened: string[] = []
const ctx = new TestClientCtx({
services: {
uiWorkspace: { openSession: (id: string) => { opened.push(id) } },
sessions: {},
},
})
const { m } = await openPanel(ctx, {
useWorkspaces: useHookOf({ ...workspacesSnapshotValue, archivedSessionIds: ['b'] }),
})
await click(queryAll<HTMLButtonElement>(m.container, '.lc-ov-grid > .lc-ov-session').find(c => text(c).includes('beta session'))!)
assert.deepEqual(opened, [], 'the archived card never navigates')
await m.unmount()
})
@@ -531,7 +567,8 @@ describe('OverviewPanel', () => {
}))
await flush()
assert.ok(text(m.container).includes('上下文洞察'))
assert.ok(text(m.container).includes('活跃会话'))
assert.ok(text(m.container).includes('全部会话'), 'the All default labels the sessions card and the coverage option')
assert.ok(text(m.container).includes('仅显示已归档'), 'the coverage options ride the zh dictionary too')
assert.ok(text(m.container).includes('Token 统计'), 'the aggregate stats row rides the zh dictionary too')
assert.ok(text(m.container).includes('系统提示词'), 'the Context tab\'s composition categories render in zh')
assert.equal(query<HTMLButtonElement>(m.container, '.lc-ov-settings').title, '打开插件设置', 'the settings chip rides the zh dictionary too')
@@ -641,3 +678,12 @@ async function actType(input: HTMLInputElement, value: string): Promise<void> {
input.dispatchEvent(new Event('input', { bubbles: true }))
})
}
/** Pick a coverage option through the React change path (native setter + change event, act-wrapped). */
async function actSelect(select: HTMLSelectElement, value: string): Promise<void> {
await act(async () => {
const setter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value')?.set
setter?.call(select, value)
select.dispatchEvent(new Event('change', { bubbles: true }))
})
}
+1
View File
@@ -15,6 +15,7 @@ export function rowOf(over: Partial<OverviewRow> = {}): OverviewRow {
updatedAt: 100,
running: false,
current: false,
archived: false,
timeline: null,
activity: null,
family: [over.timeline ?? null],
+3 -4
View File
@@ -196,9 +196,8 @@ describe('kpisOf', () => {
} as unknown as ContextTimeline,
}),
]
const kpi = kpisOf(rows, 5, prices, 'usd')
const kpi = kpisOf(rows, prices, 'usd')
assert.equal(kpi.sessions, 2)
assert.equal(kpi.listed, 5)
assert.equal(kpi.tokens, 200)
assert.equal(kpi.turns, 5)
assert.ok(kpi.cost !== null && Math.abs(kpi.cost - 390e-6) < 1e-12, 'the DeepSeek peak bucket doubles: 2 × (50×0.1 + 100×1 + 10×1 + 40×2) per 1M')
@@ -229,7 +228,7 @@ describe('kpisOf', () => {
}),
rowOf(),
]
const kpi = kpisOf(rows, 3, prices, 'usd')
const kpi = kpisOf(rows, prices, 'usd')
assert.equal(kpi.costSessions, 1, 'only the priced session counts toward the cost cell')
assert.equal(kpi.usageSessions, 2, 'both billed sessions feed the cache-hit rate')
assert.ok(kpi.cost !== null && Math.abs(kpi.cost - 390e-6) < 1e-12, 'the unpriced session adds nothing to the estimate')
@@ -238,7 +237,7 @@ describe('kpisOf', () => {
})
test('an unbilled set zeroes and dashes', () => {
const kpi = kpisOf([rowOf()], 1, null, 'cny')
const kpi = kpisOf([rowOf()], null, 'cny')
assert.equal(kpi.tokens, 0)
assert.equal(kpi.turns, 0)
assert.equal(kpi.cost, null)
+24 -5
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'
import { describe, test } from 'vitest'
import { billedOf, rowsOfSnapshot, sessionsSnapshotOf, tokenPartsOf } from '../../src/client/overview'
import { billedOf, coveredRowsOf, rowsOfSnapshot, sessionsSnapshotOf, tokenPartsOf } from '../../src/client/overview'
import { mergeCostUsage } from '../../src/client/cost'
import type { SessionCostUsage } from '../../src/shared/types'
import { COST } from './overview.fixtures'
@@ -71,7 +71,7 @@ describe('rowsOfSnapshot', () => {
assert.equal(rows.length, 4)
const [b, c, d, e] = rows
assert.deepEqual(b, {
id: 'b', title: 'shown', updatedAt: 5, running: true, current: true,
id: 'b', title: 'shown', updatedAt: 5, running: true, current: true, archived: false,
timeline: null, activity: null, family: [null], familyCost: null,
})
assert.equal(c.title, 'titled')
@@ -82,7 +82,7 @@ describe('rowsOfSnapshot', () => {
assert.equal(e.updatedAt, 0, 'a non-numeric stamp zeroes')
})
test('archived sessions drop; a hostile archive set fails open', () => {
test('archived sessions are tagged, not dropped; a hostile archive set fails open', () => {
const snap = snapshotOf([
['a', { displayTitle: 'live' }],
['b', { displayTitle: 'archived' }],
@@ -90,7 +90,10 @@ describe('rowsOfSnapshot', () => {
])
const rows = rowsOfSnapshot(snap, { archivedSessionIds: ['b', 7, null] })
assert.ok(rows !== null)
assert.deepEqual(rows.map(r => r.id), ['a', 'c'], 'archived ids drop; non-string entries archive nothing')
assert.deepEqual(rows.map(r => r.id), ['a', 'b', 'c'])
assert.equal(rows[1].archived, true, 'the archived id tags its row')
assert.equal(rows[0].archived, false)
assert.equal(rows[2].archived, false, 'non-string entries archive nothing')
// Fail-open shapes: an absent seat, a bare record, a malformed set, and a throwing accessor all keep the full list.
assert.equal(rowsOfSnapshot(snap)?.length, 3)
assert.equal(rowsOfSnapshot(snap, null)?.length, 3)
@@ -247,7 +250,8 @@ describe('rowsOfSnapshot: the family fold', () => {
})
})
test('hostile descendant projections degrade to null members, never a throw', () => { const rows = rowsOfSnapshot(snapshotOf([
test('hostile descendant projections degrade to null members, never a throw', () => {
const rows = rowsOfSnapshot(snapshotOf([
['root', { displayTitle: 'root', projectionValues: { contextTimeline: billedHead(1, COST) } }],
['kid', { origin: 'subagent', parentId: 'root', projectionValues: { contextTimeline: 7, contextActivity: 'x' } }],
['junk', 5 as unknown as Record<string, unknown>],
@@ -259,3 +263,18 @@ describe('rowsOfSnapshot: the family fold', () => {
assert.equal(billedOf(rows[0]), 200)
})
})
describe('coveredRowsOf', () => {
test('all passes the list through; the narrow scopes split on the archived tag', () => {
const rows = [
{ id: 'a', archived: false },
{ id: 'b', archived: true },
{ id: 'c', archived: true },
] as unknown as Parameters<typeof coveredRowsOf>[0]
assert.equal(coveredRowsOf(rows, 'all'), rows)
assert.deepEqual(coveredRowsOf(rows, 'active').map(r => r.id), ['a'])
assert.deepEqual(coveredRowsOf(rows, 'archived').map(r => r.id), ['b', 'c'])
assert.equal(coveredRowsOf([], 'archived').length, 0)
assert.deepEqual(coveredRowsOf([], 'all'), [])
})
})