dsh-token-total 1.0.0 → 1.0.2
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/README.md +5 -4
- package/lib/client.js +566 -159
- package/lib/index.js +355 -54
- package/lib/types/client/ContextUsageEntry.d.ts +22 -0
- package/lib/types/client/context-meter-core.d.ts +26 -0
- package/lib/types/client/context-takeover.d.ts +4 -0
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/session-usage-core.d.ts +9 -0
- package/lib/types/client/use-session-usage.d.ts +2 -18
- package/lib/types/collector.d.ts +27 -12
- package/lib/types/context-types.d.ts +23 -2
- package/lib/types/model-names.d.ts +6 -0
- package/lib/types/query.d.ts +8 -0
- package/lib/types/session-usage.d.ts +8 -9
- package/lib/types/store.d.ts +32 -0
- package/lib/types/wire.d.ts +30 -1
- package/package.json +1 -1
- package/src/client/ContextUsageEntry.module.css +20 -0
- package/src/client/ContextUsageEntry.tsx +89 -0
- package/src/client/StatsLineEnhanced.tsx +17 -35
- package/src/client/TokenTotalPanel.module.css +72 -5
- package/src/client/TokenTotalPanel.tsx +204 -56
- package/src/client/context-meter-core.ts +54 -0
- package/src/client/context-takeover.ts +35 -0
- package/src/client/index.tsx +21 -3
- package/src/client/locales.ts +4 -0
- package/src/client/session-usage-core.ts +54 -0
- package/src/client/use-session-usage.ts +45 -98
- package/src/collector.ts +138 -67
- package/src/context-types.ts +17 -2
- package/src/index.ts +29 -0
- package/src/model-names.ts +34 -0
- package/src/query.ts +84 -13
- package/src/routes.ts +20 -2
- package/src/session-usage.ts +50 -38
- package/src/store.ts +136 -0
- package/src/wire.ts +24 -1
package/lib/client.js
CHANGED
|
@@ -29406,13 +29406,13 @@ 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";
|
|
29410
|
-
const tagId$
|
|
29411
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$
|
|
29409
|
+
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";
|
|
29410
|
+
const tagId$2 = "dsh-token-total/TokenTotalPanel.module.css";
|
|
29411
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$2 + "\"]") === null) {
|
|
29412
29412
|
const tag = document.createElement("style");
|
|
29413
29413
|
tag.dataset.plugin = "dsh-token-total";
|
|
29414
|
-
tag.dataset.pluginCss = tagId$
|
|
29415
|
-
tag.textContent = css$
|
|
29414
|
+
tag.dataset.pluginCss = tagId$2;
|
|
29415
|
+
tag.textContent = css$2;
|
|
29416
29416
|
document.head.appendChild(tag);
|
|
29417
29417
|
}
|
|
29418
29418
|
var TokenTotalPanel_module_default = {
|
|
@@ -29481,9 +29481,12 @@ window.__ModuleLoader__.load({
|
|
|
29481
29481
|
"donutDim": "donutDim_768838850",
|
|
29482
29482
|
"donutCenter": "donutCenter_768838850",
|
|
29483
29483
|
"donutLabel": "donutLabel_768838850",
|
|
29484
|
-
"
|
|
29485
|
-
"
|
|
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
|
|
29740
|
-
const TOP_BREAKDOWN =
|
|
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
|
-
|
|
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,
|
|
@@ -29958,7 +29967,7 @@ window.__ModuleLoader__.load({
|
|
|
29958
29967
|
* (zero input/output but real cache hits) or a failed-call day (requests
|
|
29959
29968
|
* but no tokens) both carry real usage and must not read as "no data". */
|
|
29960
29969
|
function isEmptyRange(stats) {
|
|
29961
|
-
return stats.tokens === 0 && stats.cacheHit === 0 && stats.requests === 0 && stats.turns === 0;
|
|
29970
|
+
return stats.tokens === 0 && stats.cacheHit === 0 && stats.requests === 0 && stats.failures === 0 && stats.turns === 0;
|
|
29962
29971
|
}
|
|
29963
29972
|
function StatCards({ stats, t }) {
|
|
29964
29973
|
const cards = [
|
|
@@ -29997,7 +30006,8 @@ window.__ModuleLoader__.load({
|
|
|
29997
30006
|
{
|
|
29998
30007
|
icon: import_lucide_react.MessagesSquare,
|
|
29999
30008
|
label: t("requests"),
|
|
30000
|
-
value: String(stats.requests)
|
|
30009
|
+
value: String(stats.requests),
|
|
30010
|
+
sub: stats.failures > 0 ? `${t("requestsFailed")} ${stats.failures} · ${formatPercent(stats.failures / Math.max(stats.requests, 1) * 100)}` : void 0
|
|
30001
30011
|
}
|
|
30002
30012
|
];
|
|
30003
30013
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -30486,8 +30496,22 @@ window.__ModuleLoader__.load({
|
|
|
30486
30496
|
})]
|
|
30487
30497
|
});
|
|
30488
30498
|
}
|
|
30489
|
-
|
|
30490
|
-
|
|
30499
|
+
/** Provider hues run 1..5 then the gray tail (a provider beyond the palette is
|
|
30500
|
+
* rare: the card lists 8 rows, the palette names 5). */
|
|
30501
|
+
function providerColor(index) {
|
|
30502
|
+
return index < 5 ? `var(--dsw-provider-${index + 1})` : "var(--dsw-provider-other)";
|
|
30503
|
+
}
|
|
30504
|
+
/** Model hues run 1..10 then the gray Other step (mirrors colorForModel). */
|
|
30505
|
+
function modelColor(index) {
|
|
30506
|
+
return index < TOP_MODELS ? `var(--dsw-chart-${index + 1})` : OTHER_COLOR;
|
|
30507
|
+
}
|
|
30508
|
+
function UsageBreakdown({ models, providers, total, providerNames, t, panelRef }) {
|
|
30509
|
+
const [tip, setTip] = (0, react.useState)(null);
|
|
30510
|
+
const providerLabel = (0, react.useCallback)((provider) => {
|
|
30511
|
+
const named = providerNames?.[provider];
|
|
30512
|
+
return typeof named === "string" && named.trim() !== "" ? named : provider;
|
|
30513
|
+
}, [providerNames]);
|
|
30514
|
+
const shareText = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
|
|
30491
30515
|
const modelRows = (0, react.useMemo)(() => {
|
|
30492
30516
|
const byName = /* @__PURE__ */ new Map();
|
|
30493
30517
|
for (const m of models) {
|
|
@@ -30496,73 +30520,184 @@ window.__ModuleLoader__.load({
|
|
|
30496
30520
|
tokens: 0,
|
|
30497
30521
|
cacheHit: 0,
|
|
30498
30522
|
cacheMiss: 0,
|
|
30499
|
-
|
|
30523
|
+
split: /* @__PURE__ */ new Map(),
|
|
30524
|
+
sources: /* @__PURE__ */ new Set()
|
|
30500
30525
|
};
|
|
30501
30526
|
acc.tokens += m.tokens;
|
|
30502
30527
|
acc.cacheHit += m.cacheHit;
|
|
30503
30528
|
acc.cacheMiss += m.cacheMiss;
|
|
30504
|
-
for (const
|
|
30529
|
+
for (const item of m.providerTokens ?? []) acc.split.set(item.provider, (acc.split.get(item.provider) ?? 0) + item.tokens);
|
|
30530
|
+
for (const provider of m.providers ?? (m.provider === "" ? [] : [m.provider])) acc.sources.add(provider);
|
|
30505
30531
|
byName.set(name, acc);
|
|
30506
30532
|
}
|
|
30507
|
-
return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc], i) =>
|
|
30508
|
-
|
|
30509
|
-
|
|
30510
|
-
|
|
30511
|
-
|
|
30512
|
-
|
|
30513
|
-
|
|
30514
|
-
|
|
30515
|
-
|
|
30516
|
-
|
|
30533
|
+
return [...byName.entries()].sort((a, b) => b[1].tokens - a[1].tokens).slice(0, TOP_BREAKDOWN).map(([name, acc], i) => {
|
|
30534
|
+
const split = [...acc.split.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([provider, tokens], j) => ({
|
|
30535
|
+
key: provider,
|
|
30536
|
+
name: providerLabel(provider),
|
|
30537
|
+
tokens,
|
|
30538
|
+
color: providerColor(j)
|
|
30539
|
+
}));
|
|
30540
|
+
const known = new Set(split.map((child) => child.key));
|
|
30541
|
+
const named = [...acc.sources].filter((provider) => !known.has(provider)).sort().map((provider, j) => ({
|
|
30542
|
+
key: provider,
|
|
30543
|
+
name: providerLabel(provider),
|
|
30544
|
+
tokens: null,
|
|
30545
|
+
color: providerColor(split.length + j)
|
|
30546
|
+
}));
|
|
30547
|
+
return {
|
|
30548
|
+
key: name,
|
|
30549
|
+
name,
|
|
30550
|
+
tokens: acc.tokens,
|
|
30551
|
+
cacheHit: acc.cacheHit,
|
|
30552
|
+
cacheMiss: acc.cacheMiss,
|
|
30553
|
+
color: modelColor(i),
|
|
30554
|
+
breakdownLabel: t("byProvider"),
|
|
30555
|
+
breakdown: [...split, ...named]
|
|
30556
|
+
};
|
|
30557
|
+
});
|
|
30558
|
+
}, [
|
|
30559
|
+
models,
|
|
30560
|
+
providerLabel,
|
|
30561
|
+
t
|
|
30562
|
+
]);
|
|
30517
30563
|
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
|
-
|
|
30520
|
-
|
|
30521
|
-
|
|
30522
|
-
|
|
30523
|
-
|
|
30524
|
-
|
|
30525
|
-
|
|
30526
|
-
|
|
30527
|
-
|
|
30528
|
-
|
|
30564
|
+
return [...providers].filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, TOP_BREAKDOWN).map((p, i) => {
|
|
30565
|
+
const label = providerLabel(p.provider);
|
|
30566
|
+
return {
|
|
30567
|
+
key: p.provider,
|
|
30568
|
+
name: label,
|
|
30569
|
+
subtitle: label === p.provider ? void 0 : p.provider,
|
|
30570
|
+
tokens: p.tokens,
|
|
30571
|
+
cacheHit: p.cacheHit,
|
|
30572
|
+
cacheMiss: p.cacheMiss,
|
|
30573
|
+
color: providerColor(i),
|
|
30574
|
+
breakdownLabel: t("byModel"),
|
|
30575
|
+
breakdown: [...p.modelTokens ?? []].sort((a, b) => b.tokens - a.tokens || a.model.localeCompare(b.model)).map((m, j) => ({
|
|
30576
|
+
key: m.model,
|
|
30577
|
+
name: m.model,
|
|
30578
|
+
tokens: m.tokens,
|
|
30579
|
+
color: modelColor(j)
|
|
30580
|
+
}))
|
|
30581
|
+
};
|
|
30582
|
+
});
|
|
30583
|
+
}, [
|
|
30584
|
+
providers,
|
|
30585
|
+
providerLabel,
|
|
30586
|
+
t
|
|
30587
|
+
]);
|
|
30529
30588
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
30530
30589
|
className: TokenTotalPanel_module_default.section,
|
|
30531
|
-
children: [
|
|
30532
|
-
|
|
30533
|
-
|
|
30534
|
-
|
|
30535
|
-
|
|
30536
|
-
|
|
30537
|
-
|
|
30538
|
-
|
|
30539
|
-
|
|
30540
|
-
|
|
30541
|
-
|
|
30542
|
-
|
|
30543
|
-
|
|
30544
|
-
|
|
30545
|
-
|
|
30546
|
-
|
|
30547
|
-
|
|
30548
|
-
|
|
30549
|
-
|
|
30550
|
-
|
|
30551
|
-
|
|
30552
|
-
|
|
30553
|
-
className: clsx(TokenTotalPanel_module_default.segItem, tab === "provider" && TokenTotalPanel_module_default.segActive),
|
|
30554
|
-
onClick: () => setTab("provider"),
|
|
30555
|
-
children: t("byProvider")
|
|
30590
|
+
children: [
|
|
30591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30592
|
+
className: TokenTotalPanel_module_default.sectionHead,
|
|
30593
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
30594
|
+
className: TokenTotalPanel_module_default.sectionTitle,
|
|
30595
|
+
children: t("usageRanking")
|
|
30596
|
+
})
|
|
30597
|
+
}),
|
|
30598
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30599
|
+
className: TokenTotalPanel_module_default.rankCards,
|
|
30600
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankCard, {
|
|
30601
|
+
title: t("byModel"),
|
|
30602
|
+
rows: modelRows,
|
|
30603
|
+
total,
|
|
30604
|
+
t,
|
|
30605
|
+
onTip: setTip
|
|
30606
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RankCard, {
|
|
30607
|
+
title: t("byProvider"),
|
|
30608
|
+
rows: providerRows,
|
|
30609
|
+
total,
|
|
30610
|
+
t,
|
|
30611
|
+
onTip: setTip
|
|
30556
30612
|
})]
|
|
30557
|
-
})
|
|
30613
|
+
}),
|
|
30614
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTip, {
|
|
30615
|
+
anchor: tip?.anchor ?? null,
|
|
30616
|
+
panelRef,
|
|
30617
|
+
children: tip && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
30618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30619
|
+
className: TokenTotalPanel_module_default.tipTitle,
|
|
30620
|
+
children: tip.title
|
|
30621
|
+
}),
|
|
30622
|
+
tip.subtitle !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30623
|
+
className: TokenTotalPanel_module_default.tipSubtitle,
|
|
30624
|
+
children: tip.subtitle
|
|
30625
|
+
}),
|
|
30626
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30627
|
+
className: TokenTotalPanel_module_default.tipRow,
|
|
30628
|
+
children: [
|
|
30629
|
+
t("total"),
|
|
30630
|
+
" ",
|
|
30631
|
+
formatTokens(tip.tokens),
|
|
30632
|
+
" · ",
|
|
30633
|
+
shareText(tip.tokens)
|
|
30634
|
+
]
|
|
30635
|
+
}),
|
|
30636
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30637
|
+
className: TokenTotalPanel_module_default.tipRow,
|
|
30638
|
+
children: [
|
|
30639
|
+
t("cacheRateColumn"),
|
|
30640
|
+
" ",
|
|
30641
|
+
cacheRateText(tip.cacheHit, tip.cacheMiss)
|
|
30642
|
+
]
|
|
30643
|
+
}),
|
|
30644
|
+
tip.breakdown.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30645
|
+
className: TokenTotalPanel_module_default.tipBreakdown,
|
|
30646
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30647
|
+
className: TokenTotalPanel_module_default.tipLabel,
|
|
30648
|
+
children: tip.breakdownLabel
|
|
30649
|
+
}), tip.breakdown.map((child) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30650
|
+
className: TokenTotalPanel_module_default.tipRow,
|
|
30651
|
+
children: [
|
|
30652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
30653
|
+
className: TokenTotalPanel_module_default.legendSwatch,
|
|
30654
|
+
style: { background: child.color }
|
|
30655
|
+
}),
|
|
30656
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30657
|
+
className: TokenTotalPanel_module_default.tipName,
|
|
30658
|
+
children: child.name
|
|
30659
|
+
}),
|
|
30660
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30661
|
+
className: TokenTotalPanel_module_default.tipValue,
|
|
30662
|
+
children: child.tokens === null ? "" : `${formatCompact(child.tokens)} · ${shareText(child.tokens)}`
|
|
30663
|
+
})
|
|
30664
|
+
]
|
|
30665
|
+
}, child.key))]
|
|
30666
|
+
})
|
|
30667
|
+
] })
|
|
30668
|
+
})
|
|
30669
|
+
]
|
|
30670
|
+
});
|
|
30671
|
+
}
|
|
30672
|
+
/** One ranking card: a titled list whose rows explain themselves on hover.
|
|
30673
|
+
* The tip is owned by UsageBreakdown (one open tip at a time), so a row only
|
|
30674
|
+
* reports which row it is, never where to draw. */
|
|
30675
|
+
function RankCard({ title, rows, total, t, onTip }) {
|
|
30676
|
+
const share = (n) => total > 0 ? formatPercent(n / total * 100) : "—";
|
|
30677
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
30678
|
+
className: TokenTotalPanel_module_default.rankCard,
|
|
30679
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30680
|
+
className: TokenTotalPanel_module_default.rankCardTitle,
|
|
30681
|
+
children: title
|
|
30558
30682
|
}), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
30559
30683
|
className: TokenTotalPanel_module_default.empty,
|
|
30560
30684
|
children: t("empty")
|
|
30561
30685
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
30562
|
-
className: TokenTotalPanel_module_default.
|
|
30686
|
+
className: TokenTotalPanel_module_default.rankList,
|
|
30563
30687
|
"data-dsh-usage-glass": "list",
|
|
30564
30688
|
children: rows.map((r, rank) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
30565
30689
|
className: TokenTotalPanel_module_default.modelRow,
|
|
30690
|
+
onMouseEnter: (e) => onTip({
|
|
30691
|
+
anchor: e.currentTarget,
|
|
30692
|
+
title: r.name,
|
|
30693
|
+
subtitle: r.subtitle,
|
|
30694
|
+
tokens: r.tokens,
|
|
30695
|
+
cacheHit: r.cacheHit,
|
|
30696
|
+
cacheMiss: r.cacheMiss,
|
|
30697
|
+
breakdownLabel: r.breakdownLabel,
|
|
30698
|
+
breakdown: r.breakdown
|
|
30699
|
+
}),
|
|
30700
|
+
onMouseLeave: () => onTip(null),
|
|
30566
30701
|
children: [
|
|
30567
30702
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30568
30703
|
className: TokenTotalPanel_module_default.modelRank,
|
|
@@ -30580,7 +30715,7 @@ window.__ModuleLoader__.load({
|
|
|
30580
30715
|
children: r.name
|
|
30581
30716
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
30582
30717
|
className: TokenTotalPanel_module_default.modelProvider,
|
|
30583
|
-
children:
|
|
30718
|
+
children: share(r.tokens)
|
|
30584
30719
|
})]
|
|
30585
30720
|
}),
|
|
30586
30721
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -31064,94 +31199,85 @@ window.__ModuleLoader__.load({
|
|
|
31064
31199
|
return Math.min(MAX_MEASURED_SCALE, Math.max(MIN_MEASURED_SCALE, actual / prior));
|
|
31065
31200
|
}
|
|
31066
31201
|
//#endregion
|
|
31067
|
-
//#region src/client/
|
|
31068
|
-
|
|
31069
|
-
|
|
31070
|
-
|
|
31071
|
-
|
|
31072
|
-
|
|
31073
|
-
|
|
31074
|
-
|
|
31075
|
-
|
|
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) {
|
|
31202
|
+
//#region src/client/session-usage-core.ts
|
|
31203
|
+
const ZERO = {
|
|
31204
|
+
uncachedInputTokens: 0,
|
|
31205
|
+
outputTokens: 0,
|
|
31206
|
+
cacheReadTokens: 0,
|
|
31207
|
+
cacheWriteTokens: 0
|
|
31208
|
+
};
|
|
31209
|
+
/** Add the root and descendants once; tree.total is already that sum. */
|
|
31210
|
+
function combinedTotals(tree, self = tree.self) {
|
|
31087
31211
|
return {
|
|
31088
|
-
uncachedInputTokens:
|
|
31089
|
-
outputTokens:
|
|
31090
|
-
cacheReadTokens:
|
|
31091
|
-
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
|
-
/**
|
|
31095
|
-
function
|
|
31096
|
-
|
|
31218
|
+
/** One consistent split for the pill, cache hit rate and every dialog row. */
|
|
31219
|
+
function sessionUsageForDisplay(native, tree) {
|
|
31220
|
+
const self = tree?.self ?? native ?? { ...ZERO };
|
|
31221
|
+
return {
|
|
31222
|
+
self,
|
|
31223
|
+
subagents: tree?.subagents ?? { ...ZERO },
|
|
31224
|
+
total: tree === null ? self : combinedTotals(tree, self),
|
|
31225
|
+
subagentSessions: tree?.subagentSessions ?? 0
|
|
31226
|
+
};
|
|
31097
31227
|
}
|
|
31098
|
-
/** Whether a totals record carries any billable token at all. */
|
|
31099
31228
|
function hasAnyTokens(totals) {
|
|
31100
|
-
|
|
31101
|
-
return totals.uncachedInputTokens + totals.outputTokens + totals.cacheReadTokens + totals.cacheWriteTokens > 0;
|
|
31229
|
+
return totals !== void 0 && billedTotal(totals) > 0;
|
|
31102
31230
|
}
|
|
31103
|
-
/** Total billed tokens of a flat totals record (prompt side + completion). */
|
|
31104
31231
|
function billedTotal(totals) {
|
|
31105
31232
|
return totals.uncachedInputTokens + totals.cacheReadTokens + totals.cacheWriteTokens + totals.outputTokens;
|
|
31106
31233
|
}
|
|
31107
|
-
|
|
31108
|
-
|
|
31109
|
-
|
|
31110
|
-
*
|
|
31111
|
-
|
|
31112
|
-
|
|
31113
|
-
|
|
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
|
-
*/
|
|
31234
|
+
//#endregion
|
|
31235
|
+
//#region src/client/use-session-usage.ts
|
|
31236
|
+
/** The composer row's window onto the whole session tree. The native usage
|
|
31237
|
+
* projection only moves for main-agent calls, so poll the displayed session
|
|
31238
|
+
* even before its first child appears. Requests are serialized and late answers
|
|
31239
|
+
* from a previous session are discarded. Failures keep the last good reading. */
|
|
31240
|
+
const SESSION_POLL_MS = 4e3;
|
|
31121
31241
|
function useSessionUsage(sessionId, signature) {
|
|
31122
|
-
const [
|
|
31123
|
-
const
|
|
31124
|
-
const [beat, setBeat] = (0, react.useState)(0);
|
|
31125
|
-
const hasSubagents = (tree?.subagentSessions ?? 0) > 0;
|
|
31242
|
+
const [reading, setReading] = (0, react.useState)(null);
|
|
31243
|
+
const refresh = (0, react.useRef)(null);
|
|
31126
31244
|
(0, react.useEffect)(() => {
|
|
31127
|
-
if (
|
|
31128
|
-
|
|
31129
|
-
|
|
31130
|
-
|
|
31245
|
+
if (typeof sessionId !== "string" || sessionId === "") return;
|
|
31246
|
+
let active = true;
|
|
31247
|
+
let pending = false;
|
|
31248
|
+
let again = false;
|
|
31249
|
+
const request = () => {
|
|
31250
|
+
if (!active) return;
|
|
31251
|
+
if (pending) {
|
|
31252
|
+
again = true;
|
|
31253
|
+
return;
|
|
31254
|
+
}
|
|
31255
|
+
pending = true;
|
|
31256
|
+
fetchSessionUsage(sessionId).then((tree) => {
|
|
31257
|
+
if (active) setReading({
|
|
31258
|
+
sessionId,
|
|
31259
|
+
tree
|
|
31260
|
+
});
|
|
31261
|
+
}).catch(() => {}).finally(() => {
|
|
31262
|
+
pending = false;
|
|
31263
|
+
if (active && again) {
|
|
31264
|
+
again = false;
|
|
31265
|
+
request();
|
|
31266
|
+
}
|
|
31267
|
+
});
|
|
31268
|
+
};
|
|
31269
|
+
refresh.current = request;
|
|
31270
|
+
const handle = setInterval(request, SESSION_POLL_MS);
|
|
31131
31271
|
return () => {
|
|
31272
|
+
active = false;
|
|
31273
|
+
refresh.current = null;
|
|
31132
31274
|
clearInterval(handle);
|
|
31133
31275
|
};
|
|
31134
|
-
}, [
|
|
31276
|
+
}, [sessionId]);
|
|
31135
31277
|
(0, react.useEffect)(() => {
|
|
31136
|
-
|
|
31137
|
-
|
|
31138
|
-
|
|
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
|
-
]);
|
|
31278
|
+
refresh.current?.();
|
|
31279
|
+
}, [sessionId, signature]);
|
|
31280
|
+
const tree = reading !== null && reading.sessionId === sessionId ? reading.tree : null;
|
|
31155
31281
|
return {
|
|
31156
31282
|
tree,
|
|
31157
31283
|
totals: tree === null ? null : combinedTotals(tree)
|
|
@@ -31159,13 +31285,13 @@ window.__ModuleLoader__.load({
|
|
|
31159
31285
|
}
|
|
31160
31286
|
//#endregion
|
|
31161
31287
|
//#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/StatsLineEnhanced.module.css?inline
|
|
31162
|
-
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";
|
|
31163
|
-
const tagId = "dsh-token-total/StatsLineEnhanced.module.css";
|
|
31164
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
31288
|
+
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: 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";
|
|
31289
|
+
const tagId$1 = "dsh-token-total/StatsLineEnhanced.module.css";
|
|
31290
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$1 + "\"]") === null) {
|
|
31165
31291
|
const tag = document.createElement("style");
|
|
31166
31292
|
tag.dataset.plugin = "dsh-token-total";
|
|
31167
|
-
tag.dataset.pluginCss = tagId;
|
|
31168
|
-
tag.textContent = css;
|
|
31293
|
+
tag.dataset.pluginCss = tagId$1;
|
|
31294
|
+
tag.textContent = css$1;
|
|
31169
31295
|
document.head.appendChild(tag);
|
|
31170
31296
|
}
|
|
31171
31297
|
var StatsLineEnhanced_module_default = {
|
|
@@ -31401,6 +31527,7 @@ window.__ModuleLoader__.load({
|
|
|
31401
31527
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
31402
31528
|
ref: rootRef,
|
|
31403
31529
|
className: anchorClass(dock),
|
|
31530
|
+
"data-dsh-token-total-stat": true,
|
|
31404
31531
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
31405
31532
|
type: "button",
|
|
31406
31533
|
className: StatsLineEnhanced_module_default.pill,
|
|
@@ -31446,16 +31573,10 @@ window.__ModuleLoader__.load({
|
|
|
31446
31573
|
}
|
|
31447
31574
|
function UsagePill({ usage, tree, t, dialog, dock }) {
|
|
31448
31575
|
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog(dialog);
|
|
31449
|
-
const
|
|
31450
|
-
|
|
31451
|
-
|
|
31452
|
-
|
|
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;
|
|
31576
|
+
const display = sessionUsageForDisplay(usage, tree?.tree ?? null);
|
|
31577
|
+
const subagentTotals = display.subagents;
|
|
31578
|
+
const showSubagents = display.subagentSessions > 0;
|
|
31579
|
+
const billed = display.total;
|
|
31459
31580
|
const total = billedTotal(billed);
|
|
31460
31581
|
const totalText = t("message.turnUsage.count", { count: formatTokensCompact(total) });
|
|
31461
31582
|
const cacheHit = cacheHitPercentPrecise(billed);
|
|
@@ -31464,6 +31585,7 @@ window.__ModuleLoader__.load({
|
|
|
31464
31585
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
31465
31586
|
ref: rootRef,
|
|
31466
31587
|
className: anchorClass(dock),
|
|
31588
|
+
"data-dsh-token-total-stat": true,
|
|
31467
31589
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
31468
31590
|
type: "button",
|
|
31469
31591
|
className: StatsLineEnhanced_module_default.pill,
|
|
@@ -31531,9 +31653,9 @@ window.__ModuleLoader__.load({
|
|
|
31531
31653
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(tokenBreakdown(billed).cacheMiss) }),
|
|
31532
31654
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("message.turnUsage.output") }),
|
|
31533
31655
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billed.outputTokens) }),
|
|
31534
|
-
showSubagents &&
|
|
31656
|
+
showSubagents && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
31535
31657
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.main") }),
|
|
31536
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(
|
|
31658
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(display.self)) }),
|
|
31537
31659
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.subagents") }),
|
|
31538
31660
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dd", { children: [
|
|
31539
31661
|
exactCount(billedTotal(subagentTotals)),
|
|
@@ -31541,7 +31663,7 @@ window.__ModuleLoader__.load({
|
|
|
31541
31663
|
t("stats.dialog.subagentCount", { count: tree?.tree?.subagentSessions ?? 0 })
|
|
31542
31664
|
] }),
|
|
31543
31665
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("stats.dialog.treeTotal") }),
|
|
31544
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(
|
|
31666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: exactCount(billedTotal(billed)) })
|
|
31545
31667
|
] })
|
|
31546
31668
|
]
|
|
31547
31669
|
})
|
|
@@ -31563,7 +31685,7 @@ window.__ModuleLoader__.load({
|
|
|
31563
31685
|
const stats = (0, react.useMemo)(() => projected ?? deriveStats(settledNodes ?? []), [projected, settledNodes]);
|
|
31564
31686
|
const tokenScale = (0, react.useMemo)(() => measuredTokenScale(settledNodes ?? []) ?? 1, [settledNodes]);
|
|
31565
31687
|
const tree = useSessionUsage(sessionId, usageSignature(usage, stats.steps));
|
|
31566
|
-
const hasTokens =
|
|
31688
|
+
const hasTokens = hasAnyTokens(sessionUsageForDisplay(usage, tree.tree).total);
|
|
31567
31689
|
if (stats.steps === 0 && !hasTokens) return null;
|
|
31568
31690
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31569
31691
|
ref: markDockContainer,
|
|
@@ -31629,7 +31751,7 @@ window.__ModuleLoader__.load({
|
|
|
31629
31751
|
const StatsUsageEntry = (0, react.memo)(function StatsUsageEntry({ useProjection, t, sessionId }) {
|
|
31630
31752
|
const usage = useProjection("tokenUsage");
|
|
31631
31753
|
const tree = useSessionUsage(sessionId, usageSignature(usage, usage === void 0 ? 0 : 1));
|
|
31632
|
-
if (!(
|
|
31754
|
+
if (!hasAnyTokens(sessionUsageForDisplay(usage, tree.tree).total)) return null;
|
|
31633
31755
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsagePill, {
|
|
31634
31756
|
usage,
|
|
31635
31757
|
tree,
|
|
@@ -31638,6 +31760,269 @@ window.__ModuleLoader__.load({
|
|
|
31638
31760
|
});
|
|
31639
31761
|
});
|
|
31640
31762
|
//#endregion
|
|
31763
|
+
//#region src/client/context-meter-core.ts
|
|
31764
|
+
/** Mirrors the host meter's occupancy calculation exactly. */
|
|
31765
|
+
function contextOccupancy(pressure) {
|
|
31766
|
+
const usedTokens = pressure?.projectedTokens ?? pressure?.pressureTokens;
|
|
31767
|
+
if (usedTokens === void 0 || pressure?.contextWindow === void 0) return null;
|
|
31768
|
+
return {
|
|
31769
|
+
percent: Math.min(100, Math.round(usedTokens / pressure.contextWindow * 100)),
|
|
31770
|
+
usedTokens,
|
|
31771
|
+
contextWindow: pressure.contextWindow
|
|
31772
|
+
};
|
|
31773
|
+
}
|
|
31774
|
+
function contextBreakdownTotal(breakdown) {
|
|
31775
|
+
return breakdown === void 0 ? 0 : breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
|
|
31776
|
+
}
|
|
31777
|
+
function contextBreakdownSegments(percent, breakdown) {
|
|
31778
|
+
const total = contextBreakdownTotal(breakdown);
|
|
31779
|
+
if (breakdown === void 0 || total === 0) return [{
|
|
31780
|
+
key: "total",
|
|
31781
|
+
width: percent
|
|
31782
|
+
}];
|
|
31783
|
+
return [
|
|
31784
|
+
{
|
|
31785
|
+
key: "systemTokens",
|
|
31786
|
+
width: percent * breakdown.systemTokens / total
|
|
31787
|
+
},
|
|
31788
|
+
{
|
|
31789
|
+
key: "toolsTokens",
|
|
31790
|
+
width: percent * breakdown.toolsTokens / total
|
|
31791
|
+
},
|
|
31792
|
+
{
|
|
31793
|
+
key: "messageTokens",
|
|
31794
|
+
width: percent * breakdown.messageTokens / total
|
|
31795
|
+
}
|
|
31796
|
+
].filter((segment) => segment.width > 0);
|
|
31797
|
+
}
|
|
31798
|
+
function formatContextTokens(value, units) {
|
|
31799
|
+
const scaled = (candidate) => candidate >= 100 ? String(Math.round(candidate)) : String(Math.round(candidate * 10) / 10);
|
|
31800
|
+
if (value < 1e3) return String(value);
|
|
31801
|
+
if (value < 1e6) return units.thousand.replace("{value}", scaled(value / 1e3));
|
|
31802
|
+
return units.million.replace("{value}", scaled(value / 1e6));
|
|
31803
|
+
}
|
|
31804
|
+
//#endregion
|
|
31805
|
+
//#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/ContextUsageEntry.module.css?inline
|
|
31806
|
+
const css = ".root_1877794043 { display: inline-flex; align-items: center; min-width: 0; font: inherit; font-variant-numeric: tabular-nums; line-height: inherit; }\n.trigger_1877794043 { display: inline-flex; align-items: center; gap: 6px; box-sizing: border-box; max-width: 100%; padding: 1px 8px; border: 0; border-radius: 24px; background: transparent; color: var(--dsw-alias-label-tertiary); font: inherit; line-height: inherit; white-space: nowrap; cursor: pointer; }\n.trigger_1877794043 svg { flex: none; }\n.track_1877794043 { fill: none; stroke: var(--dsw-alias-separator-primary); stroke-width: 1.5; }\n.fill_1877794043 { fill: none; stroke: var(--dsw-alias-label-tertiary); stroke-width: 2; stroke-linecap: round; }\n.trigger_1877794043:hover, .trigger_1877794043[aria-expanded='true'] { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); }\n.panel_1877794043 { position: fixed; z-index: 1100; box-sizing: border-box; width: max-content; min-width: min(300px, calc(100vw - 24px)); max-width: min(440px, calc(100vw - 24px)); padding: 16px; border-radius: 12px; background: var(--dsw-specific-menu); backdrop-filter: var(--dsw-menu-backdrop-filter); box-shadow: var(--dsw-elevation-prominent); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }\n.header_1877794043 { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; color: var(--dsw-alias-label-primary); font-weight: 500; }\n.percent_1877794043 { font-variant-numeric: tabular-nums; }\n.figures_1877794043 { margin-left: auto; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; }\n.bar_1877794043 { display: flex; width: 100%; height: 8px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-separator-primary); }\n.segment_1877794043 { height: 100%; }\n.systemTokens_1877794043 { background: var(--dsw-alias-label-primary); }\n.toolsTokens_1877794043 { background: var(--dsw-alias-label-secondary); }\n.messageTokens_1877794043 { background: var(--dsw-alias-label-tertiary); }\n.rows_1877794043 { display: grid; gap: 6px; margin: 12px 0 0; color: var(--dsw-alias-label-tertiary); }\n.row_1877794043 { display: flex; align-items: center; justify-content: space-between; gap: 16px; }\n.row_1877794043 dt { display: flex; align-items: center; gap: 8px; }\n.row_1877794043 dd { margin: 0; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }\n.swatch_1877794043 { width: 8px; height: 8px; border-radius: 2px; }\n";
|
|
31807
|
+
const tagId = "dsh-token-total/ContextUsageEntry.module.css";
|
|
31808
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
31809
|
+
const tag = document.createElement("style");
|
|
31810
|
+
tag.dataset.plugin = "dsh-token-total";
|
|
31811
|
+
tag.dataset.pluginCss = tagId;
|
|
31812
|
+
tag.textContent = css;
|
|
31813
|
+
document.head.appendChild(tag);
|
|
31814
|
+
}
|
|
31815
|
+
var ContextUsageEntry_module_default = {
|
|
31816
|
+
"root": "root_1877794043",
|
|
31817
|
+
"trigger": "trigger_1877794043",
|
|
31818
|
+
"track": "track_1877794043",
|
|
31819
|
+
"fill": "fill_1877794043",
|
|
31820
|
+
"panel": "panel_1877794043",
|
|
31821
|
+
"header": "header_1877794043",
|
|
31822
|
+
"percent": "percent_1877794043",
|
|
31823
|
+
"figures": "figures_1877794043",
|
|
31824
|
+
"bar": "bar_1877794043",
|
|
31825
|
+
"segment": "segment_1877794043",
|
|
31826
|
+
"systemTokens": "systemTokens_1877794043",
|
|
31827
|
+
"toolsTokens": "toolsTokens_1877794043",
|
|
31828
|
+
"messageTokens": "messageTokens_1877794043",
|
|
31829
|
+
"rows": "rows_1877794043",
|
|
31830
|
+
"row": "row_1877794043",
|
|
31831
|
+
"swatch": "swatch_1877794043"
|
|
31832
|
+
};
|
|
31833
|
+
//#endregion
|
|
31834
|
+
//#region src/client/ContextUsageEntry.tsx
|
|
31835
|
+
const CONTEXT_LOCALE_NS = "tokenTotalContext";
|
|
31836
|
+
const CONTEXT_LOCALES = {
|
|
31837
|
+
zh: {
|
|
31838
|
+
label: "上下文",
|
|
31839
|
+
used: "已用上下文",
|
|
31840
|
+
system: "系统提示词",
|
|
31841
|
+
tools: "工具",
|
|
31842
|
+
messages: "对话",
|
|
31843
|
+
aria: "上下文占用 {percent}",
|
|
31844
|
+
thousand: "{value}千",
|
|
31845
|
+
million: "{value}百万"
|
|
31846
|
+
},
|
|
31847
|
+
en: {
|
|
31848
|
+
label: "Context",
|
|
31849
|
+
used: "Context used",
|
|
31850
|
+
system: "System",
|
|
31851
|
+
tools: "Tools",
|
|
31852
|
+
messages: "Conversation",
|
|
31853
|
+
aria: "Context usage {percent}",
|
|
31854
|
+
thousand: "{value}K",
|
|
31855
|
+
million: "{value}M"
|
|
31856
|
+
},
|
|
31857
|
+
"zh-TW": {
|
|
31858
|
+
label: "上下文",
|
|
31859
|
+
used: "已用上下文",
|
|
31860
|
+
system: "系統提示詞",
|
|
31861
|
+
tools: "工具",
|
|
31862
|
+
messages: "對話",
|
|
31863
|
+
aria: "上下文使用量 {percent}",
|
|
31864
|
+
thousand: "{value}千",
|
|
31865
|
+
million: "{value}百萬"
|
|
31866
|
+
}
|
|
31867
|
+
};
|
|
31868
|
+
function ContextUsageEntry({ useProjection, t, installContextTakeover }) {
|
|
31869
|
+
const pressure = useProjection("contextPressure");
|
|
31870
|
+
const breakdown = useProjection("contextBreakdown");
|
|
31871
|
+
const context = contextOccupancy(pressure);
|
|
31872
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
31873
|
+
const rootRef = (0, react.useRef)(null);
|
|
31874
|
+
const panelRef = (0, react.useRef)(null);
|
|
31875
|
+
const position = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
|
|
31876
|
+
open: open && context !== null,
|
|
31877
|
+
anchorRef: rootRef,
|
|
31878
|
+
panelRef,
|
|
31879
|
+
side: "top",
|
|
31880
|
+
gap: 8,
|
|
31881
|
+
margin: 12
|
|
31882
|
+
});
|
|
31883
|
+
(0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open && context !== null, setOpen, panelRef);
|
|
31884
|
+
(0, react.useLayoutEffect)(() => {
|
|
31885
|
+
const anchor = rootRef.current;
|
|
31886
|
+
if (anchor === null || context === null || installContextTakeover === void 0) return;
|
|
31887
|
+
return installContextTakeover(anchor);
|
|
31888
|
+
}, [context !== null, installContextTakeover]);
|
|
31889
|
+
(0, react.useEffect)(() => {
|
|
31890
|
+
if (context === null && open) setOpen(false);
|
|
31891
|
+
}, [context, open]);
|
|
31892
|
+
(0, react.useEffect)(() => {
|
|
31893
|
+
if (!open || context === null) return;
|
|
31894
|
+
const onKeyDown = (event) => {
|
|
31895
|
+
if (event.key === "Escape") setOpen(false);
|
|
31896
|
+
};
|
|
31897
|
+
document.addEventListener("keydown", onKeyDown);
|
|
31898
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
31899
|
+
}, [context, open]);
|
|
31900
|
+
if (context === null) return null;
|
|
31901
|
+
const percent = context.percent;
|
|
31902
|
+
const reading = `${percent}%`;
|
|
31903
|
+
const total = breakdown === void 0 ? 0 : breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
|
|
31904
|
+
const segments = contextBreakdownSegments(percent, breakdown);
|
|
31905
|
+
const format = (value) => formatContextTokens(value, {
|
|
31906
|
+
thousand: t("thousand"),
|
|
31907
|
+
million: t("million")
|
|
31908
|
+
});
|
|
31909
|
+
const label = (part) => t(part);
|
|
31910
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
31911
|
+
ref: rootRef,
|
|
31912
|
+
className: ContextUsageEntry_module_default.root,
|
|
31913
|
+
"data-dsh-token-total-stat": "context",
|
|
31914
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
31915
|
+
type: "button",
|
|
31916
|
+
className: ContextUsageEntry_module_default.trigger,
|
|
31917
|
+
"aria-label": t("aria", { percent: reading }),
|
|
31918
|
+
"aria-haspopup": "dialog",
|
|
31919
|
+
"aria-expanded": open,
|
|
31920
|
+
onClick: () => setOpen(!open),
|
|
31921
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
31922
|
+
viewBox: "0 0 14 14",
|
|
31923
|
+
width: "14",
|
|
31924
|
+
height: "14",
|
|
31925
|
+
"aria-hidden": "true",
|
|
31926
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
31927
|
+
className: ContextUsageEntry_module_default.track,
|
|
31928
|
+
cx: "7",
|
|
31929
|
+
cy: "7",
|
|
31930
|
+
r: "5.5"
|
|
31931
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
31932
|
+
className: ContextUsageEntry_module_default.fill,
|
|
31933
|
+
cx: "7",
|
|
31934
|
+
cy: "7",
|
|
31935
|
+
r: "5.5",
|
|
31936
|
+
strokeDasharray: `${2 * Math.PI * 5.5 * percent / 100} ${2 * Math.PI * 5.5}`,
|
|
31937
|
+
transform: "rotate(-90 7 7)"
|
|
31938
|
+
})]
|
|
31939
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: reading })]
|
|
31940
|
+
}), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31941
|
+
ref: panelRef,
|
|
31942
|
+
className: ContextUsageEntry_module_default.panel,
|
|
31943
|
+
style: position ?? MEASURE_STYLE,
|
|
31944
|
+
role: "dialog",
|
|
31945
|
+
"aria-label": t("used"),
|
|
31946
|
+
children: [
|
|
31947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31948
|
+
className: ContextUsageEntry_module_default.header,
|
|
31949
|
+
children: [
|
|
31950
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("used") }),
|
|
31951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31952
|
+
className: ContextUsageEntry_module_default.percent,
|
|
31953
|
+
children: reading
|
|
31954
|
+
}),
|
|
31955
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
31956
|
+
className: ContextUsageEntry_module_default.figures,
|
|
31957
|
+
children: `~${format(context.usedTokens)} / ${format(context.contextWindow)}`
|
|
31958
|
+
})
|
|
31959
|
+
]
|
|
31960
|
+
}),
|
|
31961
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31962
|
+
className: ContextUsageEntry_module_default.bar,
|
|
31963
|
+
children: segments.map((segment) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31964
|
+
className: `${ContextUsageEntry_module_default.segment} ${ContextUsageEntry_module_default[segment.key] ?? ""}`,
|
|
31965
|
+
style: { width: `${segment.width}%` }
|
|
31966
|
+
}, segment.key))
|
|
31967
|
+
}),
|
|
31968
|
+
breakdown !== void 0 && total > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dl", {
|
|
31969
|
+
className: ContextUsageEntry_module_default.rows,
|
|
31970
|
+
children: [
|
|
31971
|
+
"systemTokens",
|
|
31972
|
+
"toolsTokens",
|
|
31973
|
+
"messageTokens"
|
|
31974
|
+
].map((key, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31975
|
+
className: ContextUsageEntry_module_default.row,
|
|
31976
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dt", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: `${ContextUsageEntry_module_default.swatch} ${ContextUsageEntry_module_default[key]}` }), label([
|
|
31977
|
+
"system",
|
|
31978
|
+
"tools",
|
|
31979
|
+
"messages"
|
|
31980
|
+
][index])] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: `~${format(breakdown[key])}` })]
|
|
31981
|
+
}, key))
|
|
31982
|
+
})
|
|
31983
|
+
]
|
|
31984
|
+
}), document.body)]
|
|
31985
|
+
});
|
|
31986
|
+
}
|
|
31987
|
+
//#endregion
|
|
31988
|
+
//#region src/client/context-takeover.ts
|
|
31989
|
+
/** The host ContextMeter is hard-coded beside composer.dock, not a slot.
|
|
31990
|
+
* Suppress only that recognizable sibling while our replacement is mounted;
|
|
31991
|
+
* never hide a whole dock, slot wrapper, or another plugin's readout. */
|
|
31992
|
+
function installContextTakeover(anchor) {
|
|
31993
|
+
const dock = anchor.closest("[data-composer-dock]");
|
|
31994
|
+
const view = anchor.ownerDocument.defaultView;
|
|
31995
|
+
if (dock === null || view === null) return () => {};
|
|
31996
|
+
const hidden = /* @__PURE__ */ new Map();
|
|
31997
|
+
const reconcile = () => {
|
|
31998
|
+
for (const sibling of Array.from(dock.children)) {
|
|
31999
|
+
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;
|
|
32000
|
+
if (sibling.querySelector("button[aria-haspopup=\"dialog\"] svg[viewBox=\"0 0 14 14\"] circle[cx=\"7\"][cy=\"7\"][r=\"5.5\"]") === null) continue;
|
|
32001
|
+
hidden.set(sibling, {
|
|
32002
|
+
display: sibling.style.getPropertyValue("display"),
|
|
32003
|
+
priority: sibling.style.getPropertyPriority("display")
|
|
32004
|
+
});
|
|
32005
|
+
sibling.setAttribute("data-dsh-token-total-native-context", "");
|
|
32006
|
+
sibling.style.setProperty("display", "none", "important");
|
|
32007
|
+
}
|
|
32008
|
+
};
|
|
32009
|
+
reconcile();
|
|
32010
|
+
const observer = new view.MutationObserver(reconcile);
|
|
32011
|
+
observer.observe(dock, {
|
|
32012
|
+
childList: true,
|
|
32013
|
+
subtree: true
|
|
32014
|
+
});
|
|
32015
|
+
return () => {
|
|
32016
|
+
observer.disconnect();
|
|
32017
|
+
for (const [element, previous] of hidden) {
|
|
32018
|
+
element.removeAttribute("data-dsh-token-total-native-context");
|
|
32019
|
+
if (previous.display === "") element.style.removeProperty("display");
|
|
32020
|
+
else element.style.setProperty("display", previous.display, previous.priority);
|
|
32021
|
+
}
|
|
32022
|
+
hidden.clear();
|
|
32023
|
+
};
|
|
32024
|
+
}
|
|
32025
|
+
//#endregion
|
|
31641
32026
|
//#region src/client/locales.ts
|
|
31642
32027
|
/**
|
|
31643
32028
|
* Usage panel dictionaries, registered into the DSH locale service. The keys
|
|
@@ -31670,6 +32055,7 @@ window.__ModuleLoader__.load({
|
|
|
31670
32055
|
cachedTokens: "cached",
|
|
31671
32056
|
sessions: "Sessions",
|
|
31672
32057
|
requests: "Requests",
|
|
32058
|
+
requestsFailed: "Failed",
|
|
31673
32059
|
activeDays: "Active days",
|
|
31674
32060
|
cacheRate: "Avg cache hit rate",
|
|
31675
32061
|
cacheRateHint: "Cached input tokens as a share of all input tokens in the range",
|
|
@@ -31738,6 +32124,7 @@ window.__ModuleLoader__.load({
|
|
|
31738
32124
|
cachedTokens: "缓存命中",
|
|
31739
32125
|
sessions: "会话数量",
|
|
31740
32126
|
requests: "请求数量",
|
|
32127
|
+
requestsFailed: "失败",
|
|
31741
32128
|
activeDays: "活跃天数",
|
|
31742
32129
|
cacheRate: "缓存命中率",
|
|
31743
32130
|
cacheRateHint: "时间段内缓存命中 token 占输入 token 的比例",
|
|
@@ -31806,6 +32193,7 @@ window.__ModuleLoader__.load({
|
|
|
31806
32193
|
cachedTokens: "快取命中",
|
|
31807
32194
|
sessions: "會話數量",
|
|
31808
32195
|
requests: "請求數量",
|
|
32196
|
+
requestsFailed: "失敗",
|
|
31809
32197
|
activeDays: "活躍天數",
|
|
31810
32198
|
cacheRate: "快取命中率",
|
|
31811
32199
|
cacheRateHint: "時間範圍內快取命中 token 佔輸入 token 的比例",
|
|
@@ -31968,14 +32356,29 @@ window.__ModuleLoader__.load({
|
|
|
31968
32356
|
const COMPOSER_DOCK = "conversation.composer.dock";
|
|
31969
32357
|
/** The pre-split takeover: ONE entry at the official 'stats' id, rendering the
|
|
31970
32358
|
* whole row (DSH <= 0.2.0-rc.2). */
|
|
31971
|
-
function
|
|
32359
|
+
function installContextEntry(ctx) {
|
|
31972
32360
|
return ctx.slots.register({
|
|
32361
|
+
name: COMPOSER_DOCK,
|
|
32362
|
+
id: "token-total-context",
|
|
32363
|
+
order: 2,
|
|
32364
|
+
priority: -1,
|
|
32365
|
+
locale: CONTEXT_LOCALE_NS,
|
|
32366
|
+
inject: () => ({ installContextTakeover })
|
|
32367
|
+
}, ContextUsageEntry);
|
|
32368
|
+
}
|
|
32369
|
+
function installStatsRowEntry(ctx) {
|
|
32370
|
+
const offStats = ctx.slots.register({
|
|
31973
32371
|
name: COMPOSER_DOCK,
|
|
31974
32372
|
id: "stats",
|
|
31975
32373
|
order: 0,
|
|
31976
32374
|
priority: -1,
|
|
31977
32375
|
locale: LOCALE_NS
|
|
31978
32376
|
}, StatsLineEnhanced);
|
|
32377
|
+
const offContext = installContextEntry(ctx);
|
|
32378
|
+
return () => {
|
|
32379
|
+
offContext();
|
|
32380
|
+
offStats();
|
|
32381
|
+
};
|
|
31979
32382
|
}
|
|
31980
32383
|
/** The split takeover: one entry per official pill, each shadowed at its own
|
|
31981
32384
|
* id (DSH >= 0.2.1-alpha.1). */
|
|
@@ -31994,7 +32397,9 @@ window.__ModuleLoader__.load({
|
|
|
31994
32397
|
priority: -1,
|
|
31995
32398
|
locale: LOCALE_NS
|
|
31996
32399
|
}, StatsUsageEntry);
|
|
32400
|
+
const offContext = installContextEntry(ctx);
|
|
31997
32401
|
return () => {
|
|
32402
|
+
offContext();
|
|
31998
32403
|
offUsage();
|
|
31999
32404
|
offActivity();
|
|
32000
32405
|
};
|
|
@@ -32004,10 +32409,12 @@ window.__ModuleLoader__.load({
|
|
|
32004
32409
|
const offZh = ctx.locale.register(LOCALE_NS, "zh", zh);
|
|
32005
32410
|
const offEn = ctx.locale.register(LOCALE_NS, "en", en);
|
|
32006
32411
|
const offZhTw = ctx.locale.register(LOCALE_NS, "zh-TW", zhTW);
|
|
32412
|
+
const offContext = Object.entries(CONTEXT_LOCALES).map(([locale, dict]) => ctx.locale.register(CONTEXT_LOCALE_NS, locale, dict));
|
|
32007
32413
|
return () => {
|
|
32008
32414
|
offZh();
|
|
32009
32415
|
offEn();
|
|
32010
32416
|
offZhTw();
|
|
32417
|
+
for (const off of offContext) off();
|
|
32011
32418
|
};
|
|
32012
32419
|
}, "dsh-token-total: dictionaries");
|
|
32013
32420
|
ctx.effect(() => {
|