dsh-token-total 1.0.9 → 1.1.0

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 CHANGED
@@ -50,7 +50,7 @@ DSH(DeepSeek Harness)Web 插件:**Token 用量统计看板** + **单会话
50
50
 
51
51
  在每个会话原生的「对话 / 轨迹」旁新增 **用量** tab;沿用会话切换与视图导航,不另造顶部栏或输入框。
52
52
 
53
- - **上下文窗口** 面板(形制参考 [magpie](https://github.com/yetone/magpie))位于子代理/模型明细之下、请求记录之上:左侧「已用上下文」大字即模型窗口里现在压着多少 token,右侧是最近一次回答的提示词缓存命中 / 未命中比例;下方用 squarified treemap 画出窗口里到底装了什么——系统提示词、工具目录、注入记忆(运行时上下文 / 技能目录 / 压缩摘要)、读过的文件(按路径聚合,同一文件多次读取合成一块)、其余工具结果(按工具名聚合)与会话内容,底部图例给出六类各自大小;鼠标悬浮任一色块可看该块的精确 token 数与占比(占上下文/占模型窗口),色块随每次刷新平滑重排、右上角「实时」徽标打上最近一次估算的时间戳。面板可折叠,窄视口下自动降低文字层级。
53
+ - **上下文窗口** 面板(形制参考 [magpie](https://github.com/yetone/magpie))位于子代理/模型明细之下、请求记录之上:左侧「已用上下文」大字即模型窗口里现在压着多少 token,右侧是最近一次回答的提示词缓存命中 / 未命中比例;下方用 squarified treemap 画出窗口里到底装了什么——系统提示词、工具目录、注入记忆(运行时上下文 / 技能目录 / 压缩摘要)、读过的文件(按路径聚合,同一文件多次读取合成一块)、其余工具结果(按工具名聚合)与会话内容,底部图例给出六类各自大小;鼠标悬浮任一色块可看该块的精确 token 数与占比(占上下文/占模型窗口),色块随每次刷新平滑重排、右上角「实时」徽标打上最近一次估算的时间戳。面板可折叠:收起后标题行中间仍留一行摘要——`已用 / 模型窗口 · 占比%`(窗口未知时写「窗口未知」),其后一条堆叠条,**整条长度就是模型窗口**:有颜色的部分是已用的六类(按类别占比分色),尾部灰色是还没用上的余量,所以条本身就是占用率;悬浮可读各类精确大小与空闲 token。摘要与展开态同源,随轮询一起刷新。窄视口下自动降低文字层级,卡宽不足约 620px 时先收起堆叠条、再收起模型名,只留数字与「实时」徽标。
54
54
 
55
55
  - 浅色页面两行统计卡:第一行请求数量(附失败数)、总 tokens(附精确值)、缓存命中率(附命中 token)、token 速度(附平均首 token);第二行输入、输出、缓存读取、缓存写入四个 token 桶。不显示费用。
56
56
  - 子代理明细使用每个子代理的**真实名称**(取自子代理标签,缺失时回退到会话标题、工作目录或会话 id)。
package/lib/client.js CHANGED
@@ -32833,7 +32833,7 @@ window.__ModuleLoader__.load({
32833
32833
  }
32834
32834
  //#endregion
32835
32835
  //#region \0token-total-css:/home/runner/work/dsh-token-total/dsh-token-total/src/client/ContextWindowPanel.module.css?inline
32836
- const css$1 = "/* The context-window panel: magpie's shape on this page's surfaces — a header\n * with the model and a live pill, the used total beside the newest answer's\n * prompt-cache split, a treemap of what occupies the window, and one legend row\n * naming the six buckets. Category colors live as custom properties on the tone\n * classes so the tile and the legend dot share one palette. */\n.panel_1441117854 { background:#fff; border:1px solid #e4e7ee; border-radius:12px; margin:0 0 16px; overflow:hidden; }\n.head_1441117854 { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:12px 16px 0; }\n.headCollapsed_1441117854 { padding-bottom:12px; }\n.toggle_1441117854 { display:flex; align-items:center; gap:6px; min-width:0; background:none; border:0; padding:0; font:inherit; color:#394354; cursor:pointer; }\n.toggle_1441117854 h2 { font-size:13px; font-weight:600; margin:0; white-space:nowrap; }\n.chevron_1441117854 { flex:none; color:#99a1af; transition:transform .2s; }\n.chevronOpen_1441117854 { transform:rotate(90deg); }\n.headRight_1441117854 { display:flex; align-items:center; gap:8px; min-width:0; }\n.model_1441117854 { min-width:0; max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; color:#8b93a4; }\n.livePill_1441117854 { display:inline-flex; align-items:center; gap:4px; flex:none; padding:1px 8px 1px 6px; border-radius:999px; background:#e9f8f1; color:#128a63; font-size:10px; font-weight:500; }\n.liveDot_1441117854 { width:5px; height:5px; border-radius:50%; background:#18a97b; box-shadow:0 0 0 2px #d6f2e5; }\n.liveTime_1441117854 { font-variant-numeric:tabular-nums; opacity:.85; }\n.toggle_1441117854:focus-visible { outline:2px solid #2ab1c6; outline-offset:2px; }\n.body_1441117854 { padding:10px 16px 14px; }\n.body_1441117854[hidden] { display:none; }\n.summary_1441117854 { display:grid; grid-template-columns:minmax(0,1fr) minmax(180px,260px); align-items:end; gap:18px; padding-bottom:12px; }\n.used_1441117854 { display:flex; flex-direction:column; gap:1px; min-width:0; }\n.label_1441117854 { font-size:11px; color:#8790a3; }\n.total_1441117854 { font-size:28px; font-weight:650; line-height:1.15; letter-spacing:-.6px; color:#1b2130; font-variant-numeric:tabular-nums; white-space:nowrap; }\n.total_1441117854 small { margin-left:4px; font-size:11px; font-weight:500; letter-spacing:0; color:#98a0ae; }\n.window_1441117854 { font-size:11px; color:#939aab; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.cache_1441117854 { display:flex; flex-direction:column; gap:6px; min-width:0; }\n.cacheBar_1441117854 { display:flex; width:100%; height:5px; border-radius:999px; overflow:hidden; background:#eef0f4; }\n.cacheHit_1441117854 { height:100%; background:#2ec4a0; }\n.cacheMiss_1441117854 { height:100%; background:#dfe3ea; }\n.cacheEmpty_1441117854 { display:block; width:100%; height:100%; background:repeating-linear-gradient(90deg,#e6e9ef 0 3px,transparent 3px 7px); }\n.cacheLegend_1441117854 { display:flex; flex-wrap:wrap; gap:2px 10px; font-size:10px; color:#8b93a4; font-variant-numeric:tabular-nums; }\n.cacheLegend_1441117854 i { display:inline-block; width:6px; height:6px; margin-right:4px; border-radius:50%; }\n.cacheHitDot_1441117854 { background:#2ec4a0; }\n.cacheMissDot_1441117854 { background:#dfe3ea; }\n.cachePending_1441117854 { color:#9aa3b3; }\n.note_1441117854 { margin:0 0 10px; padding:5px 8px; border:1px solid #f2e4c6; border-radius:6px; background:#fdf7ea; font-size:10.5px; line-height:1.5; color:#8a6a2a; font-variant-numeric:tabular-nums; }\n.map_1441117854 { position:relative; height:268px; border-radius:9px; overflow:hidden; }\n/* Slots glide into their new geometry when a refresh re-writes the estimate,\n * so a change in the data reads as motion instead of a silent repaint. */\n.slot_1441117854 { position:absolute; box-sizing:border-box; padding:1.5px; transition:left .35s, top .35s, width .35s, height .35s; }\n.tile_1441117854 { display:flex; flex-direction:column; gap:1px; box-sizing:border-box; width:100%; height:100%; padding:5px 6px; border:1px solid var(--ctx-line); border-radius:7px; background:var(--ctx-soft); color:var(--ctx-ink); overflow:hidden; }\n.tileLabel_1441117854 { font-size:11px; font-weight:600; line-height:1.25; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }\n.tileSize_1441117854 { font-size:10px; opacity:.72; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-variant-numeric:tabular-nums; }\n/* The hover card: exact figures a tile has no room to print. */\n.tip_1441117854 { position:absolute; z-index:2; box-sizing:border-box; width:220px; padding:8px 10px; border:1px solid #e0e4ec; border-radius:8px; background:#fff; box-shadow:0 8px 20px rgba(23,32,54,.12); font-size:11px; line-height:1.55; pointer-events:none; }\n.tipName_1441117854 { font-weight:600; color:#1b2130; overflow-wrap:anywhere; }\n.tipMeta_1441117854 { color:#6f7889; font-variant-numeric:tabular-nums; }\n.tipShare_1441117854 { color:#4a5567; font-variant-numeric:tabular-nums; }\n.legend_1441117854 { display:flex; flex-wrap:wrap; gap:5px 14px; list-style:none; margin:0; padding:10px 0 0; border-top:1px solid #f2f4f8; margin-top:10px; font-size:11px; color:#6f7889; }\n.legend_1441117854 li { display:flex; align-items:center; gap:5px; }\n.legendName_1441117854 { white-space:nowrap; }\n.legendValue_1441117854 { color:#98a0ae; font-variant-numeric:tabular-nums; }\n.swatch_1441117854 { display:inline-block; flex:none; box-sizing:border-box; width:9px; height:9px; border-radius:3px; background:var(--ctx-soft); background:color-mix(in srgb, var(--ctx) 50%, var(--ctx-soft)); border:1px solid var(--ctx-line); }\n.empty_1441117854 { margin:0; padding:24px 0; text-align:center; font-size:12px; color:#9aa2b1; }\n/* Category palette: `--ctx` is the accent tint, `--ctx-soft` the tile fill (and\n * the legend swatch, so the dots match the tiles exactly), `--ctx-ink` the tile\n * text, `--ctx-line` its hairline border. */\n.toneSystem_1441117854 { --ctx:#a346e6; --ctx-soft:#f6ecfd; --ctx-ink:#6c2e9e; --ctx-line:#ecdafa; }\n.toneTools_1441117854 { --ctx:#4f79f6; --ctx-soft:#eaf0fe; --ctx-ink:#3a5aa8; --ctx-line:#d9e3fb; }\n.toneMemory_1441117854 { --ctx:#8fb21e; --ctx-soft:#f1f5d8; --ctx-ink:#5a7012; --ctx-line:#dfe8bd; }\n.toneFiles_1441117854 { --ctx:#18a97b; --ctx-soft:#e6f6ee; --ctx-ink:#1c7a56; --ctx-line:#d2ecdf; }\n.toneResults_1441117854 { --ctx:#e8853f; --ctx-soft:#fdeee3; --ctx-ink:#a35d24; --ctx-line:#f7ddc8; }\n.toneConversation_1441117854 { --ctx:#c2327f; --ctx-soft:#fbe9f3; --ctx-ink:#8c2259; --ctx-line:#f2d3e4; }\n@media (max-width:700px) {\n .head_1441117854 { padding:11px 12px 0; }\n .headCollapsed_1441117854 { padding-bottom:11px; }\n .body_1441117854 { padding:9px 12px 12px; }\n .summary_1441117854 { grid-template-columns:minmax(0,1fr); gap:10px; }\n .total_1441117854 { font-size:24px; }\n .map_1441117854 { height:220px; }\n .model_1441117854 { display:none; }\n}\n@media (prefers-reduced-motion:reduce) {\n .chevron_1441117854 { transition:none; }\n .slot_1441117854 { transition:none; }\n}\n";
32836
+ const css$1 = "/* The context-window panel: magpie's shape on this page's surfaces — a header\n * with the model and a live pill, the used total beside the newest answer's\n * prompt-cache split, a treemap of what occupies the window, and one legend row\n * naming the six buckets. Category colors live as custom properties on the tone\n * classes so the tile and the legend dot share one palette. */\n.panel_1441117854 { background:#fff; border:1px solid #e4e7ee; border-radius:12px; margin:0 0 16px; overflow:hidden; container-type:inline-size; }\n.head_1441117854 { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:12px 16px 0; }\n.headCollapsed_1441117854 { padding-bottom:12px; }\n.toggle_1441117854 { display:flex; align-items:center; gap:6px; min-width:0; flex:none; background:none; border:0; padding:0; font:inherit; color:#394354; cursor:pointer; }\n.toggle_1441117854 h2 { font-size:13px; font-weight:600; margin:0; white-space:nowrap; }\n.chevron_1441117854 { flex:none; color:#99a1af; transition:transform .2s; }\n.chevronOpen_1441117854 { transform:rotate(90deg); }\n.headLead_1441117854 { display:flex; align-items:center; gap:8px; min-width:0; flex:1 1 auto; }\n/* Folded digest: the used/window figures plus a strip whose whole length is the\n * model's window — the tinted head is what the six buckets occupy, and the grey\n * track behind it is the room still free. A collapsed panel answers \"how full,\n * with what, and how much is left\" while it refreshes. */\n.digest_1441117854 { display:flex; align-items:center; gap:8px; min-width:0; flex:1 1 auto; }\n/* The figures never shrink — a truncated count is worse than no strip — so the\n * strip absorbs the shortfall and steps aside once it can no longer be read. */\n.digestTotal_1441117854 { flex:none; white-space:nowrap; font-size:11px; color:#6f7889; font-variant-numeric:tabular-nums; }\n/* The track is the free room: `#eef0f4` shows through after the used share. */\n.digestBar_1441117854 { display:flex; flex:1 1 auto; min-width:0; max-width:320px; height:8px; border-radius:999px; overflow:hidden; background:#e8ebf1; }\n.digestSeg_1441117854 { height:100%; flex:none; background:var(--ctx); transition:width .35s; }\n.headRight_1441117854 { display:flex; align-items:center; gap:8px; min-width:0; flex:none; }\n.model_1441117854 { min-width:0; max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; color:#8b93a4; }\n.livePill_1441117854 { display:inline-flex; align-items:center; gap:4px; flex:none; padding:1px 8px 1px 6px; border-radius:999px; background:#e9f8f1; color:#128a63; font-size:10px; font-weight:500; }\n.liveDot_1441117854 { width:5px; height:5px; border-radius:50%; background:#18a97b; box-shadow:0 0 0 2px #d6f2e5; }\n.liveTime_1441117854 { font-variant-numeric:tabular-nums; opacity:.85; }\n.toggle_1441117854:focus-visible { outline:2px solid #2ab1c6; outline-offset:2px; }\n.body_1441117854 { padding:10px 16px 14px; }\n.body_1441117854[hidden] { display:none; }\n.summary_1441117854 { display:grid; grid-template-columns:minmax(0,1fr) minmax(180px,260px); align-items:end; gap:18px; padding-bottom:12px; }\n.used_1441117854 { display:flex; flex-direction:column; gap:1px; min-width:0; }\n.label_1441117854 { font-size:11px; color:#8790a3; }\n.total_1441117854 { font-size:28px; font-weight:650; line-height:1.15; letter-spacing:-.6px; color:#1b2130; font-variant-numeric:tabular-nums; white-space:nowrap; }\n.total_1441117854 small { margin-left:4px; font-size:11px; font-weight:500; letter-spacing:0; color:#98a0ae; }\n.window_1441117854 { font-size:11px; color:#939aab; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.cache_1441117854 { display:flex; flex-direction:column; gap:6px; min-width:0; }\n.cacheBar_1441117854 { display:flex; width:100%; height:5px; border-radius:999px; overflow:hidden; background:#eef0f4; }\n.cacheHit_1441117854 { height:100%; background:#2ec4a0; }\n.cacheMiss_1441117854 { height:100%; background:#dfe3ea; }\n.cacheEmpty_1441117854 { display:block; width:100%; height:100%; background:repeating-linear-gradient(90deg,#e6e9ef 0 3px,transparent 3px 7px); }\n.cacheLegend_1441117854 { display:flex; flex-wrap:wrap; gap:2px 10px; font-size:10px; color:#8b93a4; font-variant-numeric:tabular-nums; }\n.cacheLegend_1441117854 i { display:inline-block; width:6px; height:6px; margin-right:4px; border-radius:50%; }\n.cacheHitDot_1441117854 { background:#2ec4a0; }\n.cacheMissDot_1441117854 { background:#dfe3ea; }\n.cachePending_1441117854 { color:#9aa3b3; }\n.note_1441117854 { margin:0 0 10px; padding:5px 8px; border:1px solid #f2e4c6; border-radius:6px; background:#fdf7ea; font-size:10.5px; line-height:1.5; color:#8a6a2a; font-variant-numeric:tabular-nums; }\n.map_1441117854 { position:relative; height:268px; border-radius:9px; overflow:hidden; }\n/* Slots glide into their new geometry when a refresh re-writes the estimate,\n * so a change in the data reads as motion instead of a silent repaint. */\n.slot_1441117854 { position:absolute; box-sizing:border-box; padding:1.5px; transition:left .35s, top .35s, width .35s, height .35s; }\n.tile_1441117854 { display:flex; flex-direction:column; gap:1px; box-sizing:border-box; width:100%; height:100%; padding:5px 6px; border:1px solid var(--ctx-line); border-radius:7px; background:var(--ctx-soft); color:var(--ctx-ink); overflow:hidden; }\n.tileLabel_1441117854 { font-size:11px; font-weight:600; line-height:1.25; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }\n.tileSize_1441117854 { font-size:10px; opacity:.72; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-variant-numeric:tabular-nums; }\n/* The hover card: exact figures a tile has no room to print. */\n.tip_1441117854 { position:absolute; z-index:2; box-sizing:border-box; width:220px; padding:8px 10px; border:1px solid #e0e4ec; border-radius:8px; background:#fff; box-shadow:0 8px 20px rgba(23,32,54,.12); font-size:11px; line-height:1.55; pointer-events:none; }\n.tipName_1441117854 { font-weight:600; color:#1b2130; overflow-wrap:anywhere; }\n.tipMeta_1441117854 { color:#6f7889; font-variant-numeric:tabular-nums; }\n.tipShare_1441117854 { color:#4a5567; font-variant-numeric:tabular-nums; }\n.legend_1441117854 { display:flex; flex-wrap:wrap; gap:5px 14px; list-style:none; margin:0; padding:10px 0 0; border-top:1px solid #f2f4f8; margin-top:10px; font-size:11px; color:#6f7889; }\n.legend_1441117854 li { display:flex; align-items:center; gap:5px; }\n.legendName_1441117854 { white-space:nowrap; }\n.legendValue_1441117854 { color:#98a0ae; font-variant-numeric:tabular-nums; }\n.swatch_1441117854 { display:inline-block; flex:none; box-sizing:border-box; width:9px; height:9px; border-radius:3px; background:var(--ctx-soft); background:color-mix(in srgb, var(--ctx) 50%, var(--ctx-soft)); border:1px solid var(--ctx-line); }\n.empty_1441117854 { margin:0; padding:24px 0; text-align:center; font-size:12px; color:#9aa2b1; }\n/* Category palette: `--ctx` is the accent tint, `--ctx-soft` the tile fill (and\n * the legend swatch, so the dots match the tiles exactly), `--ctx-ink` the tile\n * text, `--ctx-line` its hairline border. */\n.toneSystem_1441117854 { --ctx:#a346e6; --ctx-soft:#f6ecfd; --ctx-ink:#6c2e9e; --ctx-line:#ecdafa; }\n.toneTools_1441117854 { --ctx:#4f79f6; --ctx-soft:#eaf0fe; --ctx-ink:#3a5aa8; --ctx-line:#d9e3fb; }\n.toneMemory_1441117854 { --ctx:#8fb21e; --ctx-soft:#f1f5d8; --ctx-ink:#5a7012; --ctx-line:#dfe8bd; }\n.toneFiles_1441117854 { --ctx:#18a97b; --ctx-soft:#e6f6ee; --ctx-ink:#1c7a56; --ctx-line:#d2ecdf; }\n.toneResults_1441117854 { --ctx:#e8853f; --ctx-soft:#fdeee3; --ctx-ink:#a35d24; --ctx-line:#f7ddc8; }\n.toneConversation_1441117854 { --ctx:#c2327f; --ctx-soft:#fbe9f3; --ctx-ink:#8c2259; --ctx-line:#f2d3e4; }\n@media (max-width:700px) {\n .head_1441117854 { padding:11px 12px 0; }\n .headCollapsed_1441117854 { padding-bottom:11px; }\n .body_1441117854 { padding:9px 12px 12px; }\n .summary_1441117854 { grid-template-columns:minmax(0,1fr); gap:10px; }\n .total_1441117854 { font-size:24px; }\n .map_1441117854 { height:220px; }\n .model_1441117854 { display:none; }\n}\n/* Container queries measure this card, not the page — the panel sits in a\n * column, so below ~620px the strip is no longer readable and steps aside; the\n * figures keep every number, and the tooltip still holds the split. */\n@container (max-width:620px) {\n .digestBar_1441117854 { display:none; }\n .model_1441117854 { display:none; }\n}\n@media (prefers-reduced-motion:reduce) {\n .chevron_1441117854 { transition:none; }\n .slot_1441117854 { transition:none; }\n .digestSeg_1441117854 { transition:none; }\n}\n";
32837
32837
  const tagId$1 = "dsh-token-total/ContextWindowPanel.module.css";
32838
32838
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId$1 + "\"]") === null) {
32839
32839
  const tag = document.createElement("style");
@@ -32849,6 +32849,11 @@ window.__ModuleLoader__.load({
32849
32849
  "toggle": "toggle_1441117854",
32850
32850
  "chevron": "chevron_1441117854",
32851
32851
  "chevronOpen": "chevronOpen_1441117854",
32852
+ "headLead": "headLead_1441117854",
32853
+ "digest": "digest_1441117854",
32854
+ "digestTotal": "digestTotal_1441117854",
32855
+ "digestBar": "digestBar_1441117854",
32856
+ "digestSeg": "digestSeg_1441117854",
32852
32857
  "headRight": "headRight_1441117854",
32853
32858
  "model": "model_1441117854",
32854
32859
  "livePill": "livePill_1441117854",
@@ -33073,30 +33078,63 @@ window.__ModuleLoader__.load({
33073
33078
  value: formatCompact(context.contextWindow),
33074
33079
  percent: String(percent ?? 0)
33075
33080
  });
33081
+ const categoryTotal = totals.reduce((sum, row) => sum + row.tokens, 0);
33082
+ const windowKnown = context.contextWindow !== null && context.contextWindow > 0;
33083
+ const usedShare = windowKnown ? Math.min(100, Math.max(0, reading.usedTokens / context.contextWindow * 100)) : 100;
33084
+ const freeTokens = windowKnown ? Math.max(0, context.contextWindow - reading.usedTokens) : 0;
33085
+ const digestLabel = [
33086
+ `${t("contextUsed")} ${formatTokens(reading.usedTokens)} token`,
33087
+ ...totals.map((row) => `${t(CATEGORY_LABEL[row.key])} ${formatCompact(row.tokens)}`),
33088
+ windowKnown ? `${t("contextFree")} ${formatTokens(freeTokens)} token` : windowText
33089
+ ].join(" · ");
33090
+ const digestText = context.contextWindow === null ? t("contextDigestUnknown", { used: formatCompact(reading.usedTokens) }) : t("contextDigest", {
33091
+ used: formatCompact(reading.usedTokens),
33092
+ window: formatCompact(context.contextWindow),
33093
+ percent: String(percent ?? 0)
33094
+ });
33076
33095
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
33077
33096
  className: ContextWindowPanel_module_default.panel,
33078
33097
  "data-dsh-context-window": true,
33079
33098
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
33080
33099
  className: open ? ContextWindowPanel_module_default.head : `${ContextWindowPanel_module_default.head} ${ContextWindowPanel_module_default.headCollapsed}`,
33081
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
33082
- type: "button",
33083
- className: ContextWindowPanel_module_default.toggle,
33084
- onClick: () => setOpen(!open),
33085
- "aria-expanded": open,
33086
- "aria-controls": "dsh-context-window-body",
33087
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
33088
- className: `${ContextWindowPanel_module_default.chevron} ${open ? ContextWindowPanel_module_default.chevronOpen : ""}`,
33089
- width: "12",
33090
- height: "12",
33091
- viewBox: "0 0 12 12",
33092
- "aria-hidden": "true",
33093
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
33094
- d: "m4 2 4 4-4 4",
33095
- stroke: "currentColor",
33096
- strokeWidth: "1.5",
33097
- fill: "none"
33098
- })
33099
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("contextTitle") })]
33100
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
33101
+ className: ContextWindowPanel_module_default.headLead,
33102
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
33103
+ type: "button",
33104
+ className: ContextWindowPanel_module_default.toggle,
33105
+ onClick: () => setOpen(!open),
33106
+ "aria-expanded": open,
33107
+ "aria-controls": "dsh-context-window-body",
33108
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
33109
+ className: `${ContextWindowPanel_module_default.chevron} ${open ? ContextWindowPanel_module_default.chevronOpen : ""}`,
33110
+ width: "12",
33111
+ height: "12",
33112
+ viewBox: "0 0 12 12",
33113
+ "aria-hidden": "true",
33114
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
33115
+ d: "m4 2 4 4-4 4",
33116
+ stroke: "currentColor",
33117
+ strokeWidth: "1.5",
33118
+ fill: "none"
33119
+ })
33120
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("contextTitle") })]
33121
+ }), !open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
33122
+ className: ContextWindowPanel_module_default.digest,
33123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33124
+ className: ContextWindowPanel_module_default.digestTotal,
33125
+ title: `${t("contextUsed")} ${formatTokens(reading.usedTokens)} token · ${windowText}`,
33126
+ children: digestText
33127
+ }), categoryTotal > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33128
+ className: ContextWindowPanel_module_default.digestBar,
33129
+ role: "img",
33130
+ "aria-label": digestLabel,
33131
+ title: digestLabel,
33132
+ children: totals.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33133
+ className: `${ContextWindowPanel_module_default.digestSeg} ${CATEGORY_TONE[row.key]}`,
33134
+ style: { width: `${row.tokens / categoryTotal * usedShare}%` }
33135
+ }, row.key))
33136
+ })]
33137
+ })]
33100
33138
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
33101
33139
  className: ContextWindowPanel_module_default.headRight,
33102
33140
  children: [model !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -34118,6 +34156,9 @@ window.__ModuleLoader__.load({
34118
34156
  contextLive: "实时",
34119
34157
  contextWindowKnown: "模型窗口 {value} · 已用 {percent}%",
34120
34158
  contextWindowUnknown: "模型的窗口大小未知",
34159
+ contextDigest: "{used} / {window} · {percent}%",
34160
+ contextDigestUnknown: "{used} / 窗口未知",
34161
+ contextFree: "空闲",
34121
34162
  contextCache: "提示词缓存",
34122
34163
  contextCacheHit: "命中",
34123
34164
  contextCacheMiss: "未命中",
@@ -34206,6 +34247,9 @@ window.__ModuleLoader__.load({
34206
34247
  contextLive: "Live",
34207
34248
  contextWindowKnown: "Window {value} · {percent}% used",
34208
34249
  contextWindowUnknown: "Model window size unknown",
34250
+ contextDigest: "{used} / {window} · {percent}%",
34251
+ contextDigestUnknown: "{used} / window unknown",
34252
+ contextFree: "free",
34209
34253
  contextCache: "Prompt cache",
34210
34254
  contextCacheHit: "hit",
34211
34255
  contextCacheMiss: "miss",
@@ -34294,6 +34338,9 @@ window.__ModuleLoader__.load({
34294
34338
  contextLive: "即時",
34295
34339
  contextWindowKnown: "模型視窗 {value} · 已用 {percent}%",
34296
34340
  contextWindowUnknown: "模型的視窗大小未知",
34341
+ contextDigest: "{used} / {window} · {percent}%",
34342
+ contextDigestUnknown: "{used} / 視窗未知",
34343
+ contextFree: "空閒",
34297
34344
  contextCache: "提示詞快取",
34298
34345
  contextCacheHit: "命中",
34299
34346
  contextCacheMiss: "未命中",
@@ -64,6 +64,9 @@ export declare const usageZh: {
64
64
  contextLive: string;
65
65
  contextWindowKnown: string;
66
66
  contextWindowUnknown: string;
67
+ contextDigest: string;
68
+ contextDigestUnknown: string;
69
+ contextFree: string;
67
70
  contextCache: string;
68
71
  contextCacheHit: string;
69
72
  contextCacheMiss: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-token-total",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "DSH web plugin: token 用量统计看板 + 单会话(主智能体 + 子智能体)用量统计。提供今日/近3天/近7天/近1月/近3月/自定义筛选、六个汇总卡片、活跃热力图、缓存命中率折线与分模型柱状图、按模型/按供应商 Top5 列表。",
5
5
  "icon": "./icon.svg",
6
6
  "type": "module",
@@ -3,14 +3,26 @@
3
3
  * prompt-cache split, a treemap of what occupies the window, and one legend row
4
4
  * naming the six buckets. Category colors live as custom properties on the tone
5
5
  * classes so the tile and the legend dot share one palette. */
6
- .panel { background:#fff; border:1px solid #e4e7ee; border-radius:12px; margin:0 0 16px; overflow:hidden; }
6
+ .panel { background:#fff; border:1px solid #e4e7ee; border-radius:12px; margin:0 0 16px; overflow:hidden; container-type:inline-size; }
7
7
  .head { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:12px 16px 0; }
8
8
  .headCollapsed { padding-bottom:12px; }
9
- .toggle { display:flex; align-items:center; gap:6px; min-width:0; background:none; border:0; padding:0; font:inherit; color:#394354; cursor:pointer; }
9
+ .toggle { display:flex; align-items:center; gap:6px; min-width:0; flex:none; background:none; border:0; padding:0; font:inherit; color:#394354; cursor:pointer; }
10
10
  .toggle h2 { font-size:13px; font-weight:600; margin:0; white-space:nowrap; }
11
11
  .chevron { flex:none; color:#99a1af; transition:transform .2s; }
12
12
  .chevronOpen { transform:rotate(90deg); }
13
- .headRight { display:flex; align-items:center; gap:8px; min-width:0; }
13
+ .headLead { display:flex; align-items:center; gap:8px; min-width:0; flex:1 1 auto; }
14
+ /* Folded digest: the used/window figures plus a strip whose whole length is the
15
+ * model's window — the tinted head is what the six buckets occupy, and the grey
16
+ * track behind it is the room still free. A collapsed panel answers "how full,
17
+ * with what, and how much is left" while it refreshes. */
18
+ .digest { display:flex; align-items:center; gap:8px; min-width:0; flex:1 1 auto; }
19
+ /* The figures never shrink — a truncated count is worse than no strip — so the
20
+ * strip absorbs the shortfall and steps aside once it can no longer be read. */
21
+ .digestTotal { flex:none; white-space:nowrap; font-size:11px; color:#6f7889; font-variant-numeric:tabular-nums; }
22
+ /* The track is the free room: `#eef0f4` shows through after the used share. */
23
+ .digestBar { display:flex; flex:1 1 auto; min-width:0; max-width:320px; height:8px; border-radius:999px; overflow:hidden; background:#e8ebf1; }
24
+ .digestSeg { height:100%; flex:none; background:var(--ctx); transition:width .35s; }
25
+ .headRight { display:flex; align-items:center; gap:8px; min-width:0; flex:none; }
14
26
  .model { min-width:0; max-width:220px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:11px; color:#8b93a4; }
15
27
  .livePill { display:inline-flex; align-items:center; gap:4px; flex:none; padding:1px 8px 1px 6px; border-radius:999px; background:#e9f8f1; color:#128a63; font-size:10px; font-weight:500; }
16
28
  .liveDot { width:5px; height:5px; border-radius:50%; background:#18a97b; box-shadow:0 0 0 2px #d6f2e5; }
@@ -71,7 +83,15 @@
71
83
  .map { height:220px; }
72
84
  .model { display:none; }
73
85
  }
86
+ /* Container queries measure this card, not the page — the panel sits in a
87
+ * column, so below ~620px the strip is no longer readable and steps aside; the
88
+ * figures keep every number, and the tooltip still holds the split. */
89
+ @container (max-width:620px) {
90
+ .digestBar { display:none; }
91
+ .model { display:none; }
92
+ }
74
93
  @media (prefers-reduced-motion:reduce) {
75
94
  .chevron { transition:none; }
76
95
  .slot { transition:none; }
96
+ .digestSeg { transition:none; }
77
97
  }
@@ -157,14 +157,52 @@ export function ContextWindowPanel({ context, t, updatedAt }: {
157
157
  value: formatCompact(context.contextWindow),
158
158
  percent: String(percent ?? 0),
159
159
  })
160
+ // Folded, the body's figures are out of sight, so the header carries the
161
+ // digest itself: used / window · share, then a strip whose whole length is
162
+ // the model's window — the tinted part is what is in use, cut into the six
163
+ // buckets in legend order, and the grey tail is the room still free. So the
164
+ // bar reads as occupancy, not as a share of itself. Both read the same
165
+ // snapshot the body does, so every poll redraws them in place — the figures
166
+ // and the proportions stay live without the panel ever being opened.
167
+ const categoryTotal = totals.reduce((sum, row) => sum + row.tokens, 0)
168
+ // Without a reported window there is no "free room" to draw: the strip spans
169
+ // what is in use instead of inventing a denominator.
170
+ const windowKnown = context.contextWindow !== null && context.contextWindow > 0
171
+ const usedShare = windowKnown
172
+ ? Math.min(100, Math.max(0, reading.usedTokens / context.contextWindow! * 100))
173
+ : 100
174
+ const freeTokens = windowKnown ? Math.max(0, context.contextWindow! - reading.usedTokens) : 0
175
+ const digestLabel = [
176
+ `${t('contextUsed')} ${formatTokens(reading.usedTokens)} token`,
177
+ ...totals.map(row => `${t(CATEGORY_LABEL[row.key])} ${formatCompact(row.tokens)}`),
178
+ windowKnown ? `${t('contextFree')} ${formatTokens(freeTokens)} token` : windowText,
179
+ ].join(' · ')
180
+ const digestText = context.contextWindow === null
181
+ ? t('contextDigestUnknown', { used: formatCompact(reading.usedTokens) })
182
+ : t('contextDigest', {
183
+ used: formatCompact(reading.usedTokens),
184
+ window: formatCompact(context.contextWindow),
185
+ percent: String(percent ?? 0),
186
+ })
160
187
  return <section className={css.panel} data-dsh-context-window>
161
188
  {/* Collapsed, the body's spacing is gone, so the header pads its own
162
189
  bottom — otherwise the folded row reads heavy on top. */}
163
190
  <header className={open ? css.head : `${css.head} ${css.headCollapsed}`}>
164
- <button type="button" className={css.toggle} onClick={() => setOpen(!open)} aria-expanded={open} aria-controls="dsh-context-window-body">
165
- <svg className={`${css.chevron} ${open ? css.chevronOpen : ''}`} width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="m4 2 4 4-4 4" stroke="currentColor" strokeWidth="1.5" fill="none" /></svg>
166
- <h2>{t('contextTitle')}</h2>
167
- </button>
191
+ <div className={css.headLead}>
192
+ <button type="button" className={css.toggle} onClick={() => setOpen(!open)} aria-expanded={open} aria-controls="dsh-context-window-body">
193
+ <svg className={`${css.chevron} ${open ? css.chevronOpen : ''}`} width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="m4 2 4 4-4 4" stroke="currentColor" strokeWidth="1.5" fill="none" /></svg>
194
+ <h2>{t('contextTitle')}</h2>
195
+ </button>
196
+ {!open && <div className={css.digest}>
197
+ <span className={css.digestTotal} title={`${t('contextUsed')} ${formatTokens(reading.usedTokens)} token · ${windowText}`}>
198
+ {digestText}
199
+ </span>
200
+ {categoryTotal > 0 && <span className={css.digestBar} role="img" aria-label={digestLabel} title={digestLabel}>
201
+ {totals.map(row => <span key={row.key} className={`${css.digestSeg} ${CATEGORY_TONE[row.key]}`}
202
+ style={{ width: `${row.tokens / categoryTotal * usedShare}%` }} />)}
203
+ </span>}
204
+ </div>}
205
+ </div>
168
206
  <div className={css.headRight}>
169
207
  {model !== '' && <span className={css.model} title={`${context.provider} / ${context.model}`}>{model}</span>}
170
208
  <span className={css.livePill}>
@@ -18,6 +18,8 @@ export const usageZh = {
18
18
  live: '实时更新', asOf: '统计截至', refresh: '刷新',
19
19
  contextTitle: '上下文窗口', contextUsed: '已用上下文', contextLive: '实时',
20
20
  contextWindowKnown: '模型窗口 {value} · 已用 {percent}%', contextWindowUnknown: '模型的窗口大小未知',
21
+ contextDigest: '{used} / {window} · {percent}%', contextDigestUnknown: '{used} / 窗口未知',
22
+ contextFree: '空闲',
21
23
  contextCache: '提示词缓存', contextCacheHit: '命中', contextCacheMiss: '未命中',
22
24
  contextCachePending: '供应商回答可见',
23
25
  contextSystem: '系统提示词', contextTools: '工具', contextMemory: '记忆',
@@ -48,6 +50,8 @@ export const usageEn: Record<SessionUsageKey, string> = {
48
50
  live: 'Live', asOf: 'as of', refresh: 'Refresh',
49
51
  contextTitle: 'Context window', contextUsed: 'Context used', contextLive: 'Live',
50
52
  contextWindowKnown: 'Window {value} · {percent}% used', contextWindowUnknown: 'Model window size unknown',
53
+ contextDigest: '{used} / {window} · {percent}%', contextDigestUnknown: '{used} / window unknown',
54
+ contextFree: 'free',
51
55
  contextCache: 'Prompt cache', contextCacheHit: 'hit', contextCacheMiss: 'miss',
52
56
  contextCachePending: 'Visible once the provider answers',
53
57
  contextSystem: 'System prompt', contextTools: 'Tools', contextMemory: 'Memory',
@@ -76,6 +80,8 @@ export const usageZhTW: Record<SessionUsageKey, string> = {
76
80
  live: '即時更新', asOf: '統計截至', refresh: '重新整理',
77
81
  contextTitle: '上下文視窗', contextUsed: '已用上下文', contextLive: '即時',
78
82
  contextWindowKnown: '模型視窗 {value} · 已用 {percent}%', contextWindowUnknown: '模型的視窗大小未知',
83
+ contextDigest: '{used} / {window} · {percent}%', contextDigestUnknown: '{used} / 視窗未知',
84
+ contextFree: '空閒',
79
85
  contextCache: '提示詞快取', contextCacheHit: '命中', contextCacheMiss: '未命中',
80
86
  contextCachePending: '供應商回答可見',
81
87
  contextSystem: '系統提示詞', contextTools: '工具', contextMemory: '記憶',