/** * Browser half of @local/dsh-session-cost. * * A compact capsule beside DSH's own stats shows what the open conversation * consumed and what it cost; opening it reveals the breakdown — a cost-share * bar, the per-token-kind rows, the cache-hit share, which price window * applied — plus copy and "与日志核对" actions. The second registration renders * the row's rate-table form on the Plugins page, with a live preview of what the * edited table actually prices. * * Styling uses DSH's theme tokens only (read from the running theme registry, so * the readout follows light/dark), and ships as a real stylesheet rather than * inline styles — that is what makes hover, focus-visible, and a popover * possible. The capsule, buttons, badge and tooltip come from * `@deepseek-ai/dsh-client-ui-primitives` when present and degrade to plain * elements when not. * * Where the numbers come from: * * - token buckets: the `tokenUsage` projection, which DSH's own `dsh-token-meter` * folds from the durable log (cache reads included, exactly as the provider * billed them); * - the model that spent them: the `modelSelection` projection; * - prices: this plugin's Host row config, fetched from * `/api/session-cost/config` and refreshed on a slow interval so a rate edit * reaches an open page without a reload. * * The pill prices the whole conversation at the conversation's current model. * That is exact for the ordinary case and documented as an approximation for a * conversation that switched models mid-flight; `session_cost` (and the * "与日志核对" button, which asks the Host to recompute from the log) gives the * exact per-route figures. * * The bundle is served as-is by the Web client's module loader, so it uses the * `window.__ModuleLoader__.load({ id, factory })` wrapper instead of a build step. */ window.__ModuleLoader__.load({ id: "@local/dsh-session-cost", factory: (require) => { const react = require("react"); /** Primitives are optional: without them everything degrades to plain elements. */ let primitives; try { primitives = require("@deepseek-ai/dsh-client-ui-primitives"); } catch { primitives = undefined; } const Tag = primitives?.Tag; // `ActionButton` and `TextInput` fall back to their native tags: reading the // property off an absent module would yield `undefined`, and React needs a // tag name to render the element at all. const ActionButton = primitives?.Button ?? "button"; const Tooltip = primitives?.Tooltip; // `Input` is a plain text-entry primitive; the form passes every native // input prop through, so using it when present is a drop-in. const TextInput = primitives?.Input ?? "input"; const module = { exports: {} }; const exports = module.exports; /** This package's name, used for the Plugins-page slot keys. */ const PACKAGE_NAME = "@local/dsh-session-cost"; /** Host routes registered by lib/index.js. */ const CONFIG_PATH = "/api/session-cost/config"; const VERIFY_PATH = "/api/session-cost/verify"; const FAMILY_PATH = "/api/session-cost/family"; /** How often the open page re-reads the rate table. */ const CONFIG_REFRESH_MS = 30000; /** * How long the Pill keeps a delegated-family answer. * * The family is asked for even while the panel is closed, because the pill * is the number a reader takes away and it has to include what the * conversation delegated. Re-asking on every render would hammer the Host, * so an answer is reused for this long and every panel open re-reads it. */ const FAMILY_TTL_MS = 15000; /** Class prefix, so every rule is scoped to this package. */ const NS = "dsh-cost"; /** Remember the expanded state, so a reload keeps the reader's choice. */ const OPEN_KEY = `${NS}:open`; /** Gap left between the panel's top edge and whatever clips it. */ const PANEL_GAP = 8; /** Below this the readout is not worth opening, so the floor outranks a tiny room. */ const PANEL_FLOOR = 220; /** * The package stylesheet. Every colour is a theme token; only dynamic * geometry (a bar segment's width) stays inline, because CSS cannot know it. */ const CSS = ` .${NS}-root { position: relative; display: block; } .${NS}-pill { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 8px; box-sizing: border-box; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 999px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; line-height: 1; cursor: pointer; white-space: nowrap; transition: background-color .12s ease, border-color .12s ease, color .12s ease; } .${NS}-pill:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); } .${NS}-pill:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; } .${NS}-pill[data-open="true"] { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); } .${NS}-pill[data-tone="warn"] { border-color: var(--dsw-alias-state-warn-primary); } .${NS}-pill[data-tone="warn"] .${NS}-amount { color: var(--dsw-alias-state-warn-primary); } .${NS}-amount { color: var(--dsw-alias-label-primary); font-weight: 600; font-variant-numeric: tabular-nums; } .${NS}-family { color: var(--dsw-alias-label-secondary); font-size: 10px; letter-spacing: .02em; padding: 1px 4px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2); } .${NS}-self { color: var(--dsw-alias-brand-primary); } .${NS}-derived { color: var(--dsw-alias-state-warn-primary); } .${NS}-sep { color: var(--dsw-alias-border-l2); } .${NS}-tokens { font-variant-numeric: tabular-nums; } .${NS}-chevron { display: inline-block; color: var(--dsw-alias-label-secondary); transition: transform .12s ease; } .${NS}-pill[data-open="true"] .${NS}-chevron { transform: rotate(180deg); } .${NS}-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); flex: none; } .${NS}-pill[data-running="true"] .${NS}-dot { background: var(--dsw-alias-state-success-primary); } /* The panel opens upward from a pill that DSH mounts inside the conversation's scroll port, so the room above it ends at that port's top edge, not at the window's: a panel taller than the room would push its own header past the edge and out of sight. It therefore scrolls instead of growing — this rule is the first-paint ceiling, and the readout replaces it with a measured one as soon as it can see the DOM (see 'panelCeiling'). */ .${NS}-panel { position: absolute; bottom: calc(100% + 8px); left: 0; z-index: 40; width: 344px; max-width: min(344px, 86vw); max-height: max(220px, min(64vh, calc(100vh - 200px))); overflow: hidden auto; overscroll-behavior: contain; --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); --dsh-scrollbar-width: 6px; --dsh-scrollbar-thumb-border: 2px; --dsh-scrollbar-track-margin: 12px; padding: 10px 12px 11px; box-sizing: border-box; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-overlay); color: var(--dsw-alias-label-primary); font-size: 11px; line-height: 18px; box-shadow: 0 8px 24px rgb(0 0 0 / 12%); } .${NS}-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .${NS}-title { font-size: 12px; font-weight: 600; } .${NS}-sub { display: inline-flex; align-items: center; gap: 6px; color: var(--dsw-alias-label-secondary); } .${NS}-muted { color: var(--dsw-alias-label-secondary); } .${NS}-error { color: var(--dsw-alias-state-error-primary); } .${NS}-warn { color: var(--dsw-alias-state-warn-primary); } .${NS}-ok { color: var(--dsw-alias-state-success-primary); } .${NS}-hint { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; } .${NS}-bar { display: flex; height: 6px; margin: 9px 0 5px; border-radius: 999px; overflow: hidden; background: var(--dsw-alias-bg-layer-2); } .${NS}-bar span { display: block; height: 100%; transition: width .18s ease; } .${NS}-legend { display: flex; flex-wrap: wrap; gap: 3px 10px; } .${NS}-legend span { display: inline-flex; align-items: center; gap: 4px; color: var(--dsw-alias-label-secondary); } .${NS}-swatch { width: 7px; height: 7px; border-radius: 2px; flex: none; } .${NS}-rows { display: flex; flex-direction: column; gap: 2px; margin-top: 7px; } .${NS}-row { display: flex; align-items: baseline; gap: 8px; } .${NS}-row-label { flex: 1 1 auto; color: var(--dsw-alias-label-secondary); } .${NS}-row-tokens { color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; } .${NS}-row-amount { flex: none; min-width: 62px; text-align: right; font-variant-numeric: tabular-nums; } .${NS}-section { margin-top: 9px; padding-top: 8px; border-top: 0.5px solid var(--dsw-alias-border-l1); } .${NS}-route { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; } .${NS}-actions { display: flex; align-items: center; gap: 6px; margin-top: 9px; flex-wrap: wrap; } .${NS}-pre { margin: 8px 0 0; padding: 6px 8px; max-height: 168px; overflow: auto; white-space: pre-wrap; word-break: break-all; background: var(--dsw-alias-bg-layer-2); border-radius: 6px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; } .${NS}-foot { margin-top: 8px; color: var(--dsw-alias-label-secondary); font-size: 10px; line-height: 15px; } .${NS}-summary { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-top: 7px; } .${NS}-total { font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; } .${NS}-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; font-size: 12px; } .${NS}-field > span:first-child { color: var(--dsw-alias-label-secondary); } .${NS}-control { width: 100%; box-sizing: border-box; padding: 6px 8px; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 6px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; line-height: 20px; outline: none; } .${NS}-control:focus-visible { border-color: var(--dsw-alias-brand-primary); } .${NS}-control[disabled] { opacity: .6; } textarea.${NS}-control { min-height: 118px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 18px; } .${NS}-preview { margin: 2px 0 6px; } .${NS}-preview table { width: 100%; border-collapse: collapse; font-size: 11px; } .${NS}-preview th, .${NS}-preview td { padding: 3px 6px; text-align: right; border-bottom: 0.5px solid var(--dsw-alias-border-l1); } .${NS}-preview th:first-child, .${NS}-preview td:first-child { text-align: left; } .${NS}-preview th { color: var(--dsw-alias-label-secondary); font-weight: 500; } /* The price editor: a table of models, one row per model, so a price can be typed the way it is published rather than as JSON. */ .${NS}-rates { width: 100%; border-collapse: collapse; font-size: 12px; } .${NS}-rates th { padding: 2px 4px; text-align: left; font-weight: 500; color: var(--dsw-alias-label-secondary); white-space: nowrap; } .${NS}-rates th.${NS}-num { text-align: right; } .${NS}-rates td { padding: 2px 4px; vertical-align: middle; } .${NS}-rates tr + tr td { border-top: 0.5px solid var(--dsw-alias-border-l1); } .${NS}-rates input { width: 100%; box-sizing: border-box; padding: 3px 5px; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 4px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; outline: none; } .${NS}-rates input[type="text"] { text-align: left; } .${NS}-rates input:focus-visible { border-color: var(--dsw-alias-brand-primary); } .${NS}-rates input[disabled] { opacity: .6; } .${NS}-rates td.${NS}-key { min-width: 150px; } .${NS}-rates td.${NS}-num { width: 74px; } .${NS}-rates td.${NS}-act { width: 26px; text-align: right; } .${NS}-drop { border: 0; padding: 1px 5px; border-radius: 4px; cursor: pointer; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 13px; line-height: 18px; } .${NS}-drop:hover { color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-bg-layer-2); } .${NS}-drop[disabled] { cursor: default; opacity: .4; } .${NS}-advanced > summary { cursor: pointer; color: var(--dsw-alias-label-secondary); font-size: 12px; margin-bottom: 6px; } .${NS}-advanced[open] > summary { margin-bottom: 8px; } .${NS}-note { margin: 0 0 8px; padding: 6px 8px; border-radius: 6px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; } .${NS}-grid { display: flex; gap: 10px; flex-wrap: wrap; } .${NS}-grid > * { flex: 1 1 160px; } `; /** * Ensure this package's stylesheet is in the document. * * The static client-bundle format has no `styles.insert` builtin (that * belongs to dynamically defined packages), so the bundle owns one tagged * `