dsh-token-total 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1495,6 +1495,383 @@ async function resolveModelNames(modelRefs, llm) {
1495
1495
  return names;
1496
1496
  }
1497
1497
  //#endregion
1498
+ //#region src/session-detail.ts
1499
+ const zero = () => ({
1500
+ uncachedInputTokens: 0,
1501
+ outputTokens: 0,
1502
+ cacheReadTokens: 0,
1503
+ cacheWriteTokens: 0
1504
+ });
1505
+ function add$1(a, b) {
1506
+ a.uncachedInputTokens += b.uncachedInputTokens;
1507
+ a.outputTokens += b.outputTokens;
1508
+ a.cacheReadTokens += b.cacheReadTokens;
1509
+ a.cacheWriteTokens += b.cacheWriteTokens;
1510
+ }
1511
+ function usageOf(x) {
1512
+ const u = x?.usage ?? x;
1513
+ return {
1514
+ uncachedInputTokens: Number(u?.inputTokens) || 0,
1515
+ outputTokens: Number(u?.outputTokens) || 0,
1516
+ cacheReadTokens: Number(u?.cacheReadTokens) || 0,
1517
+ cacheWriteTokens: Number(u?.cacheWriteTokens) || 0
1518
+ };
1519
+ }
1520
+ function positive(u) {
1521
+ return u.uncachedInputTokens + u.outputTokens + u.cacheReadTokens + u.cacheWriteTokens > 0;
1522
+ }
1523
+ function routeOf(x) {
1524
+ const source = x?.message?.source ?? x?.source ?? x?.request ?? x?.context ?? x;
1525
+ return {
1526
+ provider: typeof source?.provider === "string" ? source.provider : "",
1527
+ model: typeof source?.model === "string" ? source.model : ""
1528
+ };
1529
+ }
1530
+ function streamUsage(data) {
1531
+ let firstTime;
1532
+ let usage;
1533
+ for (const r of Array.isArray(data?.stream) ? data.stream : []) {
1534
+ const t = typeof r?.time === "number" ? r.time : void 0;
1535
+ if (firstTime === void 0 && t !== void 0 && (r?.type === "chunk" || r?.chunk?.type === "text" || r?.chunk?.type === "content")) firstTime = t;
1536
+ if (r?.type === "chunk" && r?.chunk?.type === "usage") usage = usageOf(r.chunk);
1537
+ }
1538
+ return {
1539
+ usage,
1540
+ firstTime
1541
+ };
1542
+ }
1543
+ function headerOf$1(item) {
1544
+ const x = item;
1545
+ if (x?.header?.id && typeof x.header.id === "string") return x.header;
1546
+ return typeof x?.id === "string" && x.id ? x : null;
1547
+ }
1548
+ async function read(p, id, live) {
1549
+ if (live?.snapshotEvents && typeof live.inheritedEventCount === "number") return {
1550
+ events: live.snapshotEvents(),
1551
+ cut: Math.max(0, live.inheritedEventCount)
1552
+ };
1553
+ if (p.open) {
1554
+ const h = await p.open(id, "read");
1555
+ const out = [];
1556
+ try {
1557
+ for (let off = 0;;) {
1558
+ const page = await h.read(off, 500);
1559
+ if (!page.events.length) break;
1560
+ out.push(...page.events);
1561
+ off += page.events.length;
1562
+ }
1563
+ } finally {
1564
+ await h.close().catch(() => {});
1565
+ }
1566
+ return {
1567
+ events: out,
1568
+ cut: Math.max(0, Number(h.inheritedEventCount) || 0)
1569
+ };
1570
+ }
1571
+ if (p.inspect) {
1572
+ const x = await p.inspect(id);
1573
+ return {
1574
+ events: x.events,
1575
+ cut: Math.max(0, Number(x.inheritedEventCount) || 0)
1576
+ };
1577
+ }
1578
+ throw new Error("no readable persistence seam (need open or inspect)");
1579
+ }
1580
+ function nameOf(h, id) {
1581
+ const x = h;
1582
+ if (typeof x?.name === "string" && x.name) return x.name;
1583
+ if (typeof x?.title === "string" && x.title) return x.title;
1584
+ if (typeof x?.cwd === "string" && x.cwd) return x.cwd.split(/[\\/]/).filter(Boolean).pop() || id;
1585
+ return id;
1586
+ }
1587
+ async function one(p, id, header, live) {
1588
+ const { events, cut } = await read(p, id, live);
1589
+ const fold = new UsageFold();
1590
+ const totals = zero();
1591
+ const records = [];
1592
+ const byKey = /* @__PURE__ */ new Map();
1593
+ let route = live?.requestContext?.() ?? {};
1594
+ let ordinal = 0;
1595
+ let currentTurn = 0;
1596
+ let effort = "";
1597
+ let pendingTopic = "";
1598
+ const turnTopics = /* @__PURE__ */ new Map();
1599
+ const own = events.filter((e) => !(cut > 0 && typeof e.seq === "number" && e.seq < cut));
1600
+ let label = "", title = "";
1601
+ for (const ev of own) {
1602
+ const d = ev.data ?? {};
1603
+ if (!label && ev.type === "subagent/descriptor" && typeof d.label === "string" && d.label.trim()) label = d.label.trim();
1604
+ if (!title && ev.type === "session/title" && typeof d.title === "string" && d.title.trim()) title = d.title.trim();
1605
+ if (label && title) break;
1606
+ }
1607
+ const name = label || title || nameOf(header, id);
1608
+ for (const ev of own) {
1609
+ const d = ev.data ?? {};
1610
+ const turn = Number.isFinite(d.turn) ? d.turn : 0;
1611
+ const step = Number.isFinite(d.step) ? d.step : 0;
1612
+ if (ev.type === "request/context") {
1613
+ route = {
1614
+ ...route,
1615
+ ...routeOf(d)
1616
+ };
1617
+ continue;
1618
+ }
1619
+ const r = routeOf(d);
1620
+ if (r.provider || r.model) route = {
1621
+ ...route,
1622
+ ...r
1623
+ };
1624
+ if (ev.type === "turn/start") {
1625
+ currentTurn = turn;
1626
+ if (pendingTopic && !turnTopics.has(currentTurn)) {
1627
+ turnTopics.set(currentTurn, pendingTopic);
1628
+ pendingTopic = "";
1629
+ }
1630
+ } else if (ev.type === "user/message" && d.source?.kind === "user") {
1631
+ const block = (Array.isArray(d.content) ? d.content : []).find((b) => b?.type === "text" && typeof b.text === "string");
1632
+ const text = String(block?.text ?? "").replace(/\s+/g, " ").trim();
1633
+ if (text) {
1634
+ if (currentTurn > 0 && !turnTopics.has(currentTurn)) turnTopics.set(currentTurn, text);
1635
+ else pendingTopic = text;
1636
+ }
1637
+ } else if (ev.type === "request/header") {
1638
+ const e = d.header?.config?.reasoningEffort;
1639
+ if (typeof e === "string" && e) {
1640
+ effort = e;
1641
+ for (const x of records) if (x.status === "running") x.effort = e;
1642
+ }
1643
+ }
1644
+ let rec;
1645
+ if (ev.type === "step/start" || ev.type === "llm/retry-started") {
1646
+ const key = `${turn}:${step}`;
1647
+ if (ev.type === "llm/retry-started" || !byKey.has(key)) {
1648
+ const previous = byKey.get(key);
1649
+ if (previous && ev.type === "llm/retry-started" && previous.status === "running") {
1650
+ previous.status = "failed";
1651
+ previous.finishedAt = ev.time;
1652
+ }
1653
+ rec = {
1654
+ id: `${id}:${turn}:${step}:${ordinal++}`,
1655
+ sessionId: id,
1656
+ sessionName: name,
1657
+ turn,
1658
+ step,
1659
+ startedAt: ev.time,
1660
+ finishedAt: null,
1661
+ provider: r.provider || route.provider || "",
1662
+ model: r.model || route.model || "",
1663
+ status: "running",
1664
+ totals: zero(),
1665
+ firstTokenMs: null,
1666
+ tokensPerSecond: null,
1667
+ turnName: "",
1668
+ effort,
1669
+ _attempt: ev.type === "llm/retry-started" ? 1 : 0,
1670
+ _lastUsage: zero()
1671
+ };
1672
+ records.push(rec);
1673
+ byKey.set(key, rec);
1674
+ }
1675
+ } else if (ev.type === "assistant/chunk" || ev.type === "assistant/message" || ev.type === "assistant/attempt") {
1676
+ rec = byKey.get(`${turn}:${step}`);
1677
+ if (!rec) {
1678
+ rec = {
1679
+ id: `${id}:${turn}:${step}:${ordinal++}`,
1680
+ sessionId: id,
1681
+ sessionName: name,
1682
+ turn,
1683
+ step,
1684
+ startedAt: ev.time,
1685
+ finishedAt: null,
1686
+ provider: r.provider || route.provider || "",
1687
+ model: r.model || route.model || "",
1688
+ status: "running",
1689
+ totals: zero(),
1690
+ firstTokenMs: null,
1691
+ tokensPerSecond: null,
1692
+ turnName: "",
1693
+ effort,
1694
+ _attempt: 0,
1695
+ _lastUsage: zero()
1696
+ };
1697
+ records.push(rec);
1698
+ byKey.set(`${turn}:${step}`, rec);
1699
+ }
1700
+ if (r.provider) rec.provider = r.provider;
1701
+ if (r.model) rec.model = r.model;
1702
+ const su = streamUsage(d);
1703
+ const u = ev.type === "assistant/message" ? d.usage ? usageOf(d) : su.usage : ev.type === "assistant/chunk" ? d.chunk?.type === "usage" ? usageOf(d.chunk) : void 0 : d.usage ? usageOf(d) : su.usage;
1704
+ if (u) {
1705
+ rec._lastUsage = u;
1706
+ rec.totals = u;
1707
+ if (positive(u) && rec.firstTokenMs === null) {
1708
+ const rawFirst = su.firstTime ?? (typeof d.chunk?.time === "number" ? d.chunk.time : ev.time);
1709
+ rec.firstTokenMs = Math.max(0, rawFirst - rec.startedAt);
1710
+ }
1711
+ }
1712
+ if (ev.type !== "assistant/chunk") {
1713
+ rec.finishedAt = ev.time;
1714
+ rec.status = ev.type === "assistant/attempt" ? "failed" : "completed";
1715
+ }
1716
+ const elapsedMs = rec.finishedAt === null ? null : Math.max(0, rec.finishedAt - rec.startedAt);
1717
+ if (elapsedMs !== null && elapsedMs > 0 && rec.totals.outputTokens > 0) rec.tokensPerSecond = rec.totals.outputTokens / (elapsedMs / 1e3);
1718
+ } else if (ev.type === "turn/end") {
1719
+ const recs = records.filter((x) => x.turn === turn && x.status === "running");
1720
+ for (const x of recs) {
1721
+ x.status = "interrupted";
1722
+ x.finishedAt = ev.time;
1723
+ }
1724
+ } else if (ev.type === "compaction/summary") {
1725
+ const u = usageOf(d);
1726
+ rec = {
1727
+ id: `${id}:compaction:${ordinal++}`,
1728
+ sessionId: id,
1729
+ sessionName: name,
1730
+ turn: 0,
1731
+ step: 0,
1732
+ startedAt: ev.time,
1733
+ finishedAt: ev.time,
1734
+ provider: typeof d.provider === "string" ? d.provider : "",
1735
+ model: typeof d.model === "string" ? d.model : "",
1736
+ status: "completed",
1737
+ totals: u,
1738
+ firstTokenMs: null,
1739
+ tokensPerSecond: null,
1740
+ turnName: "",
1741
+ effort: "",
1742
+ _attempt: 0,
1743
+ _lastUsage: u
1744
+ };
1745
+ records.push(rec);
1746
+ }
1747
+ const sample = fold.fold(ev);
1748
+ if (sample && !sample.turn) add$1(totals, {
1749
+ uncachedInputTokens: sample.inputTokens,
1750
+ outputTokens: sample.outputTokens,
1751
+ cacheReadTokens: sample.cacheReadTokens,
1752
+ cacheWriteTokens: sample.cacheWriteTokens
1753
+ });
1754
+ }
1755
+ const requests = records.length;
1756
+ const firsts = records.map((x) => x.firstTokenMs).filter((x) => x !== null);
1757
+ const outputSeconds = records.reduce((n, x) => n + x.totals.outputTokens, 0);
1758
+ const elapsed = records.reduce((n, x) => n + (x.firstTokenMs ?? 0), 0);
1759
+ for (const x of records) {
1760
+ x.turnName = turnTopics.get(x.turn) ?? "";
1761
+ delete x._attempt;
1762
+ delete x._lastUsage;
1763
+ }
1764
+ return {
1765
+ id,
1766
+ name,
1767
+ header,
1768
+ totals,
1769
+ records,
1770
+ requests,
1771
+ averageFirstTokenMs: firsts.length ? firsts.reduce((a, b) => a + b, 0) / firsts.length : null,
1772
+ tokensPerSecond: elapsed > 0 ? outputSeconds / (elapsed / 1e3) : null
1773
+ };
1774
+ }
1775
+ async function buildSessionUsageDetail(persistence, sessions, rootId, ctx) {
1776
+ const headers = (await persistence.list(typeof persistence.open === "function" ? void 0 : void 0)).map(headerOf$1).filter((x) => x !== null);
1777
+ const live = new Map((sessions?.list() ?? []).map((x) => [x.id, x]));
1778
+ const children = /* @__PURE__ */ new Map();
1779
+ for (const h of headers) if (h.parentSession) children.set(h.parentSession, [...children.get(h.parentSession) ?? [], h.id]);
1780
+ for (const h of live.values()) {
1781
+ const p = h.header?.parentSession ?? h.parentSession;
1782
+ if (p) children.set(p, [...(children.get(p) ?? []).filter((x) => x !== h.id), h.id]);
1783
+ }
1784
+ const ids = [];
1785
+ const seen = /* @__PURE__ */ new Set([rootId]);
1786
+ const q = [rootId];
1787
+ while (q.length) for (const c of children.get(q.shift()) ?? []) if (!seen.has(c)) {
1788
+ seen.add(c);
1789
+ ids.push(c);
1790
+ q.push(c);
1791
+ }
1792
+ const all = [rootId, ...ids];
1793
+ const parts = await Promise.all(all.map((id) => one(persistence, id, headers.find((h) => h.id === id), live.get(id))));
1794
+ const root = parts[0];
1795
+ const totals = zero();
1796
+ const models = /* @__PURE__ */ new Map();
1797
+ const records = parts.flatMap((x) => x.records);
1798
+ for (const p of parts) add$1(totals, p.totals);
1799
+ for (const r of records) {
1800
+ const key = r.model || "(unknown)";
1801
+ const m = models.get(key) ?? {
1802
+ totals: zero(),
1803
+ requests: 0,
1804
+ providers: /* @__PURE__ */ new Set()
1805
+ };
1806
+ add$1(m.totals, r.totals);
1807
+ m.requests++;
1808
+ if (r.provider) m.providers.add(r.provider);
1809
+ models.set(key, m);
1810
+ }
1811
+ const subagents = parts.slice(1).map((p) => ({
1812
+ sessionId: p.id,
1813
+ name: p.name,
1814
+ totals: p.totals,
1815
+ requests: p.requests,
1816
+ averageFirstTokenMs: p.averageFirstTokenMs,
1817
+ tokensPerSecond: p.tokensPerSecond
1818
+ }));
1819
+ const routeMap = /* @__PURE__ */ new Map();
1820
+ for (const r of records) if (r.provider || r.model) {
1821
+ const id = `${r.provider}/${r.model}`;
1822
+ routeMap.set(id, {
1823
+ id,
1824
+ provider: r.provider,
1825
+ name: r.provider,
1826
+ model: r.model,
1827
+ status: r.status === "running" ? "running" : r.status === "failed" ? "failed" : "completed"
1828
+ });
1829
+ }
1830
+ const llm = ctx?.get?.("llm");
1831
+ for (const x of llm?.listProviders?.() ?? []) if (x?.id && !routeMap.has(x.id)) routeMap.set(x.id, {
1832
+ id: x.id,
1833
+ provider: x.id,
1834
+ name: x.name || x.id,
1835
+ model: "",
1836
+ status: "idle"
1837
+ });
1838
+ const providerNames = resolveProviderNames(llm);
1839
+ const refOf = (provider, model) => provider ? `${provider}/${model}` : model;
1840
+ const refs = [];
1841
+ for (const [model, m] of models) if (model !== "(unknown)") {
1842
+ if (m.providers.size) for (const provider of m.providers) refs.push(refOf(provider, model));
1843
+ else refs.push(refOf("", model));
1844
+ }
1845
+ const names = await resolveModelNames(refs, llm);
1846
+ const displayNameOf = (model, providers) => {
1847
+ const keys = providers.size ? [...providers].map((provider) => refOf(provider, model)) : [refOf("", model)];
1848
+ for (const key of keys) {
1849
+ const resolved = names.get(key)?.trim();
1850
+ if (resolved) return resolved;
1851
+ }
1852
+ };
1853
+ return {
1854
+ sessionId: rootId,
1855
+ totals,
1856
+ requests: parts.reduce((n, p) => n + p.requests, 0),
1857
+ averageFirstTokenMs: root.averageFirstTokenMs,
1858
+ tokensPerSecond: root.tokensPerSecond,
1859
+ subagents,
1860
+ models: [...models.entries()].map(([model, x]) => {
1861
+ const name = displayNameOf(model, x.providers);
1862
+ return {
1863
+ model,
1864
+ ...name ? { name } : {},
1865
+ providers: [...x.providers].map((provider) => providerNames[provider] || provider),
1866
+ totals: x.totals,
1867
+ requests: x.requests
1868
+ };
1869
+ }),
1870
+ records,
1871
+ routes: [...routeMap.values()]
1872
+ };
1873
+ }
1874
+ //#endregion
1498
1875
  //#region src/wire.ts
1499
1876
  /** Default cap for a JSON request body. The panel posts tiny objects
1500
1877
  * ({ range, from?, to? }); the cap only bounds a misbehaving trusted
@@ -1832,6 +2209,16 @@ function buildUsageRoute(ctx, deps) {
1832
2209
  });
1833
2210
  return;
1834
2211
  }
2212
+ if (method === "POST" && path === "/token-total/api/session-detail") {
2213
+ const body = await readJsonBody(req);
2214
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
2215
+ if (sessionId === "" || sessionId.length > 200) throw new UsageError(400, "token total: session needs a valid sessionId");
2216
+ writeJson(res, {
2217
+ ok: true,
2218
+ value: await buildSessionUsageDetail(deps.persistence(), deps.sessions(), sessionId, ctx)
2219
+ });
2220
+ return;
2221
+ }
1835
2222
  if (method === "POST" && path === "/token-total/api/status") {
1836
2223
  writeJson(res, {
1837
2224
  ok: true,
@@ -0,0 +1,14 @@
1
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { SessionUsageDetail } from '../wire.ts';
3
+ import { SESSION_USAGE_NS, type SessionUsageKey } from './session-usage-locales.ts';
4
+ type T = (key: SessionUsageKey) => string;
5
+ export type SessionUsageViewProps = PropsLocale<typeof SESSION_USAGE_NS> & {
6
+ sessionId: string;
7
+ };
8
+ /** Pure presentation export also used by offline visual regression fixtures. */
9
+ export declare function SessionUsageContent({ detail, t }: {
10
+ detail: SessionUsageDetail;
11
+ t: T;
12
+ }): JSX.Element;
13
+ export declare function SessionUsageView({ sessionId, t }: SessionUsageViewProps): JSX.Element;
14
+ export {};
@@ -11,7 +11,7 @@
11
11
  * prefix-stripping proxy; the host's own route key stays absolute
12
12
  * (`/token-total/api` in src/routes.ts), because only the browser half is relative.
13
13
  */
14
- import type { BackfillStatus, SessionUsageTree, TokenTotalRange, TokenTotalRequest } from '../wire.ts';
14
+ import type { BackfillStatus, SessionUsageDetail, SessionUsageTree, TokenTotalRange, TokenTotalRequest } from '../wire.ts';
15
15
  /** One wire failure. */
16
16
  export declare class UsageApiError extends Error {
17
17
  readonly code: string;
@@ -29,3 +29,5 @@ export declare function rebuildStats(): Promise<BackfillStatus>;
29
29
  * covers only the main agent, so this is what makes a delegating session's
30
30
  * cost visible. */
31
31
  export declare function fetchSessionUsage(sessionId: string): Promise<SessionUsageTree>;
32
+ /** Log-owned requests and live route states for the per-session Usage view. */
33
+ export declare function fetchSessionDetail(sessionId: string, signal?: AbortSignal): Promise<SessionUsageDetail>;
@@ -7,7 +7,7 @@
7
7
  * from the shared zero-dependency module so the host and client halves share
8
8
  * one implementation.
9
9
  */
10
- export { providerOf, modelNameOf, daysInRange as daysBetween } from '../shared.ts';
10
+ export { providerOf, modelNameOf, normalizeModelName, daysInRange as daysBetween } from '../shared.ts';
11
11
  /** Exact token counts with thousands separators — no 万/亿/k/M units, the
12
12
  * user reads the precise number (long values shrink to fit via FitText).
13
13
  * Compact units stay on the axis labels and card sub-values only. */
@@ -21,6 +21,7 @@
21
21
  import type { Context, UsagePluginConfigOwnerProps } from '../context-types.ts';
22
22
  import type { PropsLocale, PropsRuntime, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
23
23
  import { LOCALE_NS, type TokenTotalKey } from './locales.ts';
24
+ import { type SessionUsageKey } from './session-usage-locales.ts';
24
25
  /** This bundle's npm package name — the key the Plugins page matches on. */
25
26
  export declare const BUNDLE_NAME = "dsh-token-total";
26
27
  /** The id shared by the sidebar row and the main panel it selects. */
@@ -36,6 +37,7 @@ export type TokenTotalPanelPageProps = PropsLocale<typeof LOCALE_NS> & {
36
37
  declare module '@deepseek-ai/dsh-client-ui-slots' {
37
38
  interface LocaleNamespaceMap {
38
39
  'tokenTotal': TokenTotalKey;
40
+ 'sessionUsage': SessionUsageKey;
39
41
  }
40
42
  }
41
43
  export declare const inject: string[];
@@ -0,0 +1,62 @@
1
+ /** Session Usage view translations, independent of the global dashboard. */
2
+ export declare const SESSION_USAGE_NS = "sessionUsage";
3
+ export declare const usageZh: {
4
+ tab: string;
5
+ requests: string;
6
+ tokens: string;
7
+ hit: string;
8
+ first: string;
9
+ speed: string;
10
+ input: string;
11
+ output: string;
12
+ read: string;
13
+ write: string;
14
+ requestsCount: string;
15
+ inputFull: string;
16
+ outputFull: string;
17
+ readFull: string;
18
+ writeFull: string;
19
+ cacheHit: string;
20
+ compaction: string;
21
+ reqTimes: string;
22
+ children: string;
23
+ session: string;
24
+ models: string;
25
+ model: string;
26
+ records: string;
27
+ expand: string;
28
+ collapse: string;
29
+ loading: string;
30
+ empty: string;
31
+ noChildren: string;
32
+ noModels: string;
33
+ scope: string;
34
+ note: string;
35
+ failedLoad: string;
36
+ retry: string;
37
+ completed: string;
38
+ running: string;
39
+ failed: string;
40
+ interrupted: string;
41
+ main: string;
42
+ turn: string;
43
+ step: string;
44
+ started: string;
45
+ duration: string;
46
+ firstDetail: string;
47
+ provider: string;
48
+ status: string;
49
+ exact: string;
50
+ answering: string;
51
+ firstWait: string;
52
+ sec: string;
53
+ tokenWord: string;
54
+ perSec: string;
55
+ cacheShort: string;
56
+ avgCache: string;
57
+ avgRate: string;
58
+ inProgress: string;
59
+ };
60
+ export type SessionUsageKey = keyof typeof usageZh;
61
+ export declare const usageEn: Record<SessionUsageKey, string>;
62
+ export declare const usageZhTW: Record<SessionUsageKey, string>;
@@ -0,0 +1,8 @@
1
+ import type { SessionUsageDetail } from '../wire.ts';
2
+ export declare const DETAIL_POLL_MS = 1500;
3
+ export declare function useSessionDetail(sessionId: string): {
4
+ detail: SessionUsageDetail | null;
5
+ loading: boolean;
6
+ error: boolean;
7
+ retry: () => void;
8
+ };
@@ -15,7 +15,7 @@ import type { UsageWebRoute } from './context-types.ts';
15
15
  import type { UsageStore } from './store.ts';
16
16
  import type { UsageCollector } from './collector.ts';
17
17
  import type { Granularity } from './query.ts';
18
- import type { BackfillStatus, SessionUsageRequest, TokenTotalRange, TokenTotalRequest } from './wire.ts';
18
+ import type { BackfillStatus, SessionUsageDetail, SessionUsageRequest, TokenTotalRange, TokenTotalRequest } from './wire.ts';
19
19
  import type { SessionUsageService } from './session-usage.ts';
20
20
  import { type TrustInputs } from './trust-fence.ts';
21
21
  export interface RoutesDeps {
@@ -55,4 +55,4 @@ export declare function aggregateRange(store: UsageStore, from: string, to: stri
55
55
  * The host mounts it via `ctx.webServer.register` (which takes ONE WebRoute,
56
56
  * not an array 鈥?matching the real dsh-host-webserver contract). */
57
57
  export declare function buildUsageRoute(ctx: Context, deps: RoutesDeps): UsageWebRoute;
58
- export type { BackfillStatus, TokenTotalRange, TokenTotalRequest, SessionUsageRequest };
58
+ export type { BackfillStatus, TokenTotalRange, TokenTotalRequest, SessionUsageRequest, SessionUsageDetail };
@@ -0,0 +1,3 @@
1
+ import type { Context, UsageSessionPersistence, UsageSessionStore } from './context-types.ts';
2
+ import type { SessionUsageDetail } from './wire.ts';
3
+ export declare function buildSessionUsageDetail(persistence: UsageSessionPersistence, sessions: UsageSessionStore | undefined, rootId: string, ctx?: Context): Promise<SessionUsageDetail>;
@@ -129,6 +129,61 @@ export interface SessionUsageTree {
129
129
  export interface SessionUsageRequest {
130
130
  sessionId: string;
131
131
  }
132
+ /** Detailed per-session usage view used by the Usage tab. */
133
+ export interface SessionUsageDetail {
134
+ sessionId: string;
135
+ totals: SessionUsageTotals;
136
+ requests: number;
137
+ averageFirstTokenMs: number | null;
138
+ tokensPerSecond: number | null;
139
+ subagents: Array<{
140
+ sessionId: string;
141
+ name: string;
142
+ totals: SessionUsageTotals;
143
+ requests: number;
144
+ averageFirstTokenMs: number | null;
145
+ tokensPerSecond: number | null;
146
+ }>;
147
+ models: Array<{
148
+ /** Raw model id from the historical route. Stays the row key and the
149
+ * fallback label (also the tooltip) when no display name is resolved. */
150
+ model: string;
151
+ /** The model selector's display name for this model, resolved host-side
152
+ * from the llm service metadata. Optional so an older host (or an
153
+ * unresolvable route) still serves a valid payload. */
154
+ name?: string;
155
+ /** Display names (raw ids when unknown) of every provider that served this
156
+ * model in the session, first-seen order. The by-model card shows these in
157
+ * gray when two rows end up sharing one display name. */
158
+ providers?: string[];
159
+ totals: SessionUsageTotals;
160
+ requests: number;
161
+ }>;
162
+ records: Array<{
163
+ id: string;
164
+ sessionId: string;
165
+ sessionName: string;
166
+ turn: number;
167
+ step: number;
168
+ startedAt: number;
169
+ finishedAt: number | null;
170
+ provider: string;
171
+ model: string;
172
+ status: 'running' | 'completed' | 'failed' | 'interrupted';
173
+ totals: SessionUsageTotals;
174
+ firstTokenMs: number | null;
175
+ tokensPerSecond: number | null;
176
+ turnName: string;
177
+ effort: string;
178
+ }>;
179
+ routes: Array<{
180
+ id: string;
181
+ provider: string;
182
+ name: string;
183
+ model: string;
184
+ status: 'idle' | 'running' | 'completed' | 'failed';
185
+ }>;
186
+ }
132
187
  /** The backfill (historical session scan) progress state. */
133
188
  export interface BackfillStatus {
134
189
  running: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-token-total",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "DSH web plugin: token 用量统计看板 + 单会话(主智能体 + 子智能体)用量统计。提供今日/近3天/近7天/近1月/近3月/自定义筛选、六个汇总卡片、活跃热力图、缓存命中率折线与分模型柱状图、按模型/按供应商 Top5 列表。",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",