dsh-token-total 1.0.1 → 1.0.3

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/client.js CHANGED
@@ -5,9 +5,30 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
14
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
17
+ key = keys[i];
18
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
19
+ get: ((k) => from[k]).bind(null, key),
20
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
+ });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
26
+ value: mod,
27
+ enumerable: true
28
+ }) : target, mod));
9
29
  //#endregion
10
30
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
+ _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
11
32
  let react = require("react");
12
33
  let react_dom = require("react-dom");
13
34
  let react_jsx_runtime = require("react/jsx-runtime");
@@ -29406,13 +29427,13 @@ window.__ModuleLoader__.load({
29406
29427
  }
29407
29428
  //#endregion
29408
29429
  //#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/TokenTotalPanel.module.css?inline
29409
- const css$1 = "/**\n * Usage statistics panel styles — a DSH-native CSS Module. Every visual\n * value rides the DSH semantic tokens (--dsw-alias-* colors, --dsw-font-*\n * typography, --ds-* motion); no static palette values, no theme selectors.\n * The heatmap's 5 levels come from the brand accent via color-mix; model\n * colours are the fixed --dsw-chart-1..5 series (rank order by token\n * volume) plus the gray --dsw-chart-other step — the reasonix token-total\n * palette, with a lifted dark variant defined below.\n */\n\n/* The panel as a standalone main panel. It gets the same 960px content column\n the Plugins page gives it — the declarations below mirror the Plugins page's\n own `.page_768838850` so both entries render identically. Without the cap the charts\n would stretch to whatever the centre column happens to be.\n\n One deliberate difference from that page: its 28px top inset is NOT part of\n this container. A main panel starts at the window's top-left corner, so an\n inset painted by this scrolling container is a strip above the content that\n no window-drag region covers; `.backRow_768838850` below owns the inset instead, which\n is the shape the shipped page plugins use for the same reason\n (`ui-plugin-manager`'s `.pageHead_768838850`/`.detailTop_768838850` and `ui-schedule`'s page\n head). The rendered position is unchanged. */\n.page_768838850 {\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n height: 100%;\n overflow: auto;\n padding: 0 clamp(24px, 4vw, 48px) 48px;\n color: var(--dsw-alias-label-primary);\n}\n\n.page_768838850 > * {\n width: 100%;\n max-width: 960px;\n}\n\n/* The standalone panel's back control. Only the main panel renders this seat —\n the Plugins page entry already sits inside the page's own chrome, which\n draws its own crumb back to the bundle list. The spacing matches the gap the\n panel keeps between its own sections.\n\n The row also carries the panel's former 28px top inset: it starts at the\n window's top edge, so the whole strip above the back control drags the\n window (Electron app-regions are geometry, not stacking, so a strip owned by\n the scrolling container would subtract itself from the drag row beneath\n it). */\n.backRow_768838850 {\n display: flex;\n box-sizing: border-box;\n padding-top: 28px;\n margin-bottom: 18px;\n}\n\n.back_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.panel_768838850 {\n display: flex;\n flex-direction: column;\n gap: 18px;\n min-width: 0;\n}\n\n/* ── toolbar ───────────────────────────────────────────────────────────── */\n\n.toolbar_768838850 {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\n/* Each picker (time range) lives in a framed group. */\n.group_768838850 {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 2px;\n padding: 3px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n/* A range-preset chip: ghost Button inside the group, active state tinted\n with the interactive-active fill. */\n.segItem_768838850 {\n border-radius: 6px;\n}\n\n.segItem_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.segActive_768838850,\n.segActive_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active);\n color: var(--dsw-alias-label-primary);\n}\n\n.customRange_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.dateInput_768838850 {\n width: 150px;\n flex: 0 0 auto;\n}\n\n.customSep_768838850 {\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Keep the remaining toolbar action at the trailing edge. */\n.refresh_768838850 {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n/* ── states ────────────────────────────────────────────────────────────── */\n\n.errorBanner_768838850 {\n padding: 10px 14px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n color: var(--dsw-alias-state-warn-label);\n font-size: 13px;\n}\n\n.loading_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n}\n\n.empty_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 8px;\n}\n\n/* ── numeric cards ─────────────────────────────────────────────────────── */\n\n/* The two long-valued cards bookend the row — total tokens leads it, the top\n model closes it — each on a wider track than the four short numerics\n between them. At the Plugins page's 960px content width that is six columns\n on one row. */\n.cards_768838850 {\n display: grid;\n grid-template-columns: 1.3fr repeat(4, 1fr) 1.3fr;\n gap: 10px;\n}\n\n/* Below the width where six cards hold their own, the grid folds to four\n tracks and the two long cards take two each: they keep their extra width\n instead of degrading to the numerics' size, and each row still fills. */\n@media (max-width: 720px) {\n .cards_768838850 {\n grid-template-columns: repeat(4, 1fr);\n }\n\n .cards_768838850 > :first-child,\n .cards_768838850 > :last-child {\n grid-column: span 2;\n }\n}\n\n@media (max-width: 430px) {\n .cards_768838850 {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n.card_768838850 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n}\n\n.cardHead_768838850 {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.cardIcon_768838850 {\n color: var(--dsw-alias-label-tertiary);\n flex: 0 0 auto;\n}\n\n.cardLabel_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.cardValue_768838850 {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n line-height: 1.15;\n}\n\n.cardValueSm_768838850 {\n font-size: 14px;\n}\n\n.cardValueWrap_768838850 {\n word-break: break-word;\n}\n\n/* The \"most used model\" card value: the model name over the provider, the\n same two-line anatomy as the per-model list rows (modelName/modelProvider\n classes), instead of the raw \"provider/model\" ref. */\n.cardModelLines_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n/* A muted second line under a card value (the absolute cached-token volume\n under the hit-rate percentage). */\n.cardSub_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 1.2;\n}\n\n/* ── sections / chart chrome ───────────────────────────────────────────── */\n\n.section_768838850 {\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n}\n\n.sectionHead_768838850 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.sectionTitle_768838850 {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.trendNote_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.chartWrap_768838850 {\n position: relative;\n min-width: 0;\n}\n\n/* ── heatmap ───────────────────────────────────────────────────────────── */\n\n.heatWrap_768838850 {\n position: relative;\n /* hidden, not auto: the SVG now fills the container exactly, and an\n auto scrollbar would flash when a fixed tooltip passes over the wrap's\n right edge. */\n overflow: hidden;\n min-width: 0;\n}\n\n.heatmap_768838850 {\n display: block;\n /* The 364 cells each keep their own two-layer drop-shadow chain (see\n `.heatCell_768838850`) — 728 blurs that the compositor would otherwise re-run on\n every scroll repaint of the tile, which is what made scrolling this panel\n drop frames while idle was clean (#4). Promoting the SVG onto its own\n compositing layer takes that rasterization off the scroll path and leaves\n the painted result untouched, so the cell depth stays as designed. Note\n that the promotion is what does the work: `contain: paint` on its own\n measured no better than no mitigation at all. */\n transform: translateZ(0);\n}\n\n.heatLegend_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.heatCell_768838850 {\n display: inline-block;\n border-radius: 3px;\n /* No width/height here: the SVG cells carry their size as geometry\n attributes (x/y/width/height), and a CSS width/height would override\n those on SVG2 rects — pinning every cell to a fixed px and widening the\n visual gap. The legend swatches carry the same size inline, so both\n always match the measured geom.size_768838850. */\n /* Depth at this size, WITHOUT a per-cell backdrop-filter: the cells are SVG\n geometry, where backdrop-filter is accepted by the CSSOM but changes\n nothing on paint (measured on 364 live cells: the screenshot came out\n byte-identical to no blur at all). What reads as a glass tile instead is\n the translucent fill below — it lets the panel's own glass bed through —\n carried by one hairline highlight over one soft under-shadow. */\n filter:\n drop-shadow(0 -0.5px 0 var(--dsw-heat-sheen))\n drop-shadow(0 0.5px 0.8px var(--dsw-heat-shade));\n}\n\n/* The five heat levels use DeepSeek's brand blue (#4D6BFE) as a fixed ramp,\n light and dark variants below — never the theme accent, which can read\n near-black against the base surface in dark mode. level0 (no data) is a\n neutral empty cell. The cells are SVG <rect>s, so the color rides `fill`\n (background is a no-op on SVG geometry) — but the legend swatches are HTML,\n so both declarations stay in step.\n Each level is mixed a little toward transparent so the bed shows through;\n the amount is deliberately modest, because the ramp is a DATA encoding and\n washing it out would cost readability. */\n.heatLevel0_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n}\n\n.heatLevel1_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n}\n\n.heatLevel2_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n}\n\n.heatLevel3_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n}\n\n.heatLevel4_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n}\n\n.heatLevel5_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n}\n\n/* ── daily trend ───────────────────────────────────────────────────────── */\n\n.chart_768838850 {\n display: block;\n}\n\n.grid_768838850 {\n stroke: var(--dsw-alias-border-l1);\n stroke-width: 1;\n}\n\n.axis_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 10px;\n}\n\n.axisRate_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n}\n\n.bar_768838850 {\n transition: opacity 0.12s ease;\n /* The hover scaleX must pivot on the bar's own centre: SVG geometry\n transforms around the SVG origin unless transform-box anchors it to the\n element's fill box. Without this the scaled bar slides sideways and\n looks like it vanishes. */\n transform-box: fill-box;\n transform-origin: center;\n}\n\n.barDim_768838850 {\n opacity: 0.35;\n}\n\n.barHit_768838850 {\n fill: transparent;\n pointer-events: all;\n}\n\n/* cache hit-rate trend curve: DeepSeek's brand blue (#4D6BFE, the same hue\n family as the heatmap ramp) — a fixed brand ramp instead of the theme\n accent, which can read near-black in some themes. */\n.trend_768838850 {\n stroke: var(--dsw-trend-line);\n stroke-width: 2;\n opacity: 0.9;\n pointer-events: none;\n}\n\n.trendDot_768838850 {\n fill: var(--dsw-trend-line);\n stroke: var(--dsw-alias-bg-layer-1);\n stroke-width: 2;\n pointer-events: none;\n}\n\n.legend_768838850 {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.legendItem_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n cursor: default;\n}\n\n.legendSwatch_768838850 {\n display: inline-block;\n width: 10px;\n height: 10px;\n border-radius: 2px;\n}\n\n.legendTrend_768838850 {\n background: var(--dsw-trend-line);\n width: 14px;\n height: 3px;\n border-radius: 2px;\n}\n\n/* ── per-model / per-provider chart + list ─────────────────────────────── */\n\n/* The ring is centred against the list beside it. The list's height is never\n tied to the chart, so expanding a row cannot stretch anything. */\n.models_768838850 {\n display: flex;\n gap: 16px;\n align-items: center;\n flex-wrap: wrap;\n min-width: 0;\n}\n\n/* The ring's own wrapper. Its diameter comes from the measured row width (see\n Donut.tsx_768838850), so it must never flex. */\n.donutWrap_768838850 {\n position: relative;\n flex: 0 0 auto;\n}\n\n.donut_768838850 {\n display: block;\n}\n\n.donutTrack_768838850 {\n stroke: var(--dsw-alias-bg-mask-1);\n}\n\n.donutSeg_768838850 {\n transition: stroke-width 0.12s ease, opacity 0.12s ease;\n}\n\n.donutDim_768838850 {\n opacity: 0.35;\n}\n\n.donutCenter_768838850 {\n fill: var(--dsw-alias-label-primary);\n font-size: 18px;\n font-weight: 600;\n}\n\n.donutLabel_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n/* Two ranking cards, side by side: MODELS left, PROVIDERS right. Both are\n always on screen (no lens/tab switch), so they share one row grid and\n stretch to a common height; a narrow pane stacks them instead. */\n.rankCards_768838850 {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n}\n\n@media (max-width: 720px) {\n .rankCards_768838850 {\n grid-template-columns: minmax(0, 1fr);\n }\n}\n\n/* Each card is the bordered surface its rows live in; the title is a recessed\n header strip, so the two rankings read as two equal panels. */\n.rankCard_768838850 {\n display: flex;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n.rankCardTitle_768838850 {\n padding: 8px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.rankList_768838850 {\n list-style: none;\n margin: 0;\n padding: 0;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Every row opens a hover tip (the cross-dimension detail), so it carries the\n same hover affordance as the chart marks. */\n.modelRow_768838850:hover {\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 60%, transparent);\n}\n\n/* Position column: the row's rank inside its section. The Other bucket holds\n no single rank, so it renders an empty (but equally wide) cell. */\n.modelRank_768838850 {\n flex: 0 0 auto;\n min-width: 14px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.modelRow_768838850 {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 13px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.modelRow_768838850:last-child {\n border-bottom: none;\n}\n\n/* \"Other\" aggregate row: the entire row is clickable to expand/collapse the\n detail list. The chevron button inside it stops propagation so\n double-firing is avoided. */\n.modelRowExpandable_768838850 {\n cursor: pointer;\n}\n\n/* \"Other\" row toggle (chevron) that expands the aggregated tail models. */\n.modelToggle_768838850 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-right: 4px;\n border: none;\n background: none;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n vertical-align: middle;\n}\n\n/* Expanded detail rows: indented, slightly recessed. */\n.modelRowSub_768838850 {\n padding-left: 28px;\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent);\n font-size: 13px;\n}\n\n/* Second-level indent: a model row inside a provider that is itself folded\n into the Other bucket. Wider than .modelRowSub_768838850 so the nesting stays\n readable. Declared after it — same specificity, so this one wins. */\n.modelRowDeep_768838850 {\n padding-left: 48px;\n}\n\n/* \"Other\" details expand with an accordion: the wrapper is a grid whose\n single track animates 0fr → 1fr. */\n.modelOtherWrap_768838850 {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 0.25s ease;\n}\n\n.modelOtherOpen_768838850 {\n grid-template-rows: 1fr;\n}\n\n.modelOtherList_768838850 {\n overflow: hidden;\n min-height: 0;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/* Two-line row anatomy: [swatch] [name / provider] ……… [tokens / share].\n The name block takes the free width; the values block hugs the right. */\n.modelId_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.modelValues_768838850 {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 1px;\n flex: 0 0 auto;\n}\n\n.modelName_768838850 {\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.modelProvider_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.modelTokens_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.modelPct_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* ── tooltips ──────────────────────────────────────────────────────────── */\n\n/* Tooltips render as fixed-position portals on document.body_768838850 (see ChartTip),\n so they escape every ancestor's clipping and never overflow the settings\n panel: ChartTip clamps each tip inside the panel bounds and flips it above\n the hovered element when there is no room below. The plate follows the\n active theme: the DSH overlay token is a light gray in light mode and a\n dark gray under body[data-ds-dark-theme], so the tip reads correctly in\n both instead of staying dark in light mode. */\n.tip_768838850 {\n position: fixed;\n /* The tip is portaled to document.body_768838850, so it competes with the settings\n modal itself (z-index 1000) for the top layer: 100 would sit under the\n modal and the tip would be invisible. 1100 matches the official\n toast/portaled-menu layer, which is exactly the \"over a modal\" tier. */\n z-index: 1100;\n pointer-events: none;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-overlay);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n box-shadow: 0 2px 12px var(--dsw-alias-bg-mask-2);\n white-space: nowrap;\n}\n\n.tipTitle_768838850 {\n font-weight: 600;\n margin-bottom: 4px;\n}\n\n/* A provider row's raw route id, kept out of the row label but reachable on\n hover (the row shows the display name). */\n.tipSubtitle_768838850 {\n margin: -2px 0 4px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n/* Breakdown heading inside a row tip: names the cross-dimension list under it\n (\"By provider\" under a model, \"By model\" under a provider). */\n.tipLabel_768838850 {\n margin-bottom: 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n/* The breakdown line pushes its volume to the right edge of the plate. */\n.tipName_768838850 {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.tipValue_768838850 {\n margin-left: auto;\n padding-left: 12px;\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.tipRow_768838850 {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n\n/* Hover tooltip breakdown rows for the \"Other\" aggregate: indented, muted,\n same gray swatch as the Other step. */\n.tipRowOther_768838850 {\n padding-left: 8px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.tipBreakdown_768838850 {\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 4px;\n padding-top: 4px;\n}\n\n.foot_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── chart palette ──────────────────────────────────────────────────────── */\n\n/* Light variant (the theme's default): model colours are the reasonix\n token-total palette (rank order by token volume + gray Other) with a\n white veil mixed in (color-mix 30% white) — the raw hues read dark and\n heavy on the light card surface, and the veil lifts lightness and dulls\n saturation the way a translucent white overlay would, keeping the hues\n colourful instead of muddy. The heatmap ramp is DeepSeek's brand blue\n (#4D6BFE) stepping from a pale tint to the full blue; the hit-rate curve\n rides the same brand hue.\n Ranks 6-10 extend the reasonix five to ten (this plugin's addition): each\n is picked to keep the ten hues apart on both themes. */\n.panel_768838850 {\n --dsw-heat-0: #ebedf0;\n --dsw-heat-1: #dbe3ff;\n --dsw-heat-2: #b7c5ff;\n --dsw-heat-3: #8ea4ff;\n --dsw-heat-4: #6884ff;\n --dsw-heat-5: #4d6bfe;\n /* Heatmap cell depth: a highlight above, a shade below. Light trays need\n the stronger pair; the dark tray below lifts the shade instead. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.55);\n --dsw-heat-shade: rgb(0 0 0 / 0.20);\n --dsw-trend-line: #4d6bfe;\n --dsw-chart-1: color-mix(in srgb, #0576ff 70%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 70%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 70%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 70%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 70%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 70%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 70%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 70%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 70%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 70%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 70%, white);\n /* Provider series: a separate five-hue set for the provider section, so a\n provider never wears a model's colour. */\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 70%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 70%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 70%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 70%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 70%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 70%, white);\n}\n\n/* Dark variant: the same reasonix hues under a thicker white veil\n (35% — dark surfaces need the extra lift), so segments read soft and\n bright instead of sinking into the background. */\n@media (prefers-color-scheme: dark) {\n .panel_768838850 {\n --dsw-heat-0: #21262d;\n --dsw-heat-1: #2f4bd0;\n --dsw-heat-2: #4d6bfe;\n --dsw-heat-3: #6e8bff;\n --dsw-heat-4: #93aaff;\n --dsw-heat-5: #c4d0ff;\n /* Same depth pair, recalibrated: a dark tray swallows a light shade. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.30);\n --dsw-heat-shade: rgb(0 0 0 / 0.50);\n --dsw-trend-line: #7d95ff;\n --dsw-chart-1: color-mix(in srgb, #0576ff 65%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 65%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 65%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 65%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 65%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 65%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 65%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 65%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 65%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 65%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 65%, white);\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 65%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 65%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 65%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 65%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 65%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 65%, white);\n }\n}\n";
29410
- const tagId$1 = "dsh-token-total/TokenTotalPanel.module.css";
29411
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$1 + "\"]") === null) {
29430
+ const css$2 = "/**\n * Usage statistics panel styles — a DSH-native CSS Module. Every visual\n * value rides the DSH semantic tokens (--dsw-alias-* colors, --dsw-font-*\n * typography, --ds-* motion); no static palette values, no theme selectors.\n * The heatmap's 5 levels come from the brand accent via color-mix; model\n * colours are the fixed --dsw-chart-1..5 series (rank order by token\n * volume) plus the gray --dsw-chart-other step — the reasonix token-total\n * palette, with a lifted dark variant defined below.\n */\n\n/* The panel as a standalone main panel. It gets the same 960px content column\n the Plugins page gives it — the declarations below mirror the Plugins page's\n own `.page_768838850` so both entries render identically. Without the cap the charts\n would stretch to whatever the centre column happens to be.\n\n One deliberate difference from that page: its 28px top inset is NOT part of\n this container. A main panel starts at the window's top-left corner, so an\n inset painted by this scrolling container is a strip above the content that\n no window-drag region covers; `.backRow_768838850` below owns the inset instead, which\n is the shape the shipped page plugins use for the same reason\n (`ui-plugin-manager`'s `.pageHead_768838850`/`.detailTop_768838850` and `ui-schedule`'s page\n head). The rendered position is unchanged. */\n.page_768838850 {\n display: flex;\n flex-direction: column;\n align-items: center;\n box-sizing: border-box;\n height: 100%;\n overflow: auto;\n padding: 0 clamp(24px, 4vw, 48px) 48px;\n color: var(--dsw-alias-label-primary);\n}\n\n.page_768838850 > * {\n width: 100%;\n max-width: 960px;\n}\n\n/* The standalone panel's back control. Only the main panel renders this seat —\n the Plugins page entry already sits inside the page's own chrome, which\n draws its own crumb back to the bundle list. The spacing matches the gap the\n panel keeps between its own sections.\n\n The row also carries the panel's former 28px top inset: it starts at the\n window's top edge, so the whole strip above the back control drags the\n window (Electron app-regions are geometry, not stacking, so a strip owned by\n the scrolling container would subtract itself from the drag row beneath\n it). */\n.backRow_768838850 {\n display: flex;\n box-sizing: border-box;\n padding-top: 28px;\n margin-bottom: 18px;\n}\n\n.back_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.panel_768838850 {\n display: flex;\n flex-direction: column;\n gap: 18px;\n min-width: 0;\n}\n\n/* ── toolbar ───────────────────────────────────────────────────────────── */\n\n.toolbar_768838850 {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 10px;\n}\n\n/* Each picker (time range) lives in a framed group. */\n.group_768838850 {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 2px;\n padding: 3px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n/* A range-preset chip: ghost Button inside the group, active state tinted\n with the interactive-active fill. */\n.segItem_768838850 {\n border-radius: 6px;\n}\n\n.segItem_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.segActive_768838850,\n.segActive_768838850:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active);\n color: var(--dsw-alias-label-primary);\n}\n\n.customRange_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.dateInput_768838850 {\n width: 150px;\n flex: 0 0 auto;\n}\n\n.customSep_768838850 {\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Keep the remaining toolbar action at the trailing edge. */\n.refresh_768838850 {\n flex: 0 0 auto;\n margin-left: auto;\n}\n\n/* ── states ────────────────────────────────────────────────────────────── */\n\n.errorBanner_768838850 {\n padding: 10px 14px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n color: var(--dsw-alias-state-warn-label);\n font-size: 13px;\n}\n\n.loading_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n}\n\n.empty_768838850 {\n padding: 24px;\n text-align: center;\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 8px;\n}\n\n/* ── numeric cards ─────────────────────────────────────────────────────── */\n\n/* The two long-valued cards bookend the row — total tokens leads it, the top\n model closes it — each on a wider track than the four short numerics\n between them. At the Plugins page's 960px content width that is six columns\n on one row. */\n.cards_768838850 {\n display: grid;\n grid-template-columns: 1.3fr repeat(4, 1fr) 1.3fr;\n gap: 10px;\n}\n\n/* Below the width where six cards hold their own, the grid folds to four\n tracks and the two long cards take two each: they keep their extra width\n instead of degrading to the numerics' size, and each row still fills. */\n@media (max-width: 720px) {\n .cards_768838850 {\n grid-template-columns: repeat(4, 1fr);\n }\n\n .cards_768838850 > :first-child,\n .cards_768838850 > :last-child {\n grid-column: span 2;\n }\n}\n\n@media (max-width: 430px) {\n .cards_768838850 {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n.card_768838850 {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n min-width: 0;\n}\n\n.cardHead_768838850 {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.cardIcon_768838850 {\n color: var(--dsw-alias-label-tertiary);\n flex: 0 0 auto;\n}\n\n.cardLabel_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.cardValue_768838850 {\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n line-height: 1.15;\n}\n\n.cardValueSm_768838850 {\n font-size: 14px;\n}\n\n.cardValueWrap_768838850 {\n word-break: break-word;\n}\n\n/* The \"most used model\" card value: the model name over the provider, the\n same two-line anatomy as the per-model list rows (modelName/modelProvider\n classes), instead of the raw \"provider/model\" ref. */\n.cardModelLines_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n}\n\n/* A muted second line under a card value (the absolute cached-token volume\n under the hit-rate percentage). */\n.cardSub_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n line-height: 1.2;\n}\n\n/* ── sections / chart chrome ───────────────────────────────────────────── */\n\n.section_768838850 {\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n}\n\n.sectionHead_768838850 {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 10px;\n flex-wrap: wrap;\n}\n\n.sectionTitle_768838850 {\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n\n.trendNote_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.chartWrap_768838850 {\n position: relative;\n min-width: 0;\n}\n\n/* ── heatmap ───────────────────────────────────────────────────────────── */\n\n.heatWrap_768838850 {\n position: relative;\n /* hidden, not auto: the SVG now fills the container exactly, and an\n auto scrollbar would flash when a fixed tooltip passes over the wrap's\n right edge. */\n overflow: hidden;\n min-width: 0;\n}\n\n.heatmap_768838850 {\n display: block;\n /* The 364 cells each keep their own two-layer drop-shadow chain (see\n `.heatCell_768838850`) — 728 blurs that the compositor would otherwise re-run on\n every scroll repaint of the tile, which is what made scrolling this panel\n drop frames while idle was clean (#4). Promoting the SVG onto its own\n compositing layer takes that rasterization off the scroll path and leaves\n the painted result untouched, so the cell depth stays as designed. Note\n that the promotion is what does the work: `contain: paint` on its own\n measured no better than no mitigation at all. */\n transform: translateZ(0);\n}\n\n.heatLegend_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.heatCell_768838850 {\n display: inline-block;\n border-radius: 3px;\n /* No width/height here: the SVG cells carry their size as geometry\n attributes (x/y/width/height), and a CSS width/height would override\n those on SVG2 rects — pinning every cell to a fixed px and widening the\n visual gap. The legend swatches carry the same size inline, so both\n always match the measured geom.size_768838850. */\n /* Depth at this size, WITHOUT a per-cell backdrop-filter: the cells are SVG\n geometry, where backdrop-filter is accepted by the CSSOM but changes\n nothing on paint (measured on 364 live cells: the screenshot came out\n byte-identical to no blur at all). What reads as a glass tile instead is\n the translucent fill below — it lets the panel's own glass bed through —\n carried by one hairline highlight over one soft under-shadow. */\n filter:\n drop-shadow(0 -0.5px 0 var(--dsw-heat-sheen))\n drop-shadow(0 0.5px 0.8px var(--dsw-heat-shade));\n}\n\n/* The five heat levels use DeepSeek's brand blue (#4D6BFE) as a fixed ramp,\n light and dark variants below — never the theme accent, which can read\n near-black against the base surface in dark mode. level0 (no data) is a\n neutral empty cell. The cells are SVG <rect>s, so the color rides `fill`\n (background is a no-op on SVG geometry) — but the legend swatches are HTML,\n so both declarations stay in step.\n Each level is mixed a little toward transparent so the bed shows through;\n the amount is deliberately modest, because the ramp is a DATA encoding and\n washing it out would cost readability. */\n.heatLevel0_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-0) 82%, transparent);\n}\n\n.heatLevel1_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-1) 82%, transparent);\n}\n\n.heatLevel2_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-2) 82%, transparent);\n}\n\n.heatLevel3_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-3) 82%, transparent);\n}\n\n.heatLevel4_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-4) 82%, transparent);\n}\n\n.heatLevel5_768838850 {\n fill: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n background: color-mix(in srgb, var(--dsw-heat-5) 82%, transparent);\n}\n\n/* ── daily trend ───────────────────────────────────────────────────────── */\n\n.chart_768838850 {\n display: block;\n}\n\n.grid_768838850 {\n stroke: var(--dsw-alias-border-l1);\n stroke-width: 1;\n}\n\n.axis_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 10px;\n}\n\n.axisRate_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n}\n\n.bar_768838850 {\n transition: opacity 0.12s ease;\n /* The hover scaleX must pivot on the bar's own centre: SVG geometry\n transforms around the SVG origin unless transform-box anchors it to the\n element's fill box. Without this the scaled bar slides sideways and\n looks like it vanishes. */\n transform-box: fill-box;\n transform-origin: center;\n}\n\n.barDim_768838850 {\n opacity: 0.35;\n}\n\n.barHit_768838850 {\n fill: transparent;\n pointer-events: all;\n}\n\n/* cache hit-rate trend curve: DeepSeek's brand blue (#4D6BFE, the same hue\n family as the heatmap ramp) — a fixed brand ramp instead of the theme\n accent, which can read near-black in some themes. */\n.trend_768838850 {\n stroke: var(--dsw-trend-line);\n stroke-width: 2;\n opacity: 0.9;\n pointer-events: none;\n}\n\n.trendDot_768838850 {\n fill: var(--dsw-trend-line);\n stroke: var(--dsw-alias-bg-layer-1);\n stroke-width: 2;\n pointer-events: none;\n}\n\n.legend_768838850 {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.legendItem_768838850 {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n cursor: default;\n}\n\n.legendSwatch_768838850 {\n display: inline-block;\n width: 10px;\n height: 10px;\n border-radius: 2px;\n}\n\n.legendTrend_768838850 {\n background: var(--dsw-trend-line);\n width: 14px;\n height: 3px;\n border-radius: 2px;\n}\n\n/* ── per-model / per-provider chart + list ─────────────────────────────── */\n\n/* The ring is centred against the list beside it. The list's height is never\n tied to the chart, so expanding a row cannot stretch anything. */\n.models_768838850 {\n display: flex;\n gap: 16px;\n align-items: center;\n flex-wrap: wrap;\n min-width: 0;\n}\n\n/* The ring's own wrapper. Its diameter comes from the measured row width (see\n Donut.tsx_768838850), so it must never flex. */\n.donutWrap_768838850 {\n position: relative;\n flex: 0 0 auto;\n}\n\n.donut_768838850 {\n display: block;\n}\n\n.donutTrack_768838850 {\n stroke: var(--dsw-alias-bg-mask-1);\n}\n\n.donutSeg_768838850 {\n transition: stroke-width 0.12s ease, opacity 0.12s ease;\n}\n\n.donutDim_768838850 {\n opacity: 0.35;\n}\n\n.donutCenter_768838850 {\n fill: var(--dsw-alias-label-primary);\n font-size: 18px;\n font-weight: 600;\n}\n\n.donutLabel_768838850 {\n fill: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n/* Two ranking cards, side by side: MODELS left, PROVIDERS right. Both are\n always on screen (no lens/tab switch), so they share one row grid and\n stretch to a common height; a narrow pane stacks them instead. */\n.rankCards_768838850 {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n}\n\n@media (max-width: 720px) {\n .rankCards_768838850 {\n grid-template-columns: minmax(0, 1fr);\n }\n}\n\n/* Each card is the bordered surface its rows live in; the title is a recessed\n header strip, so the two rankings read as two equal panels. */\n.rankCard_768838850 {\n display: flex;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n.rankCardTitle_768838850 {\n padding: 8px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.rankList_768838850 {\n list-style: none;\n margin: 0;\n padding: 0;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Every row opens a hover tip (the cross-dimension detail), so it carries the\n same hover affordance as the chart marks. */\n.modelRow_768838850:hover {\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 60%, transparent);\n}\n\n/* Position column: the row's rank inside its section. The Other bucket holds\n no single rank, so it renders an empty (but equally wide) cell. */\n.modelRank_768838850 {\n flex: 0 0 auto;\n min-width: 14px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.modelRow_768838850 {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 13px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.modelRow_768838850:last-child {\n border-bottom: none;\n}\n\n/* \"Other\" aggregate row: the entire row is clickable to expand/collapse the\n detail list. The chevron button inside it stops propagation so\n double-firing is avoided. */\n.modelRowExpandable_768838850 {\n cursor: pointer;\n}\n\n/* \"Other\" row toggle (chevron) that expands the aggregated tail models. */\n.modelToggle_768838850 {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-right: 4px;\n border: none;\n background: none;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n vertical-align: middle;\n}\n\n/* Expanded detail rows: indented, slightly recessed. */\n.modelRowSub_768838850 {\n padding-left: 28px;\n background: color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent);\n font-size: 13px;\n}\n\n/* Second-level indent: a model row inside a provider that is itself folded\n into the Other bucket. Wider than .modelRowSub_768838850 so the nesting stays\n readable. Declared after it — same specificity, so this one wins. */\n.modelRowDeep_768838850 {\n padding-left: 48px;\n}\n\n/* \"Other\" details expand with an accordion: the wrapper is a grid whose\n single track animates 0fr → 1fr. */\n.modelOtherWrap_768838850 {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 0.25s ease;\n}\n\n.modelOtherOpen_768838850 {\n grid-template-rows: 1fr;\n}\n\n.modelOtherList_768838850 {\n overflow: hidden;\n min-height: 0;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/* Two-line row anatomy: [swatch] [name / provider] ……… [tokens / share].\n The name block takes the free width; the values block hugs the right. */\n.modelId_768838850 {\n display: flex;\n flex-direction: column;\n gap: 1px;\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.modelValues_768838850 {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 1px;\n flex: 0 0 auto;\n}\n\n.modelName_768838850 {\n color: var(--dsw-alias-label-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.modelProvider_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.modelTokens_768838850 {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.modelPct_768838850 {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* ── tooltips ──────────────────────────────────────────────────────────── */\n\n/* Tooltips render as fixed-position portals on document.body_768838850 (see ChartTip),\n so they escape every ancestor's clipping and never overflow the settings\n panel: ChartTip clamps each tip inside the panel bounds and flips it above\n the hovered element when there is no room below. The plate follows the\n active theme: the DSH overlay token is a light gray in light mode and a\n dark gray under body[data-ds-dark-theme], so the tip reads correctly in\n both instead of staying dark in light mode. */\n.tip_768838850 {\n position: fixed;\n /* The tip is portaled to document.body_768838850, so it competes with the settings\n modal itself (z-index 1000) for the top layer: 100 would sit under the\n modal and the tip would be invisible. 1100 matches the official\n toast/portaled-menu layer, which is exactly the \"over a modal\" tier. */\n z-index: 1100;\n pointer-events: none;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-overlay);\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n box-shadow: 0 2px 12px var(--dsw-alias-bg-mask-2);\n white-space: nowrap;\n}\n\n.tipTitle_768838850 {\n font-weight: 600;\n margin-bottom: 4px;\n}\n\n/* A provider row's raw route id, kept out of the row label but reachable on\n hover (the row shows the display name). */\n.tipSubtitle_768838850 {\n margin: -2px 0 4px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n/* Breakdown heading inside a row tip: names the cross-dimension list under it\n (\"By provider\" under a model, \"By model\" under a provider). */\n.tipLabel_768838850 {\n margin-bottom: 2px;\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n/* The breakdown line pushes its volume to the right edge of the plate. */\n.tipName_768838850 {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.tipValue_768838850 {\n margin-left: auto;\n padding-left: 12px;\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n}\n\n.tipRow_768838850 {\n display: flex;\n align-items: center;\n gap: 5px;\n}\n\n/* Hover tooltip breakdown rows for the \"Other\" aggregate: indented, muted,\n same gray swatch as the Other step. */\n.tipRowOther_768838850 {\n padding-left: 8px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.tipBreakdown_768838850 {\n border-top: 1px solid var(--dsw-alias-border-l1);\n margin-top: 4px;\n padding-top: 4px;\n}\n\n.foot_768838850 {\n font-size: 13px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* ── chart palette ──────────────────────────────────────────────────────── */\n\n/* Light variant (the theme's default): model colours are the reasonix\n token-total palette (rank order by token volume + gray Other) with a\n white veil mixed in (color-mix 30% white) — the raw hues read dark and\n heavy on the light card surface, and the veil lifts lightness and dulls\n saturation the way a translucent white overlay would, keeping the hues\n colourful instead of muddy. The heatmap ramp is DeepSeek's brand blue\n (#4D6BFE) stepping from a pale tint to the full blue; the hit-rate curve\n rides the same brand hue.\n Ranks 6-10 extend the reasonix five to ten (this plugin's addition): each\n is picked to keep the ten hues apart on both themes. */\n.panel_768838850 {\n --dsw-heat-0: #ebedf0;\n --dsw-heat-1: #dbe3ff;\n --dsw-heat-2: #b7c5ff;\n --dsw-heat-3: #8ea4ff;\n --dsw-heat-4: #6884ff;\n --dsw-heat-5: #4d6bfe;\n /* Heatmap cell depth: a highlight above, a shade below. Light trays need\n the stronger pair; the dark tray below lifts the shade instead. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.55);\n --dsw-heat-shade: rgb(0 0 0 / 0.20);\n --dsw-trend-line: #4d6bfe;\n --dsw-chart-1: color-mix(in srgb, #0576ff 70%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 70%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 70%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 70%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 70%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 70%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 70%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 70%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 70%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 70%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 70%, white);\n /* Provider series: a separate five-hue set for the provider section, so a\n provider never wears a model's colour. */\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 70%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 70%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 70%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 70%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 70%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 70%, white);\n}\n\n/* Dark variant: the same reasonix hues under a thicker white veil\n (35% — dark surfaces need the extra lift), so segments read soft and\n bright instead of sinking into the background. */\n@media (prefers-color-scheme: dark) {\n .panel_768838850 {\n --dsw-heat-0: #21262d;\n --dsw-heat-1: #2f4bd0;\n --dsw-heat-2: #4d6bfe;\n --dsw-heat-3: #6e8bff;\n --dsw-heat-4: #93aaff;\n --dsw-heat-5: #c4d0ff;\n /* Same depth pair, recalibrated: a dark tray swallows a light shade. */\n --dsw-heat-sheen: rgb(255 255 255 / 0.30);\n --dsw-heat-shade: rgb(0 0 0 / 0.50);\n --dsw-trend-line: #7d95ff;\n --dsw-chart-1: color-mix(in srgb, #0576ff 65%, white);\n --dsw-chart-2: color-mix(in srgb, #2f6f37 65%, white);\n --dsw-chart-3: color-mix(in srgb, #c46212 65%, white);\n --dsw-chart-4: color-mix(in srgb, #975bf1 65%, white);\n --dsw-chart-5: color-mix(in srgb, #d34591 65%, white);\n --dsw-chart-6: color-mix(in srgb, #0d8a8a 65%, white);\n --dsw-chart-7: color-mix(in srgb, #b3341f 65%, white);\n --dsw-chart-8: color-mix(in srgb, #7a9018 65%, white);\n --dsw-chart-9: color-mix(in srgb, #4b3fd0 65%, white);\n --dsw-chart-10: color-mix(in srgb, #d4a017 65%, white);\n --dsw-chart-other: color-mix(in srgb, #576270 65%, white);\n --dsw-provider-1: color-mix(in srgb, #1f7a8c 65%, white);\n --dsw-provider-2: color-mix(in srgb, #6a8e3a 65%, white);\n --dsw-provider-3: color-mix(in srgb, #a15c2f 65%, white);\n --dsw-provider-4: color-mix(in srgb, #7b5ea7 65%, white);\n --dsw-provider-5: color-mix(in srgb, #b03a5b 65%, white);\n --dsw-provider-other: color-mix(in srgb, #576270 65%, white);\n }\n}\n";
29431
+ const tagId$2 = "dsh-token-total/TokenTotalPanel.module.css";
29432
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
29412
29433
  const tag = document.createElement("style");
29413
29434
  tag.dataset.plugin = "dsh-token-total";
29414
- tag.dataset.pluginCss = tagId$1;
29415
- tag.textContent = css$1;
29435
+ tag.dataset.pluginCss = tagId$2;
29436
+ tag.textContent = css$2;
29416
29437
  document.head.appendChild(tag);
29417
29438
  }
29418
29439
  var TokenTotalPanel_module_default = {
@@ -29967,7 +29988,7 @@ window.__ModuleLoader__.load({
29967
29988
  * (zero input/output but real cache hits) or a failed-call day (requests
29968
29989
  * but no tokens) both carry real usage and must not read as "no data". */
29969
29990
  function isEmptyRange(stats) {
29970
- return stats.tokens === 0 && stats.cacheHit === 0 && stats.requests === 0 && stats.turns === 0;
29991
+ return stats.tokens === 0 && stats.cacheHit === 0 && stats.requests === 0 && stats.failures === 0 && stats.turns === 0;
29971
29992
  }
29972
29993
  function StatCards({ stats, t }) {
29973
29994
  const cards = [
@@ -30006,7 +30027,8 @@ window.__ModuleLoader__.load({
30006
30027
  {
30007
30028
  icon: import_lucide_react.MessagesSquare,
30008
30029
  label: t("requests"),
30009
- value: String(stats.requests)
30030
+ value: String(stats.requests),
30031
+ sub: stats.failures > 0 ? `${t("requestsFailed")} ${stats.failures} · ${formatPercent(stats.failures / Math.max(stats.requests, 1) * 100)}` : void 0
30010
30032
  }
30011
30033
  ];
30012
30034
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -30782,6 +30804,80 @@ window.__ModuleLoader__.load({
30782
30804
  });
30783
30805
  }
30784
30806
  //#endregion
30807
+ //#region src/client/stats-icons.tsx
30808
+ /**
30809
+ * The stat-pill glyphs.
30810
+ *
30811
+ * The shell freezes its primitives table, and that table's icon names moved
30812
+ * with the host line: 0.2.1-rc.2 exports `IconGaugeOutlineRegular` /
30813
+ * `IconDatabaseOutlineRegular` (plus the `Medium` weight) and no longer
30814
+ * exports the `…Outline16` names this plugin used to import. A missing named
30815
+ * export is `undefined` at runtime, and React then throws "Element type is
30816
+ * invalid" on the entry's first render — the shell's slot boundary catches it,
30817
+ * ABDICATES the entry, and the shadowed official pill silently returns. That
30818
+ * failure mode is invisible in types (the package's own declarations still
30819
+ * describe the old names) and looks exactly like "the plugin did nothing".
30820
+ *
30821
+ * So the glyph is resolved from the runtime table by preference order and
30822
+ * falls back to an inline drawing of the same shape when no name exists, which
30823
+ * keeps one published build rendering on every host line.
30824
+ */
30825
+ const table = _deepseek_ai_dsh_client_ui_primitives;
30826
+ /** First exported function component among `names`, or null when the shell
30827
+ * exports none of them (an older or newer icon roster). */
30828
+ function resolveIcon(...names) {
30829
+ for (const name of names) {
30830
+ const candidate = table[name];
30831
+ if (typeof candidate === "function") return candidate;
30832
+ }
30833
+ return null;
30834
+ }
30835
+ const resolvedGauge = resolveIcon("IconGaugeOutlineRegular", "IconGaugeOutlineMedium", "IconGaugeOutline16");
30836
+ const resolvedDatabase = resolveIcon("IconDatabaseOutlineRegular", "IconDatabaseOutlineMedium", "IconDatabaseOutline16");
30837
+ /** Gauge glyph for the activity pill (counts + throughput). */
30838
+ function GaugeIcon() {
30839
+ if (resolvedGauge !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(resolvedGauge, {});
30840
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
30841
+ viewBox: "0 0 16 16",
30842
+ "aria-hidden": "true",
30843
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
30844
+ d: "M8 2.5a5.5 5.5 0 0 1 5.5 5.5M8 8l2.6-2.6M2.5 8A5.5 5.5 0 0 1 8 2.5",
30845
+ fill: "none",
30846
+ stroke: "currentColor",
30847
+ strokeWidth: "1.3",
30848
+ strokeLinecap: "round"
30849
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
30850
+ cx: "8",
30851
+ cy: "8",
30852
+ r: "1",
30853
+ fill: "currentColor"
30854
+ })]
30855
+ });
30856
+ }
30857
+ /** Database glyph for the token-usage pill. */
30858
+ function DatabaseIcon() {
30859
+ if (resolvedDatabase !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(resolvedDatabase, {});
30860
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
30861
+ viewBox: "0 0 16 16",
30862
+ "aria-hidden": "true",
30863
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ellipse", {
30864
+ cx: "8",
30865
+ cy: "3.6",
30866
+ rx: "4.6",
30867
+ ry: "1.9",
30868
+ fill: "none",
30869
+ stroke: "currentColor",
30870
+ strokeWidth: "1.3"
30871
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
30872
+ d: "M3.4 3.6v8.8c0 1.05 2.06 1.9 4.6 1.9s4.6-.85 4.6-1.9V3.6M3.4 8c0 1.05 2.06 1.9 4.6 1.9s4.6-.85 4.6-1.9",
30873
+ fill: "none",
30874
+ stroke: "currentColor",
30875
+ strokeWidth: "1.3",
30876
+ strokeLinecap: "round"
30877
+ })]
30878
+ });
30879
+ }
30880
+ //#endregion
30785
30881
  //#region src/client/stat-dialog.ts
30786
30882
  /**
30787
30883
  * One trigger-anchored stat dialog seat for the composer-dock pills.
@@ -30865,29 +30961,51 @@ window.__ModuleLoader__.load({
30865
30961
  function billedInputTokens(usage) {
30866
30962
  return usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
30867
30963
  }
30964
+ /** Round a cache-read ratio to an integer percentage, ties rounded up (official). */
30965
+ function roundedIntegerPercent(cacheReadTokens, denominator) {
30966
+ const denominatorQuotient = Math.floor(denominator / 200);
30967
+ const denominatorRemainder = denominator % 200;
30968
+ let lower = 0;
30969
+ let upper = 100;
30970
+ while (lower < upper) {
30971
+ const candidate = Math.floor((lower + upper + 1) / 2);
30972
+ const factor = candidate * 2 - 1;
30973
+ if (cacheReadTokens >= factor * denominatorQuotient + Math.ceil(factor * denominatorRemainder / 200)) lower = candidate;
30974
+ else upper = candidate - 1;
30975
+ }
30976
+ return lower;
30977
+ }
30868
30978
  /**
30869
- * Plugin readout: the same ratio with exactly two decimals (85.25% → "85.25",
30870
- * full hit → "100.00"); returns the display digits without the percent sign.
30979
+ * Display-ready cache-hit share of prompt-side input (official replica):
30980
+ * integer text when integer rounding stays below 100, otherwise the minimum
30981
+ * decimal precision that still rounds below 100; full hit returns 100, and
30982
+ * no billed input returns null.
30871
30983
  */
30872
- function cacheHitPercentPrecise(usage) {
30984
+ function cacheHitPercent(usage) {
30873
30985
  const denominator = billedInputTokens(usage);
30874
30986
  if (denominator === 0) return null;
30875
- if (usage.uncachedInputTokens + usage.cacheWriteTokens === 0) return "100.00";
30876
- const percent = Math.round(usage.cacheReadTokens * 1e4 / denominator) / 100;
30877
- return Math.min(percent, 99.99).toFixed(2);
30878
- }
30879
- /** Plugin readout: the five-item breakdown of one usage sample. */
30880
- function tokenBreakdown(usage) {
30881
- const cacheHit = usage.cacheReadTokens;
30882
- const cacheMiss = usage.uncachedInputTokens + usage.cacheWriteTokens;
30883
- const input = cacheHit + cacheMiss;
30884
- return {
30885
- total: input + usage.outputTokens,
30886
- input,
30887
- cacheHit,
30888
- cacheMiss,
30889
- output: usage.outputTokens
30890
- };
30987
+ const missedInputTokens = usage.uncachedInputTokens + usage.cacheWriteTokens;
30988
+ if (missedInputTokens === 0) return "100";
30989
+ const integerPercent = roundedIntegerPercent(usage.cacheReadTokens, denominator);
30990
+ if (integerPercent < 100) return String(integerPercent);
30991
+ let decimalPlaces = 1;
30992
+ let scaledDoubleGap = missedInputTokens * 200;
30993
+ const denominatorTens = Math.floor(denominator / 10);
30994
+ while (scaledDoubleGap <= denominatorTens) {
30995
+ scaledDoubleGap *= 10;
30996
+ decimalPlaces += 1;
30997
+ }
30998
+ const denominatorOnes = denominator % 10;
30999
+ let roundedLoss = 5;
31000
+ for (let loss = 1; loss < 5; loss += 1) {
31001
+ const factor = loss * 2 + 1;
31002
+ const threshold = factor * denominatorTens + Math.floor(factor * denominatorOnes / 10);
31003
+ if (scaledDoubleGap <= threshold) {
31004
+ roundedLoss = loss;
31005
+ break;
31006
+ }
31007
+ }
31008
+ return `99.${"9".repeat(decimalPlaces - 1)}${10 - roundedLoss}`;
30891
31009
  }
30892
31010
  /** Compact token count: 517 / 12.2K / 517K / 1.2M (official formatTokens).
30893
31011
  * Named formatTokensCompact (not formatTokens) — the panel's format.ts
@@ -31205,8 +31323,7 @@ window.__ModuleLoader__.load({
31205
31323
  cacheReadTokens: 0,
31206
31324
  cacheWriteTokens: 0
31207
31325
  };
31208
- /** Use the live main-agent projection when available. Never add tree.total to
31209
- * it: that would count the main agent twice. The log is only the fallback. */
31326
+ /** Add the root and descendants once; tree.total is already that sum. */
31210
31327
  function combinedTotals(tree, self = tree.self) {
31211
31328
  return {
31212
31329
  uncachedInputTokens: self.uncachedInputTokens + tree.subagents.uncachedInputTokens,
@@ -31217,13 +31334,7 @@ window.__ModuleLoader__.load({
31217
31334
  }
31218
31335
  /** One consistent split for the pill, cache hit rate and every dialog row. */
31219
31336
  function sessionUsageForDisplay(native, tree) {
31220
- const extra = tree?.selfCompaction ?? ZERO;
31221
- const self = native === void 0 ? tree?.self ?? { ...ZERO } : {
31222
- uncachedInputTokens: native.uncachedInputTokens + extra.uncachedInputTokens,
31223
- outputTokens: native.outputTokens + extra.outputTokens,
31224
- cacheReadTokens: native.cacheReadTokens + extra.cacheReadTokens,
31225
- cacheWriteTokens: native.cacheWriteTokens + extra.cacheWriteTokens
31226
- };
31337
+ const self = tree?.self ?? native ?? { ...ZERO };
31227
31338
  return {
31228
31339
  self,
31229
31340
  subagents: tree?.subagents ?? { ...ZERO },
@@ -31291,13 +31402,13 @@ window.__ModuleLoader__.load({
31291
31402
  }
31292
31403
  //#endregion
31293
31404
  //#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/StatsLineEnhanced.module.css?inline
31294
- const css = "/* Session stats pills under the composer (shadow of the official StatsPills):\n a gauge pill (counts + output speed, click opens the time dialog) and a\n database pill (total tokens + cache hit, click opens the usage dialog).\n Replicated from the official StatsPills.module_348665330.css_348665330 + stat-dialog.module_348665330.css_348665330\n (13/20 tertiary tier, hover-pill affordance, shared dialog skin).\n\n Host contract: the row serves two containers at once, and picks between them\n at attach time — the component marks which one it landed in (see\n StatsLineEnhanced's `markDockContainer`):\n - through 0.1.6-alpha.1 the dock slot renders straight into InputBar's own\n column, and this row owns the content-width clamp, its side gutters and\n its 4px top pad — the official StatsPills.module_348665330.css_348665330 of that line;\n - 0.1.6-alpha.2 renders it inside InputBar's flex `.dock_348665330` beside the resident\n ContextMeter, which took all three jobs over, so the row only has to\n shrink inside it (the official stylesheet's current form).\n Both forms are kept side by side because one published build has to render\n correctly on either host line. */\n\n.root_348665330 {\n display: flex;\n justify-content: center;\n gap: 12px;\n box-sizing: border-box;\n /* <= 0.1.6-alpha.1: this row is the composer column's content-width box. */\n width: 100%;\n max-width: var(--dsh-chat-content-width);\n margin: 0 auto;\n padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n}\n\n/* 0.1.6-alpha.2+: InputBar's `.dock_348665330` owns the centring, the 12px gap, the top\n pad and the side clearance. */\n.root_348665330[data-dock-row] {\n width: auto;\n min-width: 0;\n max-width: 100%;\n margin: 0;\n padding: 0;\n}\n\n/* Dialog anchor: shrink-wraps its pill so the panel clamp measures the pill. */\n.anchor_348665330 {\n display: inline-flex;\n min-width: 0;\n}\n\n/* Split-mode anchor (DSH >= 0.2.1-alpha.1). The host split the single `stats`\n dock entry into one entry per pill and let its own `.dock_348665330` flex row own the\n layout, so the anchor IS the entry root and carries the text tier `.root_348665330`\n used to set — the official StatsPills.module_348665330.css_348665330 moved exactly this pair of\n declarations from `.root_348665330` onto `.anchor_348665330`. The tier stays off the plain\n `.anchor_348665330` because the pre-split row must keep its own `.root_348665330` tier. */\n.dockAnchor_348665330 {\n font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n}\n\n.pill_348665330 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n box-sizing: border-box;\n max-width: 100%;\n padding: 1px 8px;\n border: none;\n border-radius: 24px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n font: inherit;\n font-variant-numeric: tabular-nums;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.pill_348665330 svg {\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n/* Only the button form (a pill with dialog rows to open) invites interaction;\n the static span form keeps the resting tertiary reading. */\nbutton.pill_348665330 {\n cursor: pointer;\n}\n\nbutton.pill_348665330:hover,\nbutton.pill_348665330[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.label_348665330 {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sep_348665330 {\n color: var(--dsw-alias-separator-primary);\n margin: 0 6px;\n}\n\n/* Reserved slot for the speed figure: the official pill's figure only changes\n when a step settles, but the live reading refreshes on every beat. A width\n that tracked the digits would reflow the centred row on each of them and drag\n the counts pill beside it along; 10ch covers \"1200 tok/s\", the widest shape\n formatTokensPerSecond can produce. */\n.speed_348665330 {\n display: inline-block;\n min-width: 10ch;\n}\n\n/* Marks a usage figure that now includes subagent spend. Deliberately quiet:\n it rides inside the official pill's label and must not read as a badge or\n compete with the token count beside it. */\n.subagentMark_348665330 {\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n}\n\n/* Portal dialog surface: fixed in the viewport, left/top supplied inline from\n the anchored-position clamp (12px viewport margin, layers above overlays).\n Material = the official stat-dialog PAIRING, which dsh 0.1.7's menu\n redesign made mandatory: `--dsw-specific-menu` is translucent there\n (rgba 58%/45%), so the blur token must ride along — the 0.1.5-era recipe\n this file copied was a single opaque line, and a fill-only copy reads as\n bare transparency over the page (issue #6). */\n.dialogPanel_348665330 {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: 12px;\n background: var(--dsw-specific-menu);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n\n/* Dialog heading row: the section name left, the headline value right. */\n.dialogTitle_348665330 {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n/* Rule under the heading, above its rows. */\n.dialogTitleRule_348665330 {\n margin-bottom: 10px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n\n.dialogTitleValue_348665330 {\n font-variant-numeric: tabular-nums;\n}\n\n/* Heading glyph seated left of the section name. */\n.dialogTitleLabel_348665330 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.dialogTitleLabel_348665330 svg {\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n.dialogDetails_348665330 {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dialogDetails_348665330 dt,\n.dialogDetails_348665330 dd {\n min-width: 0;\n margin: 0;\n}\n\n.dialogDetails_348665330 dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n";
31295
- const tagId = "dsh-token-total/StatsLineEnhanced.module.css";
31296
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
31405
+ const css$1 = "/* Session stats pills under the composer (shadow of the official StatsPills):\n a gauge pill (counts + output speed, click opens the time dialog) and a\n database pill (total tokens + cache hit, click opens the usage dialog).\n Replicated from the official StatsPills.module_348665330.css_348665330 + stat-dialog.module_348665330.css_348665330\n (13/20 tertiary tier, hover-pill affordance, shared dialog skin).\n\n Host contract: the row serves two containers at once, and picks between them\n at attach time — the component marks which one it landed in (see\n StatsLineEnhanced's `markDockContainer`):\n - through 0.1.6-alpha.1 the dock slot renders straight into InputBar's own\n column, and this row owns the content-width clamp, its side gutters and\n its 4px top pad — the official StatsPills.module_348665330.css_348665330 of that line;\n - 0.1.6-alpha.2 renders it inside InputBar's flex `.dock_348665330` beside the resident\n ContextMeter, which took all three jobs over, so the row only has to\n shrink inside it (the official stylesheet's current form).\n Both forms are kept side by side because one published build has to render\n correctly on either host line. */\n\n.root_348665330 {\n display: flex;\n justify-content: center;\n gap: 12px;\n box-sizing: border-box;\n /* <= 0.1.6-alpha.1: this row is the composer column's content-width box. */\n width: 100%;\n max-width: var(--dsh-chat-content-width);\n margin: 0 auto;\n padding: 4px calc(var(--dsh-composer-side-clearance) + 16px) 0px;\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n}\n\n/* 0.1.6-alpha.2+: InputBar's `.dock_348665330` owns the centring, the 12px gap, the top\n pad and the side clearance. */\n.root_348665330[data-dock-row] {\n width: auto;\n min-width: 0;\n max-width: 100%;\n margin: 0;\n padding: 0;\n}\n\n/* Dialog anchor: shrink-wraps its pill so the panel clamp measures the pill. */\n.anchor_348665330 {\n display: inline-flex;\n min-width: 0;\n}\n\n/* Split-mode anchor (DSH >= 0.2.1-alpha.1). The host split the single `stats`\n dock entry into one entry per pill and let its own `.dock_348665330` flex row own the\n layout, so the anchor IS the entry root and carries the text tier `.root_348665330`\n used to set — the official StatsPills.module_348665330.css_348665330 moved exactly this pair of\n declarations from `.root_348665330` onto `.anchor_348665330`. The tier stays off the plain\n `.anchor_348665330` because the pre-split row must keep its own `.root_348665330` tier. */\n.dockAnchor_348665330 {\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n}\n\n.pill_348665330 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n box-sizing: border-box;\n max-width: 100%;\n padding: 1px 8px;\n border: none;\n border-radius: var(--dsw-radius-sm);\n background: none;\n color: var(--dsw-alias-label-tertiary);\n font: inherit;\n font-variant-numeric: tabular-nums;\n line-height: inherit;\n white-space: nowrap;\n}\n\n.pill_348665330 svg {\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n/* Only the button form (a pill with dialog rows to open) invites interaction;\n the static span form keeps the resting tertiary reading. */\nbutton.pill_348665330 {\n cursor: pointer;\n}\n\nbutton.pill_348665330:hover,\nbutton.pill_348665330[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.label_348665330 {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.sep_348665330 {\n color: var(--dsw-alias-separator-primary);\n margin: 0 6px;\n}\n\n/* The live throughput reading sits inline exactly like the official figure:\n no reserved width, no box of its own. The earlier `min-width: 10ch` slot kept\n the centred row from shifting while the number grew, but it also left a\n permanent gap before the separator — the official pill has no such slot, and\n native parity wins over that jitter fix. */\n.speed_348665330 {\n display: inline;\n}\n\n/* Portal dialog surface: fixed in the viewport, left/top supplied inline from\n the anchored-position clamp (12px viewport margin, layers above overlays).\n Material = the official stat-dialog PAIRING, which dsh 0.1.7's menu\n redesign made mandatory: `--dsw-specific-menu` is translucent there\n (rgba 58%/45%), so the blur token must ride along — the 0.1.5-era recipe\n this file copied was a single opaque line, and a fill-only copy reads as\n bare transparency over the page (issue #6). */\n.dialogPanel_348665330 {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 12px;\n border: 0;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu);\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: var(--dsh-content-font-size-secondary, 13px);\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n\n/* Dialog heading row: the section name left, the headline value right. */\n.dialogTitle_348665330 {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n/* Rule under the heading, above its rows. */\n.dialogTitleRule_348665330 {\n margin-bottom: 8px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n\n.dialogTitleValue_348665330 {\n font-variant-numeric: tabular-nums;\n}\n\n/* Heading glyph seated left of the section name. */\n.dialogTitleLabel_348665330 {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n\n.dialogTitleLabel_348665330 svg {\n width: 14px;\n height: 14px;\n flex: none;\n}\n\n.dialogDetails_348665330 {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 4px 12px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n\n.dialogDetails_348665330 dt,\n.dialogDetails_348665330 dd {\n min-width: 0;\n margin: 0;\n}\n\n.dialogDetails_348665330 dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n";
31406
+ const tagId$1 = "dsh-token-total/StatsLineEnhanced.module.css";
31407
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$1 + "\"]") === null) {
31297
31408
  const tag = document.createElement("style");
31298
31409
  tag.dataset.plugin = "dsh-token-total";
31299
- tag.dataset.pluginCss = tagId;
31300
- tag.textContent = css;
31410
+ tag.dataset.pluginCss = tagId$1;
31411
+ tag.textContent = css$1;
31301
31412
  document.head.appendChild(tag);
31302
31413
  }
31303
31414
  var StatsLineEnhanced_module_default = {
@@ -31311,7 +31422,6 @@ window.__ModuleLoader__.load({
31311
31422
  "label": "label_348665330",
31312
31423
  "sep": "sep_348665330",
31313
31424
  "speed": "speed_348665330",
31314
- "subagentMark": "subagentMark_348665330",
31315
31425
  "dialogPanel": "dialogPanel_348665330",
31316
31426
  "dialogTitle": "dialogTitle_348665330",
31317
31427
  "dialogTitleRule": "dialogTitleRule_348665330",
@@ -31346,9 +31456,12 @@ window.__ModuleLoader__.load({
31346
31456
  * Which pair to register is read from the dock's entry ledger in
31347
31457
  * src/client/index.tsx.
31348
31458
  *
31349
- * Cache-hit precision, the full session token breakdown and streaming
31350
- * throughput are always enabled; no stored preferences gate these readouts.
31351
- * The token dialog includes the cache-miss row alongside every billing bucket.
31459
+ * Streaming throughput is always enabled; no stored preference gates it. The
31460
+ * three columns are style-for-style the official ones: same tier, radius,
31461
+ * colours, same dialog rows and the same integer cache-hit figure. The one
31462
+ * difference is the middle figure's SCOPE — it totals the whole session tree
31463
+ * (this agent plus every subagent it spawned), which the native projection
31464
+ * cannot see.
31352
31465
  *
31353
31466
  * Streaming throughput replaces the gauge pill's speed
31354
31467
  * figure with a live estimate while a step is still streaming and drops back to
@@ -31378,19 +31491,16 @@ window.__ModuleLoader__.load({
31378
31491
  function anchorClass(dock) {
31379
31492
  return dock ? `${StatsLineEnhanced_module_default.anchor} ${StatsLineEnhanced_module_default.dockAnchor}` : StatsLineEnhanced_module_default.anchor;
31380
31493
  }
31381
- /** The separator + figure shape both speed arms render into the gauge pill.
31382
- * The figure sits in a width-reserved slot (css.speed): the live reading
31383
- * changes every second, and a slot that tracked its digits would reflow the
31384
- * centred row — and with it the counts pill — on every refresh. The slot is
31385
- * rendered even with no figure at all (an empty session has no official figure
31386
- * to fall back to until its first step settles), so the row's layout never
31387
- * depends on whether a number is available. */
31494
+ /** The separator + figure shape both speed arms render into the gauge pill:
31495
+ * exactly the official pill's inline shape — separator and figure appear only
31496
+ * when there is a figure, and the figure occupies no reserved width, so the
31497
+ * row reads and reflows like the native one. */
31388
31498
  function SpeedFigure({ text }) {
31389
31499
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [text !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
31390
31500
  className: StatsLineEnhanced_module_default.sep,
31391
31501
  "aria-hidden": true,
31392
31502
  children: "·"
31393
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
31503
+ }), text !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
31394
31504
  className: StatsLineEnhanced_module_default.speed,
31395
31505
  "data-stats-speed": true,
31396
31506
  children: text
@@ -31527,12 +31637,13 @@ window.__ModuleLoader__.load({
31527
31637
  className: anchorClass(dock),
31528
31638
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31529
31639
  className: StatsLineEnhanced_module_default.pill,
31530
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), label]
31640
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeIcon, {}), label]
31531
31641
  })
31532
31642
  });
31533
31643
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31534
31644
  ref: rootRef,
31535
31645
  className: anchorClass(dock),
31646
+ "data-dsh-token-total-stat": true,
31536
31647
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
31537
31648
  type: "button",
31538
31649
  className: StatsLineEnhanced_module_default.pill,
@@ -31542,7 +31653,7 @@ window.__ModuleLoader__.load({
31542
31653
  onClick: () => {
31543
31654
  setOpen(!open);
31544
31655
  },
31545
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), label]
31656
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeIcon, {}), label]
31546
31657
  }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
31547
31658
  ref: panelRef,
31548
31659
  className: StatsLineEnhanced_module_default.dialogPanel,
@@ -31555,7 +31666,7 @@ window.__ModuleLoader__.load({
31555
31666
  className: StatsLineEnhanced_module_default.dialogTitle,
31556
31667
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31557
31668
  className: StatsLineEnhanced_module_default.dialogTitleLabel,
31558
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconGaugeOutline16, {}), t("stats.dialog.title")]
31669
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GaugeIcon, {}), t("stats.dialog.title")]
31559
31670
  })
31560
31671
  }),
31561
31672
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -31578,18 +31689,16 @@ window.__ModuleLoader__.load({
31578
31689
  }
31579
31690
  function UsagePill({ usage, tree, t, dialog, dock }) {
31580
31691
  const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
31581
- const display = sessionUsageForDisplay(usage, tree?.tree ?? null);
31582
- const subagentTotals = display.subagents;
31583
- const showSubagents = display.subagentSessions > 0;
31584
- const billed = display.total;
31692
+ const billed = sessionUsageForDisplay(usage, tree?.tree ?? null).total;
31585
31693
  const total = billedTotal(billed);
31586
31694
  const totalText = t("message.turnUsage.count", { count: formatTokensCompact(total) });
31587
- const cacheHit = cacheHitPercentPrecise(billed);
31695
+ const cacheHit = cacheHitPercent(billed);
31588
31696
  const cacheHitText = cacheHit !== null ? t("stats.cacheHit", { percent: cacheHit }) : null;
31589
31697
  const exactCount = (value) => t("message.turnUsage.count", { count: formatTokens(value) });
31590
31698
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31591
31699
  ref: rootRef,
31592
31700
  className: anchorClass(dock),
31701
+ "data-dsh-token-total-stat": true,
31593
31702
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
31594
31703
  type: "button",
31595
31704
  className: StatsLineEnhanced_module_default.pill,
@@ -31599,26 +31708,13 @@ window.__ModuleLoader__.load({
31599
31708
  onClick: () => {
31600
31709
  setOpen(!open);
31601
31710
  },
31602
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDatabaseOutline16, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31711
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DatabaseIcon, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31603
31712
  className: StatsLineEnhanced_module_default.label,
31604
- children: [
31605
- totalText,
31606
- showSubagents && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
31607
- className: StatsLineEnhanced_module_default.sep,
31608
- "aria-hidden": true,
31609
- children: "·"
31610
- }),
31611
- showSubagents && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31612
- className: StatsLineEnhanced_module_default.subagentMark,
31613
- title: t("stats.dialog.subagents"),
31614
- children: ["+", tree?.tree?.subagentSessions ?? 0]
31615
- }),
31616
- cacheHitText !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
31617
- className: StatsLineEnhanced_module_default.sep,
31618
- "aria-hidden": true,
31619
- children: "·"
31620
- }), cacheHitText] })
31621
- ]
31713
+ children: [totalText, cacheHitText !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
31714
+ className: StatsLineEnhanced_module_default.sep,
31715
+ "aria-hidden": true,
31716
+ children: "·"
31717
+ }), cacheHitText] })]
31622
31718
  })]
31623
31719
  }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
31624
31720
  ref: panelRef,
@@ -31632,7 +31728,7 @@ window.__ModuleLoader__.load({
31632
31728
  className: StatsLineEnhanced_module_default.dialogTitle,
31633
31729
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
31634
31730
  className: StatsLineEnhanced_module_default.dialogTitleLabel,
31635
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDatabaseOutline16, {}), t("stats.dialog.usageTitle")]
31731
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DatabaseIcon, {}), t("stats.dialog.usageTitle")]
31636
31732
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
31637
31733
  className: StatsLineEnhanced_module_default.dialogTitleValue,
31638
31734
  children: exactCount(total)
@@ -31651,24 +31747,9 @@ window.__ModuleLoader__.load({
31651
31747
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.uncachedInputTokens) }),
31652
31748
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.cacheRead") }),
31653
31749
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.cacheReadTokens) }),
31654
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.cacheWrite") }),
31655
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.cacheWriteTokens) }),
31656
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.cacheMiss") }),
31657
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(tokenBreakdown(billed).cacheMiss) }),
31750
+ billed.cacheWriteTokens !== 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.cacheWrite") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.cacheWriteTokens) })] }),
31658
31751
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.output") }),
31659
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.outputTokens) }),
31660
- showSubagents && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
31661
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.main") }),
31662
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(display.self)) }),
31663
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.subagents") }),
31664
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
31665
- exactCount(billedTotal(subagentTotals)),
31666
- " · ",
31667
- t("stats.dialog.subagentCount", { count: tree?.tree?.subagentSessions ?? 0 })
31668
- ] }),
31669
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.treeTotal") }),
31670
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(billed)) })
31671
- ] })
31752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.outputTokens) })
31672
31753
  ]
31673
31754
  })
31674
31755
  ]
@@ -31764,6 +31845,287 @@ window.__ModuleLoader__.load({
31764
31845
  });
31765
31846
  });
31766
31847
  //#endregion
31848
+ //#region src/client/context-meter-core.ts
31849
+ /** Mirrors the host meter's occupancy calculation exactly. */
31850
+ function contextOccupancy(pressure) {
31851
+ const usedTokens = pressure?.projectedTokens ?? pressure?.pressureTokens;
31852
+ if (usedTokens === void 0 || pressure?.contextWindow === void 0) return null;
31853
+ return {
31854
+ percent: Math.min(100, Math.round(usedTokens / pressure.contextWindow * 100)),
31855
+ usedTokens,
31856
+ contextWindow: pressure.contextWindow
31857
+ };
31858
+ }
31859
+ function contextBreakdownTotal(breakdown) {
31860
+ return breakdown === void 0 ? 0 : breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
31861
+ }
31862
+ function contextBreakdownSegments(percent, breakdown) {
31863
+ const total = contextBreakdownTotal(breakdown);
31864
+ if (breakdown === void 0 || total === 0) return [{
31865
+ key: "total",
31866
+ width: percent
31867
+ }];
31868
+ return [
31869
+ {
31870
+ key: "systemTokens",
31871
+ width: percent * breakdown.systemTokens / total
31872
+ },
31873
+ {
31874
+ key: "toolsTokens",
31875
+ width: percent * breakdown.toolsTokens / total
31876
+ },
31877
+ {
31878
+ key: "messageTokens",
31879
+ width: percent * breakdown.messageTokens / total
31880
+ }
31881
+ ].filter((segment) => segment.width > 0);
31882
+ }
31883
+ function formatContextTokens(value, units) {
31884
+ const scaled = (candidate) => candidate >= 100 ? String(Math.round(candidate)) : String(Math.round(candidate * 10) / 10);
31885
+ if (value < 1e3) return String(value);
31886
+ if (value < 1e6) return units.thousand.replace("{value}", scaled(value / 1e3));
31887
+ return units.million.replace("{value}", scaled(value / 1e6));
31888
+ }
31889
+ //#endregion
31890
+ //#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/ContextUsageEntry.module.css?inline
31891
+ const css = "/* Mirror the host ContextMeter, including its theme and font-size tokens. */\n.root_1877794043 {\n flex: none;\n display: inline-flex;\n}\n\n.trigger_1877794043 {\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-tertiary);\n font-family: inherit;\n font-size: var(--dsh-content-font-size-secondary, 13px);\n font-variant-numeric: tabular-nums;\n line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));\n white-space: nowrap;\n cursor: pointer;\n background: transparent;\n border: none;\n flex: none;\n align-items: center;\n gap: 6px;\n padding: 1px 8px;\n display: inline-flex;\n}\n\n.trigger_1877794043:hover,\n.trigger_1877794043[aria-expanded='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.track_1877794043 {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2px;\n}\n\n.fill_1877794043 {\n fill: none;\n stroke: var(--dsw-alias-label-tertiary);\n stroke-width: 2px;\n stroke-linecap: round;\n}\n\n.panel_1877794043 {\n z-index: 1100;\n box-sizing: border-box;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu);\n width: min(264px, calc(100vw - 24px));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n border: 0;\n padding: 12px;\n font-size: 12px;\n line-height: 20px;\n position: fixed;\n}\n\n.header_1877794043 {\n align-items: center;\n gap: 6px;\n display: flex;\n}\n\n.figures_1877794043 {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n margin-left: auto;\n font-weight: 500;\n}\n\n.percent_1877794043 {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.headline_1877794043 { color: var(--dsw-alias-label-tertiary); }\n.headline_1877794043:empty { display: none; }\n\n.bar_1877794043 {\n corner-shape: round;\n background: var(--dsw-alias-interactive-bg-hover);\n border-radius: 999px;\n gap: 1px;\n height: 4px;\n margin: 10px 0 12px;\n display: flex;\n overflow: hidden;\n}\n\n.segment_1877794043 {\n background: var(--meter-tint, var(--dsw-alias-label-tertiary));\n border-radius: 1px;\n flex: none;\n min-width: 2px;\n height: 100%;\n}\n\n.swatch_1877794043 {\n background: var(--meter-tint);\n vertical-align: baseline;\n border-radius: 2px;\n width: 8px;\n height: 8px;\n margin-right: 6px;\n display: inline-block;\n}\n\n.colorSystem_1877794043 { --meter-tint: var(--dsw-static-neutral-bluish-400); }\n.colorTools_1877794043 { --meter-tint: #a78bfa; }\n.colorMessages_1877794043 { --meter-tint: var(--dsw-static-blue-450); }\n.rows_1877794043 { margin: 6px 0 0; }\n\n.row_1877794043 {\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n padding: 2px 0;\n display: flex;\n}\n\n.row_1877794043 dt { color: var(--dsw-alias-label-secondary); }\n.row_1877794043 dd {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-primary);\n margin: 0;\n}\n";
31892
+ const tagId = "dsh-token-total/ContextUsageEntry.module.css";
31893
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
31894
+ const tag = document.createElement("style");
31895
+ tag.dataset.plugin = "dsh-token-total";
31896
+ tag.dataset.pluginCss = tagId;
31897
+ tag.textContent = css;
31898
+ document.head.appendChild(tag);
31899
+ }
31900
+ var ContextUsageEntry_module_default = {
31901
+ "root": "root_1877794043",
31902
+ "trigger": "trigger_1877794043",
31903
+ "track": "track_1877794043",
31904
+ "fill": "fill_1877794043",
31905
+ "panel": "panel_1877794043",
31906
+ "header": "header_1877794043",
31907
+ "figures": "figures_1877794043",
31908
+ "percent": "percent_1877794043",
31909
+ "headline": "headline_1877794043",
31910
+ "bar": "bar_1877794043",
31911
+ "segment": "segment_1877794043",
31912
+ "swatch": "swatch_1877794043",
31913
+ "colorSystem": "colorSystem_1877794043",
31914
+ "colorTools": "colorTools_1877794043",
31915
+ "colorMessages": "colorMessages_1877794043",
31916
+ "rows": "rows_1877794043",
31917
+ "row": "row_1877794043"
31918
+ };
31919
+ //#endregion
31920
+ //#region src/client/ContextUsageEntry.tsx
31921
+ const CONTEXT_LOCALE_NS = "tokenTotalContext";
31922
+ const CONTEXT_LOCALES = {
31923
+ zh: {
31924
+ label: "上下文",
31925
+ used: "上下文已用",
31926
+ system: "系统提示词",
31927
+ tools: "工具定义",
31928
+ messages: "对话消息",
31929
+ aria: "上下文已用 {percent}",
31930
+ thousand: "{value}K",
31931
+ million: "{value}M"
31932
+ },
31933
+ en: {
31934
+ label: "Context",
31935
+ used: "Context used",
31936
+ system: "System",
31937
+ tools: "Tools",
31938
+ messages: "Conversation",
31939
+ aria: "Context usage {percent}",
31940
+ thousand: "{value}K",
31941
+ million: "{value}M"
31942
+ },
31943
+ "zh-TW": {
31944
+ label: "上下文",
31945
+ used: "已用上下文",
31946
+ system: "系統提示詞",
31947
+ tools: "工具",
31948
+ messages: "對話",
31949
+ aria: "上下文使用量 {percent}",
31950
+ thousand: "{value}千",
31951
+ million: "{value}百萬"
31952
+ }
31953
+ };
31954
+ function ContextUsageEntry({ useProjection, t, installContextTakeover }) {
31955
+ const pressure = useProjection("contextPressure");
31956
+ const breakdown = useProjection("contextBreakdown");
31957
+ const context = contextOccupancy(pressure);
31958
+ const [open, setOpen] = (0, react.useState)(false);
31959
+ const rootRef = (0, react.useRef)(null);
31960
+ const panelRef = (0, react.useRef)(null);
31961
+ const position = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
31962
+ open: open && context !== null,
31963
+ anchorRef: rootRef,
31964
+ panelRef,
31965
+ side: "top",
31966
+ gap: 8,
31967
+ margin: 12
31968
+ });
31969
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open && context !== null, setOpen, panelRef);
31970
+ (0, react.useLayoutEffect)(() => {
31971
+ const anchor = rootRef.current;
31972
+ if (anchor === null || context === null || installContextTakeover === void 0) return;
31973
+ return installContextTakeover(anchor);
31974
+ }, [context !== null, installContextTakeover]);
31975
+ (0, react.useEffect)(() => {
31976
+ if (context === null && open) setOpen(false);
31977
+ }, [context, open]);
31978
+ (0, react.useEffect)(() => {
31979
+ if (!open || context === null) return;
31980
+ const onKeyDown = (event) => {
31981
+ if (event.key === "Escape") setOpen(false);
31982
+ };
31983
+ document.addEventListener("keydown", onKeyDown);
31984
+ return () => document.removeEventListener("keydown", onKeyDown);
31985
+ }, [context, open]);
31986
+ if (context === null) return null;
31987
+ const percent = context.percent;
31988
+ const reading = `${percent}%`;
31989
+ const segments = contextBreakdownSegments(percent, breakdown);
31990
+ const format = (value) => formatContextTokens(value, {
31991
+ thousand: t("thousand"),
31992
+ million: t("million")
31993
+ });
31994
+ const [headBefore = "", headAfter = ""] = t("aria", { percent: "\0" }).split("\0").map((part) => part.trim());
31995
+ const rowLabels = [
31996
+ "system",
31997
+ "tools",
31998
+ "messages"
31999
+ ];
32000
+ const rowColors = [
32001
+ "colorSystem",
32002
+ "colorTools",
32003
+ "colorMessages"
32004
+ ];
32005
+ const segmentColors = {
32006
+ systemTokens: ContextUsageEntry_module_default.colorSystem,
32007
+ toolsTokens: ContextUsageEntry_module_default.colorTools,
32008
+ messageTokens: ContextUsageEntry_module_default.colorMessages
32009
+ };
32010
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
32011
+ ref: rootRef,
32012
+ className: ContextUsageEntry_module_default.root,
32013
+ "data-dsh-token-total-stat": "context",
32014
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
32015
+ type: "button",
32016
+ className: ContextUsageEntry_module_default.trigger,
32017
+ "aria-label": t("aria", { percent: reading }),
32018
+ "aria-haspopup": "dialog",
32019
+ "aria-expanded": open,
32020
+ onClick: () => setOpen(!open),
32021
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
32022
+ viewBox: "0 0 14 14",
32023
+ width: "14",
32024
+ height: "14",
32025
+ "aria-hidden": "true",
32026
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
32027
+ className: ContextUsageEntry_module_default.track,
32028
+ cx: "7",
32029
+ cy: "7",
32030
+ r: "5.5"
32031
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
32032
+ className: ContextUsageEntry_module_default.fill,
32033
+ cx: "7",
32034
+ cy: "7",
32035
+ r: "5.5",
32036
+ strokeDasharray: `${2 * Math.PI * 5.5 * percent / 100} ${2 * Math.PI * 5.5}`,
32037
+ transform: "rotate(-90 7 7)"
32038
+ })]
32039
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: reading })]
32040
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32041
+ ref: panelRef,
32042
+ className: ContextUsageEntry_module_default.panel,
32043
+ style: position ?? MEASURE_STYLE,
32044
+ role: "dialog",
32045
+ "aria-label": t("used"),
32046
+ children: [
32047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32048
+ className: ContextUsageEntry_module_default.header,
32049
+ children: [
32050
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32051
+ className: ContextUsageEntry_module_default.headline,
32052
+ children: headBefore
32053
+ }),
32054
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32055
+ className: ContextUsageEntry_module_default.percent,
32056
+ children: reading
32057
+ }),
32058
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32059
+ className: ContextUsageEntry_module_default.headline,
32060
+ children: headAfter
32061
+ }),
32062
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32063
+ className: ContextUsageEntry_module_default.figures,
32064
+ children: `~${format(context.usedTokens)} / ${format(context.contextWindow)}`
32065
+ })
32066
+ ]
32067
+ }),
32068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32069
+ className: ContextUsageEntry_module_default.bar,
32070
+ children: segments.map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
32071
+ className: segmentColors[segment.key] === void 0 ? ContextUsageEntry_module_default.segment : `${ContextUsageEntry_module_default.segment} ${segmentColors[segment.key]}`,
32072
+ style: { width: `${segment.width}%` }
32073
+ }, segment.key))
32074
+ }),
32075
+ breakdown !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
32076
+ className: ContextUsageEntry_module_default.rows,
32077
+ children: [
32078
+ "systemTokens",
32079
+ "toolsTokens",
32080
+ "messageTokens"
32081
+ ].map((key, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
32082
+ className: ContextUsageEntry_module_default.row,
32083
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dt", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `${ContextUsageEntry_module_default.swatch} ${ContextUsageEntry_module_default[rowColors[index]]}` }), t(rowLabels[index])] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: `~${format(breakdown[key])}` })]
32084
+ }, key))
32085
+ })
32086
+ ]
32087
+ }), document.body)]
32088
+ });
32089
+ }
32090
+ //#endregion
32091
+ //#region src/client/context-takeover.ts
32092
+ /** The host ContextMeter is hard-coded beside composer.dock, not a slot.
32093
+ * Suppress only that recognizable sibling while our replacement is mounted;
32094
+ * never hide a whole dock, slot wrapper, or another plugin's readout. */
32095
+ function installContextTakeover(anchor) {
32096
+ const dock = anchor.closest("[data-composer-dock]");
32097
+ const view = anchor.ownerDocument.defaultView;
32098
+ if (dock === null || view === null) return () => {};
32099
+ const hidden = /* @__PURE__ */ new Map();
32100
+ const reconcile = () => {
32101
+ for (const sibling of Array.from(dock.children)) {
32102
+ if (!(sibling instanceof view.HTMLElement) || sibling.tagName !== "SPAN" || sibling.contains(anchor) || sibling.hasAttribute("data-slot-entry") || sibling.hasAttribute("data-dsh-token-total-stat") || hidden.has(sibling)) continue;
32103
+ if (sibling.querySelector("button[aria-haspopup=\"dialog\"] svg[viewBox=\"0 0 14 14\"] circle[cx=\"7\"][cy=\"7\"][r=\"5.5\"]") === null) continue;
32104
+ hidden.set(sibling, {
32105
+ display: sibling.style.getPropertyValue("display"),
32106
+ priority: sibling.style.getPropertyPriority("display")
32107
+ });
32108
+ sibling.setAttribute("data-dsh-token-total-native-context", "");
32109
+ sibling.style.setProperty("display", "none", "important");
32110
+ }
32111
+ };
32112
+ reconcile();
32113
+ const observer = new view.MutationObserver(reconcile);
32114
+ observer.observe(dock, {
32115
+ childList: true,
32116
+ subtree: true
32117
+ });
32118
+ return () => {
32119
+ observer.disconnect();
32120
+ for (const [element, previous] of hidden) {
32121
+ element.removeAttribute("data-dsh-token-total-native-context");
32122
+ if (previous.display === "") element.style.removeProperty("display");
32123
+ else element.style.setProperty("display", previous.display, previous.priority);
32124
+ }
32125
+ hidden.clear();
32126
+ };
32127
+ }
32128
+ //#endregion
31767
32129
  //#region src/client/locales.ts
31768
32130
  /**
31769
32131
  * Usage panel dictionaries, registered into the DSH locale service. The keys
@@ -31796,6 +32158,7 @@ window.__ModuleLoader__.load({
31796
32158
  cachedTokens: "cached",
31797
32159
  sessions: "Sessions",
31798
32160
  requests: "Requests",
32161
+ requestsFailed: "Failed",
31799
32162
  activeDays: "Active days",
31800
32163
  cacheRate: "Avg cache hit rate",
31801
32164
  cacheRateHint: "Cached input tokens as a share of all input tokens in the range",
@@ -31864,6 +32227,7 @@ window.__ModuleLoader__.load({
31864
32227
  cachedTokens: "缓存命中",
31865
32228
  sessions: "会话数量",
31866
32229
  requests: "请求数量",
32230
+ requestsFailed: "失败",
31867
32231
  activeDays: "活跃天数",
31868
32232
  cacheRate: "缓存命中率",
31869
32233
  cacheRateHint: "时间段内缓存命中 token 占输入 token 的比例",
@@ -31932,6 +32296,7 @@ window.__ModuleLoader__.load({
31932
32296
  cachedTokens: "快取命中",
31933
32297
  sessions: "會話數量",
31934
32298
  requests: "請求數量",
32299
+ requestsFailed: "失敗",
31935
32300
  activeDays: "活躍天數",
31936
32301
  cacheRate: "快取命中率",
31937
32302
  cacheRateHint: "時間範圍內快取命中 token 佔輸入 token 的比例",
@@ -32094,14 +32459,29 @@ window.__ModuleLoader__.load({
32094
32459
  const COMPOSER_DOCK = "conversation.composer.dock";
32095
32460
  /** The pre-split takeover: ONE entry at the official 'stats' id, rendering the
32096
32461
  * whole row (DSH <= 0.2.0-rc.2). */
32097
- function installStatsRowEntry(ctx) {
32462
+ function installContextEntry(ctx) {
32098
32463
  return ctx.slots.register({
32464
+ name: COMPOSER_DOCK,
32465
+ id: "token-total-context",
32466
+ order: 2,
32467
+ priority: -1,
32468
+ locale: CONTEXT_LOCALE_NS,
32469
+ inject: () => ({ installContextTakeover })
32470
+ }, ContextUsageEntry);
32471
+ }
32472
+ function installStatsRowEntry(ctx) {
32473
+ const offStats = ctx.slots.register({
32099
32474
  name: COMPOSER_DOCK,
32100
32475
  id: "stats",
32101
32476
  order: 0,
32102
32477
  priority: -1,
32103
32478
  locale: LOCALE_NS
32104
32479
  }, StatsLineEnhanced);
32480
+ const offContext = installContextEntry(ctx);
32481
+ return () => {
32482
+ offContext();
32483
+ offStats();
32484
+ };
32105
32485
  }
32106
32486
  /** The split takeover: one entry per official pill, each shadowed at its own
32107
32487
  * id (DSH >= 0.2.1-alpha.1). */
@@ -32120,7 +32500,9 @@ window.__ModuleLoader__.load({
32120
32500
  priority: -1,
32121
32501
  locale: LOCALE_NS
32122
32502
  }, StatsUsageEntry);
32503
+ const offContext = installContextEntry(ctx);
32123
32504
  return () => {
32505
+ offContext();
32124
32506
  offUsage();
32125
32507
  offActivity();
32126
32508
  };
@@ -32130,10 +32512,12 @@ window.__ModuleLoader__.load({
32130
32512
  const offZh = ctx.locale.register(LOCALE_NS, "zh", zh);
32131
32513
  const offEn = ctx.locale.register(LOCALE_NS, "en", en);
32132
32514
  const offZhTw = ctx.locale.register(LOCALE_NS, "zh-TW", zhTW);
32515
+ const offContext = Object.entries(CONTEXT_LOCALES).map(([locale, dict]) => ctx.locale.register(CONTEXT_LOCALE_NS, locale, dict));
32133
32516
  return () => {
32134
32517
  offZh();
32135
32518
  offEn();
32136
32519
  offZhTw();
32520
+ for (const off of offContext) off();
32137
32521
  };
32138
32522
  }, "dsh-token-total: dictionaries");
32139
32523
  ctx.effect(() => {