dsh-token-total 1.0.0 → 1.0.1

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
@@ -29406,7 +29406,7 @@ window.__ModuleLoader__.load({
29406
29406
  }
29407
29407
  //#endregion
29408
29408
  //#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.modelList_768838850 {\n list-style: none;\n margin: 0;\n padding: 0;\n flex: 1 1 260px;\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/* 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.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";
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
29410
  const tagId$1 = "dsh-token-total/TokenTotalPanel.module.css";
29411
29411
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$1 + "\"]") === null) {
29412
29412
  const tag = document.createElement("style");
@@ -29481,9 +29481,12 @@ window.__ModuleLoader__.load({
29481
29481
  "donutDim": "donutDim_768838850",
29482
29482
  "donutCenter": "donutCenter_768838850",
29483
29483
  "donutLabel": "donutLabel_768838850",
29484
- "modelList": "modelList_768838850",
29485
- "modelRank": "modelRank_768838850",
29484
+ "rankCards": "rankCards_768838850",
29485
+ "rankCard": "rankCard_768838850",
29486
+ "rankCardTitle": "rankCardTitle_768838850",
29487
+ "rankList": "rankList_768838850",
29486
29488
  "modelRow": "modelRow_768838850",
29489
+ "modelRank": "modelRank_768838850",
29487
29490
  "modelRowExpandable": "modelRowExpandable_768838850",
29488
29491
  "modelToggle": "modelToggle_768838850",
29489
29492
  "modelRowSub": "modelRowSub_768838850",
@@ -29500,6 +29503,10 @@ window.__ModuleLoader__.load({
29500
29503
  "body": "body_768838850",
29501
29504
  "tip": "tip_768838850",
29502
29505
  "tipTitle": "tipTitle_768838850",
29506
+ "tipSubtitle": "tipSubtitle_768838850",
29507
+ "tipLabel": "tipLabel_768838850",
29508
+ "tipName": "tipName_768838850",
29509
+ "tipValue": "tipValue_768838850",
29503
29510
  "tipRow": "tipRow_768838850",
29504
29511
  "tipRowOther": "tipRowOther_768838850",
29505
29512
  "tipBreakdown": "tipBreakdown_768838850",
@@ -29736,8 +29743,8 @@ window.__ModuleLoader__.load({
29736
29743
  const TOP_MODELS = 10;
29737
29744
  const OTHER_MODEL = "\0other";
29738
29745
  const OTHER_COLOR = "var(--dsw-chart-other)";
29739
- /** How many rows each bottom breakdown tab lists (requirement 下部: 展示前 5). */
29740
- const TOP_BREAKDOWN = 5;
29746
+ /** How many rows each ranking card lists (扩展到 8 个排行). */
29747
+ const TOP_BREAKDOWN = 8;
29741
29748
  function TokenTotalPanel({ t }) {
29742
29749
  const [range, setRange] = (0, react.useState)("today");
29743
29750
  const [customFrom, setCustomFrom] = (0, react.useState)("");
@@ -29936,7 +29943,9 @@ window.__ModuleLoader__.load({
29936
29943
  models: stats.models,
29937
29944
  providers: stats.providers,
29938
29945
  total: stats.tokens,
29939
- t
29946
+ providerNames: stats.providerNames,
29947
+ t,
29948
+ panelRef
29940
29949
  }),
29941
29950
  stats.to && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
29942
29951
  className: TokenTotalPanel_module_default.foot,
@@ -30486,8 +30495,22 @@ window.__ModuleLoader__.load({
30486
30495
  })]
30487
30496
  });
30488
30497
  }
30489
- function UsageBreakdown({ models, providers, total, t }) {
30490
- const [tab, setTab] = (0, react.useState)("model");
30498
+ /** Provider hues run 1..5 then the gray tail (a provider beyond the palette is
30499
+ * rare: the card lists 8 rows, the palette names 5). */
30500
+ function providerColor(index) {
30501
+ return index < 5 ? `var(--dsw-provider-${index + 1})` : "var(--dsw-provider-other)";
30502
+ }
30503
+ /** Model hues run 1..10 then the gray Other step (mirrors colorForModel). */
30504
+ function modelColor(index) {
30505
+ return index < TOP_MODELS ? `var(--dsw-chart-${index + 1})` : OTHER_COLOR;
30506
+ }
30507
+ function UsageBreakdown({ models, providers, total, providerNames, t, panelRef }) {
30508
+ const [tip, setTip] = (0, react.useState)(null);
30509
+ const providerLabel = (0, react.useCallback)((provider) => {
30510
+ const named = providerNames?.[provider];
30511
+ return typeof named === "string" && named.trim() !== "" ? named : provider;
30512
+ }, [providerNames]);
30513
+ const shareText = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
30491
30514
  const modelRows = (0, react.useMemo)(() => {
30492
30515
  const byName = /* @__PURE__ */ new Map();
30493
30516
  for (const m of models) {
@@ -30496,73 +30519,184 @@ window.__ModuleLoader__.load({
30496
30519
  tokens: 0,
30497
30520
  cacheHit: 0,
30498
30521
  cacheMiss: 0,
30499
- providers: /* @__PURE__ */ new Set()
30522
+ split: /* @__PURE__ */ new Map(),
30523
+ sources: /* @__PURE__ */ new Set()
30500
30524
  };
30501
30525
  acc.tokens += m.tokens;
30502
30526
  acc.cacheHit += m.cacheHit;
30503
30527
  acc.cacheMiss += m.cacheMiss;
30504
- for (const provider of m.providers ?? [m.provider]) acc.providers.add(provider);
30528
+ for (const item of m.providerTokens ?? []) acc.split.set(item.provider, (acc.split.get(item.provider) ?? 0) + item.tokens);
30529
+ for (const provider of m.providers ?? (m.provider === "" ? [] : [m.provider])) acc.sources.add(provider);
30505
30530
  byName.set(name, acc);
30506
30531
  }
30507
- return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc], i) => ({
30508
- key: name,
30509
- name,
30510
- detail: acc.providers.size > 1 ? [...acc.providers].join(", ") : void 0,
30511
- tokens: acc.tokens,
30512
- cacheHit: acc.cacheHit,
30513
- cacheMiss: acc.cacheMiss,
30514
- color: `var(--dsw-chart-${i + 1})`
30515
- }));
30516
- }, [models]);
30532
+ return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc], i) => {
30533
+ const split = [...acc.split.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([provider, tokens], j) => ({
30534
+ key: provider,
30535
+ name: providerLabel(provider),
30536
+ tokens,
30537
+ color: providerColor(j)
30538
+ }));
30539
+ const known = new Set(split.map((child) => child.key));
30540
+ const named = [...acc.sources].filter((provider) => !known.has(provider)).sort().map((provider, j) => ({
30541
+ key: provider,
30542
+ name: providerLabel(provider),
30543
+ tokens: null,
30544
+ color: providerColor(split.length + j)
30545
+ }));
30546
+ return {
30547
+ key: name,
30548
+ name,
30549
+ tokens: acc.tokens,
30550
+ cacheHit: acc.cacheHit,
30551
+ cacheMiss: acc.cacheMiss,
30552
+ color: modelColor(i),
30553
+ breakdownLabel: t("byProvider"),
30554
+ breakdown: [...split, ...named]
30555
+ };
30556
+ });
30557
+ }, [
30558
+ models,
30559
+ providerLabel,
30560
+ t
30561
+ ]);
30517
30562
  const providerRows = (0, react.useMemo)(() => {
30518
- return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p, i) => ({
30519
- key: p.provider,
30520
- name: p.provider,
30521
- tokens: p.tokens,
30522
- cacheHit: p.cacheHit,
30523
- cacheMiss: p.cacheMiss,
30524
- color: `var(--dsw-provider-${i + 1})`
30525
- }));
30526
- }, [providers]);
30527
- const rows = tab === "model" ? modelRows : providerRows;
30528
- const share = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
30563
+ return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p, i) => {
30564
+ const label = providerLabel(p.provider);
30565
+ return {
30566
+ key: p.provider,
30567
+ name: label,
30568
+ subtitle: label === p.provider ? void 0 : p.provider,
30569
+ tokens: p.tokens,
30570
+ cacheHit: p.cacheHit,
30571
+ cacheMiss: p.cacheMiss,
30572
+ color: providerColor(i),
30573
+ breakdownLabel: t("byModel"),
30574
+ breakdown: [...p.modelTokens ?? []].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)).map((m, j) => ({
30575
+ key: m.model,
30576
+ name: m.model,
30577
+ tokens: m.tokens,
30578
+ color: modelColor(j)
30579
+ }))
30580
+ };
30581
+ });
30582
+ }, [
30583
+ providers,
30584
+ providerLabel,
30585
+ t
30586
+ ]);
30529
30587
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
30530
30588
  className: TokenTotalPanel_module_default.section,
30531
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30532
- className: TokenTotalPanel_module_default.sectionHead,
30533
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
30534
- className: TokenTotalPanel_module_default.sectionTitle,
30535
- children: t("usageRanking")
30536
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30537
- className: TokenTotalPanel_module_default.group,
30538
- role: "tablist",
30539
- "aria-label": t("usageRanking"),
30540
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
30541
- size: "sm",
30542
- variant: "ghost",
30543
- role: "tab",
30544
- "aria-selected": tab === "model",
30545
- className: clsx(TokenTotalPanel_module_default.segItem, tab === "model" && TokenTotalPanel_module_default.segActive),
30546
- onClick: () => setTab("model"),
30547
- children: t("byModel")
30548
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
30549
- size: "sm",
30550
- variant: "ghost",
30551
- role: "tab",
30552
- "aria-selected": tab === "provider",
30553
- className: clsx(TokenTotalPanel_module_default.segItem, tab === "provider" && TokenTotalPanel_module_default.segActive),
30554
- onClick: () => setTab("provider"),
30555
- children: t("byProvider")
30589
+ children: [
30590
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30591
+ className: TokenTotalPanel_module_default.sectionHead,
30592
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
30593
+ className: TokenTotalPanel_module_default.sectionTitle,
30594
+ children: t("usageRanking")
30595
+ })
30596
+ }),
30597
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30598
+ className: TokenTotalPanel_module_default.rankCards,
30599
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankCard, {
30600
+ title: t("byModel"),
30601
+ rows: modelRows,
30602
+ total,
30603
+ t,
30604
+ onTip: setTip
30605
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankCard, {
30606
+ title: t("byProvider"),
30607
+ rows: providerRows,
30608
+ total,
30609
+ t,
30610
+ onTip: setTip
30556
30611
  })]
30557
- })]
30612
+ }),
30613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTip, {
30614
+ anchor: tip?.anchor ?? null,
30615
+ panelRef,
30616
+ children: tip && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
30617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30618
+ className: TokenTotalPanel_module_default.tipTitle,
30619
+ children: tip.title
30620
+ }),
30621
+ tip.subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30622
+ className: TokenTotalPanel_module_default.tipSubtitle,
30623
+ children: tip.subtitle
30624
+ }),
30625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30626
+ className: TokenTotalPanel_module_default.tipRow,
30627
+ children: [
30628
+ t("total"),
30629
+ " ",
30630
+ formatTokens(tip.tokens),
30631
+ " · ",
30632
+ shareText(tip.tokens)
30633
+ ]
30634
+ }),
30635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30636
+ className: TokenTotalPanel_module_default.tipRow,
30637
+ children: [
30638
+ t("cacheRateColumn"),
30639
+ " ",
30640
+ cacheRateText(tip.cacheHit, tip.cacheMiss)
30641
+ ]
30642
+ }),
30643
+ tip.breakdown.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30644
+ className: TokenTotalPanel_module_default.tipBreakdown,
30645
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30646
+ className: TokenTotalPanel_module_default.tipLabel,
30647
+ children: tip.breakdownLabel
30648
+ }), tip.breakdown.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30649
+ className: TokenTotalPanel_module_default.tipRow,
30650
+ children: [
30651
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
30652
+ className: TokenTotalPanel_module_default.legendSwatch,
30653
+ style: { background: child.color }
30654
+ }),
30655
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30656
+ className: TokenTotalPanel_module_default.tipName,
30657
+ children: child.name
30658
+ }),
30659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30660
+ className: TokenTotalPanel_module_default.tipValue,
30661
+ children: child.tokens === null ? "" : `${formatCompact(child.tokens)} · ${shareText(child.tokens)}`
30662
+ })
30663
+ ]
30664
+ }, child.key))]
30665
+ })
30666
+ ] })
30667
+ })
30668
+ ]
30669
+ });
30670
+ }
30671
+ /** One ranking card: a titled list whose rows explain themselves on hover.
30672
+ * The tip is owned by UsageBreakdown (one open tip at a time), so a row only
30673
+ * reports which row it is, never where to draw. */
30674
+ function RankCard({ title, rows, total, t, onTip }) {
30675
+ const share = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
30676
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
30677
+ className: TokenTotalPanel_module_default.rankCard,
30678
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30679
+ className: TokenTotalPanel_module_default.rankCardTitle,
30680
+ children: title
30558
30681
  }), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
30559
30682
  className: TokenTotalPanel_module_default.empty,
30560
30683
  children: t("empty")
30561
30684
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
30562
- className: TokenTotalPanel_module_default.modelList,
30685
+ className: TokenTotalPanel_module_default.rankList,
30563
30686
  "data-dsh-usage-glass": "list",
30564
30687
  children: rows.map((r, rank) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
30565
30688
  className: TokenTotalPanel_module_default.modelRow,
30689
+ onMouseEnter: (e) => onTip({
30690
+ anchor: e.currentTarget,
30691
+ title: r.name,
30692
+ subtitle: r.subtitle,
30693
+ tokens: r.tokens,
30694
+ cacheHit: r.cacheHit,
30695
+ cacheMiss: r.cacheMiss,
30696
+ breakdownLabel: r.breakdownLabel,
30697
+ breakdown: r.breakdown
30698
+ }),
30699
+ onMouseLeave: () => onTip(null),
30566
30700
  children: [
30567
30701
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30568
30702
  className: TokenTotalPanel_module_default.modelRank,
@@ -30580,7 +30714,7 @@ window.__ModuleLoader__.load({
30580
30714
  children: r.name
30581
30715
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30582
30716
  className: TokenTotalPanel_module_default.modelProvider,
30583
- children: r.detail ?? share(r.tokens)
30717
+ children: share(r.tokens)
30584
30718
  })]
30585
30719
  }),
30586
30720
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -31064,94 +31198,92 @@ window.__ModuleLoader__.load({
31064
31198
  return Math.min(MAX_MEASURED_SCALE, Math.max(MIN_MEASURED_SCALE, actual / prior));
31065
31199
  }
31066
31200
  //#endregion
31067
- //#region src/client/use-session-usage.ts
31068
- /**
31069
- * The composer row's window onto the WHOLE session tree.
31070
- *
31071
- * dsh's native `tokenUsage` projection is the main agent of the current
31072
- * session only: a session that delegates to subagents (each subagent is its
31073
- * own persisted session, linked by `header.parentSession`) shows a fraction of
31074
- * what it actually spent, and nothing in the shipped UI shows the rest. The
31075
- * host half answers that question over `/token-total/api/session`, replaying
31076
- * the root log plus every descendant's log; this hook is the browser end.
31077
- *
31078
- * Refresh policy: the request is cheap on the host (its tree cache is
31079
- * invalidated by live events), and the native usage figure moves exactly when
31080
- * a call settles — so a change in the native signature is the trigger to
31081
- * re-ask. There is no timer: an idle session costs nothing.
31082
- *
31083
- * Failure policy: the pill must never break the composer, so a failed request
31084
- * keeps the last good answer and simply does not retry until the next trigger.
31085
- */
31086
- function add(into, from) {
31201
+ //#region src/client/session-usage-core.ts
31202
+ const ZERO = {
31203
+ uncachedInputTokens: 0,
31204
+ outputTokens: 0,
31205
+ cacheReadTokens: 0,
31206
+ cacheWriteTokens: 0
31207
+ };
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. */
31210
+ function combinedTotals(tree, self = tree.self) {
31087
31211
  return {
31088
- uncachedInputTokens: into.uncachedInputTokens + from.uncachedInputTokens,
31089
- outputTokens: into.outputTokens + from.outputTokens,
31090
- cacheReadTokens: into.cacheReadTokens + from.cacheReadTokens,
31091
- cacheWriteTokens: into.cacheWriteTokens + from.cacheWriteTokens
31212
+ uncachedInputTokens: self.uncachedInputTokens + tree.subagents.uncachedInputTokens,
31213
+ outputTokens: self.outputTokens + tree.subagents.outputTokens,
31214
+ cacheReadTokens: self.cacheReadTokens + tree.subagents.cacheReadTokens,
31215
+ cacheWriteTokens: self.cacheWriteTokens + tree.subagents.cacheWriteTokens
31092
31216
  };
31093
31217
  }
31094
- /** Fold a tree into the flat shape the pill's dialog renders. */
31095
- function combinedTotals(tree) {
31096
- return add(tree.self, tree.subagents);
31218
+ /** One consistent split for the pill, cache hit rate and every dialog row. */
31219
+ 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
+ };
31227
+ return {
31228
+ self,
31229
+ subagents: tree?.subagents ?? { ...ZERO },
31230
+ total: tree === null ? self : combinedTotals(tree, self),
31231
+ subagentSessions: tree?.subagentSessions ?? 0
31232
+ };
31097
31233
  }
31098
- /** Whether a totals record carries any billable token at all. */
31099
31234
  function hasAnyTokens(totals) {
31100
- if (totals === void 0) return false;
31101
- return totals.uncachedInputTokens + totals.outputTokens + totals.cacheReadTokens + totals.cacheWriteTokens > 0;
31235
+ return totals !== void 0 && billedTotal(totals) > 0;
31102
31236
  }
31103
- /** Total billed tokens of a flat totals record (prompt side + completion). */
31104
31237
  function billedTotal(totals) {
31105
31238
  return totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens + totals.outputTokens;
31106
31239
  }
31107
- /** How often a session that HAS subagents re-asks. The native figure only moves
31108
- * when the main agent's own call settles, so without this a subagent still
31109
- * working (or one that just finished) would not update the pill. A session
31110
- * with no subagents never polls: nothing exists to miss. */
31111
- const SUBAGENT_POLL_MS = 4e3;
31112
- /**
31113
- * Subscribe to one session's whole-tree usage.
31114
- *
31115
- * `signature` is any string that changes when the native usage figure moves;
31116
- * passing it in keeps this hook out of the projection seat and lets the caller
31117
- * decide what "a new call settled" means for its own pill. A session that turns
31118
- * out to have subagents additionally re-asks on a slow beat, because subagent
31119
- * spend lands in a different session log and no native figure moves for it.
31120
- */
31240
+ //#endregion
31241
+ //#region src/client/use-session-usage.ts
31242
+ /** The composer row's window onto the whole session tree. The native usage
31243
+ * projection only moves for main-agent calls, so poll the displayed session
31244
+ * even before its first child appears. Requests are serialized and late answers
31245
+ * from a previous session are discarded. Failures keep the last good reading. */
31246
+ const SESSION_POLL_MS = 4e3;
31121
31247
  function useSessionUsage(sessionId, signature) {
31122
- const [tree, setTree] = (0, react.useState)(null);
31123
- const latest = (0, react.useRef)("");
31124
- const [beat, setBeat] = (0, react.useState)(0);
31125
- const hasSubagents = (tree?.subagentSessions ?? 0) > 0;
31248
+ const [reading, setReading] = (0, react.useState)(null);
31249
+ const refresh = (0, react.useRef)(null);
31126
31250
  (0, react.useEffect)(() => {
31127
- if (!hasSubagents) return;
31128
- const handle = setInterval(() => {
31129
- setBeat(Date.now());
31130
- }, SUBAGENT_POLL_MS);
31251
+ if (typeof sessionId !== "string" || sessionId === "") return;
31252
+ let active = true;
31253
+ let pending = false;
31254
+ let again = false;
31255
+ const request = () => {
31256
+ if (!active) return;
31257
+ if (pending) {
31258
+ again = true;
31259
+ return;
31260
+ }
31261
+ pending = true;
31262
+ fetchSessionUsage(sessionId).then((tree) => {
31263
+ if (active) setReading({
31264
+ sessionId,
31265
+ tree
31266
+ });
31267
+ }).catch(() => {}).finally(() => {
31268
+ pending = false;
31269
+ if (active && again) {
31270
+ again = false;
31271
+ request();
31272
+ }
31273
+ });
31274
+ };
31275
+ refresh.current = request;
31276
+ const handle = setInterval(request, SESSION_POLL_MS);
31131
31277
  return () => {
31278
+ active = false;
31279
+ refresh.current = null;
31132
31280
  clearInterval(handle);
31133
31281
  };
31134
- }, [hasSubagents]);
31282
+ }, [sessionId]);
31135
31283
  (0, react.useEffect)(() => {
31136
- if (typeof sessionId !== "string" || sessionId === "") return;
31137
- const key = `${sessionId}|${signature}|${beat}`;
31138
- if (latest.current === key) return;
31139
- latest.current = key;
31140
- let cancelled = false;
31141
- fetchSessionUsage(sessionId).then((value) => {
31142
- if (cancelled) return;
31143
- setTree(value);
31144
- }).catch(() => {
31145
- latest.current = "";
31146
- });
31147
- return () => {
31148
- cancelled = true;
31149
- };
31150
- }, [
31151
- sessionId,
31152
- signature,
31153
- beat
31154
- ]);
31284
+ refresh.current?.();
31285
+ }, [sessionId, signature]);
31286
+ const tree = reading !== null && reading.sessionId === sessionId ? reading.tree : null;
31155
31287
  return {
31156
31288
  tree,
31157
31289
  totals: tree === null ? null : combinedTotals(tree)
@@ -31446,16 +31578,10 @@ window.__ModuleLoader__.load({
31446
31578
  }
31447
31579
  function UsagePill({ usage, tree, t, dialog, dock }) {
31448
31580
  const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
31449
- const nativeTotals = {
31450
- uncachedInputTokens: usage.uncachedInputTokens,
31451
- outputTokens: usage.outputTokens,
31452
- cacheReadTokens: usage.cacheReadTokens,
31453
- cacheWriteTokens: usage.cacheWriteTokens
31454
- };
31455
- const treeTotals = tree?.totals ?? null;
31456
- const subagentTotals = tree?.tree?.subagents;
31457
- const showSubagents = subagentTotals !== void 0 && hasAnyTokens(subagentTotals);
31458
- const billed = showSubagents && treeTotals !== null ? treeTotals : nativeTotals;
31581
+ const display = sessionUsageForDisplay(usage, tree?.tree ?? null);
31582
+ const subagentTotals = display.subagents;
31583
+ const showSubagents = display.subagentSessions > 0;
31584
+ const billed = display.total;
31459
31585
  const total = billedTotal(billed);
31460
31586
  const totalText = t("message.turnUsage.count", { count: formatTokensCompact(total) });
31461
31587
  const cacheHit = cacheHitPercentPrecise(billed);
@@ -31531,9 +31657,9 @@ window.__ModuleLoader__.load({
31531
31657
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(tokenBreakdown(billed).cacheMiss) }),
31532
31658
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.output") }),
31533
31659
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.outputTokens) }),
31534
- showSubagents && treeTotals !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
31660
+ showSubagents && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
31535
31661
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.main") }),
31536
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(nativeTotals)) }),
31662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(display.self)) }),
31537
31663
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.subagents") }),
31538
31664
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
31539
31665
  exactCount(billedTotal(subagentTotals)),
@@ -31541,7 +31667,7 @@ window.__ModuleLoader__.load({
31541
31667
  t("stats.dialog.subagentCount", { count: tree?.tree?.subagentSessions ?? 0 })
31542
31668
  ] }),
31543
31669
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.treeTotal") }),
31544
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(treeTotals)) })
31670
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(billed)) })
31545
31671
  ] })
31546
31672
  ]
31547
31673
  })
@@ -31563,7 +31689,7 @@ window.__ModuleLoader__.load({
31563
31689
  const stats = (0, react.useMemo)(() => projected ?? deriveStats(settledNodes ?? []), [projected, settledNodes]);
31564
31690
  const tokenScale = (0, react.useMemo)(() => measuredTokenScale(settledNodes ?? []) ?? 1, [settledNodes]);
31565
31691
  const tree = useSessionUsage(sessionId, usageSignature(usage, stats.steps));
31566
- const hasTokens = usage !== void 0 && (billedInputTokens(usage) > 0 || usage.outputTokens > 0);
31692
+ const hasTokens = hasAnyTokens(sessionUsageForDisplay(usage, tree.tree).total);
31567
31693
  if (stats.steps === 0 && !hasTokens) return null;
31568
31694
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
31569
31695
  ref: markDockContainer,
@@ -31629,7 +31755,7 @@ window.__ModuleLoader__.load({
31629
31755
  const StatsUsageEntry = (0, react.memo)(function StatsUsageEntry({ useProjection, t, sessionId }) {
31630
31756
  const usage = useProjection("tokenUsage");
31631
31757
  const tree = useSessionUsage(sessionId, usageSignature(usage, usage === void 0 ? 0 : 1));
31632
- if (!(usage !== void 0 && (billedInputTokens(usage) > 0 || usage.outputTokens > 0))) return null;
31758
+ if (!hasAnyTokens(sessionUsageForDisplay(usage, tree.tree).total)) return null;
31633
31759
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePill, {
31634
31760
  usage,
31635
31761
  tree,