dsh-context 0.19.2 → 0.20.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/lib/client.js CHANGED
@@ -29,6 +29,12 @@ var DICT_ZH = {
29
29
  "stats.compactions": "\u538B\u7F29",
30
30
  "stats.prunes": "\u526A\u679D",
31
31
  "stats.cacheHit": "\u7F13\u5B58\u547D\u4E2D",
32
+ "stats.cost": "\u9884\u4F30\u8D39\u7528",
33
+ "stats.costTip": "\u6309 DeepSeek \u5B98\u65B9\u520A\u4F8B\u4EF7\u4F30\u7B97\u6574\u4E2A\u4F1A\u8BDD\u7684\u7D2F\u8BA1\u8D39\u7528\uFF1A\u8F93\u5165\u533A\u5206\u7F13\u5B58\u547D\u4E2D/\u672A\u547D\u4E2D\uFF0C\u8F93\u51FA\u542B\u601D\u8003\uFF1B\u6309\u8BF7\u6C42\u65F6\u95F4\u533A\u5206\u9AD8\u5CF0\uFF08UTC 01:00\u201304:00\u300106:00\u201310:00\uFF09\u4E0E\u7A7A\u95F2\u65F6\u6BB5\uFF08\u534A\u4EF7\uFF09\u3002\u9002\u7528\u4E8E deepseek-v4-flash / deepseek-v4-pro\uFF08\u4E0D\u9650 provider\uFF09\uFF0C\u4EF7\u683C\u5199\u6B7B\u5728\u4EE3\u7801\u4E2D\uFF0C\u4EC5\u4F9B\u53C2\u8003\u3002",
34
+ "stats.costPriceHead": "\u6BCF\u767E\u4E07 tokens \u4EF7\u683C\uFF08\u9AD8\u5CF0 | \u7A7A\u95F2\u534A\u4EF7\uFF09\uFF1A",
35
+ "stats.costHit": "\u547D\u4E2D",
36
+ "stats.costMiss": "\u672A\u547D\u4E2D",
37
+ "stats.costOut": "\u8F93\u51FA",
32
38
  "plugin.title": "\u63D2\u4EF6\u4FE1\u606F",
33
39
  "plugin.hint": "The best DSH context plugin \u2B50",
34
40
  "plugin.name": "\u63D2\u4EF6",
@@ -133,6 +139,12 @@ var DICT_EN = {
133
139
  "stats.compactions": "Compactions",
134
140
  "stats.prunes": "Prunes",
135
141
  "stats.cacheHit": "Cache Hits",
142
+ "stats.cost": "Est. Cost",
143
+ "stats.costTip": "Rough cumulative cost of the whole session at DeepSeek\u2019s list prices: input split by cache hit/miss, output includes reasoning; each request is priced at the peak (01:00\u201304:00, 06:00\u201310:00 UTC) or half-price off-peak rate by its time. Applies to deepseek-v4-flash / deepseek-v4-pro on any provider; prices are hardcoded, for reference only.",
144
+ "stats.costPriceHead": "Per-1M-token rates (peak | off-peak at half price):",
145
+ "stats.costHit": "hit",
146
+ "stats.costMiss": "miss",
147
+ "stats.costOut": "output",
136
148
  "plugin.title": "Plugin Info",
137
149
  "plugin.hint": "The best DSH context plugin \u2B50",
138
150
  "plugin.name": "Plugin",
@@ -1053,6 +1065,23 @@ var STYLES = [
1053
1065
  ".lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
1054
1066
  ".lc-stat-value { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }",
1055
1067
  ".lc-stat-sub { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
1068
+ // Stats cell tooltip (cost cell): the explanation renders as styled DOM
1069
+ // because the harness GUI never shows the native `title` tooltip. A "?"
1070
+ // marker after the label signals the affordance; hovering ANYWHERE on the
1071
+ // cell reveals the bubble BELOW it (the cell is the tip's position frame).
1072
+ // The bubble may overlay the card content underneath — nothing below clips
1073
+ // it, and z-index 6 matches the chart tips — so it can exceed the card
1074
+ // without being cut off. Same chrome as `.lc-bar-tip`: layer-2 fill, border,
1075
+ // 6px radius, soft shadow, 120ms opacity ease.
1076
+ ".lc-stat-tipped { position: relative; }",
1077
+ ".lc-stat-q { display: inline-block; margin-left: 4px; width: 11px; height: 11px; line-height: 11px; border-radius: 50%; text-align: center; font-size: 9px; font-weight: 700; font-style: normal; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); cursor: help; }",
1078
+ ".lc-stat-tipped:hover .lc-stat-q { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-primary); }",
1079
+ ".lc-stat-tip { position: absolute; top: calc(100% + 6px); left: 0; z-index: 6; width: max-content; max-width: 280px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 6px 10px; font-size: 12px; line-height: 1.5; font-weight: 400; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; opacity: 0; transition: opacity 120ms ease; }",
1080
+ ".lc-stat-tipped:hover .lc-stat-tip { opacity: 1; }",
1081
+ ".lc-stat-tip-prices { display: block; margin-top: 5px; border-top: 1px dashed var(--dsw-alias-border-l1); padding-top: 5px; }",
1082
+ ".lc-stat-tip-head { display: block; font-weight: 600; }",
1083
+ ".lc-stat-tip-row { display: block; margin-top: 2px; }",
1084
+ ".lc-stat-tip-model { color: var(--dsw-alias-label-primary); }",
1056
1085
  ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1057
1086
  ".lc-gran, .lc-kinds { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
1058
1087
  ".lc-gran-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; }",
@@ -1443,7 +1472,7 @@ function makeEventList(kit) {
1443
1472
 
1444
1473
  // src/client/meta.ts
1445
1474
  var PLUGIN_NAME = "dsh-context";
1446
- var PLUGIN_VERSION = true ? "0.19.2" : "0.0.0-dev";
1475
+ var PLUGIN_VERSION = true ? "0.20.0" : "0.0.0-dev";
1447
1476
  var PLUGIN_REPO = true ? "https://github.com/bowenliang123/dsh-context" : "https://github.com/bowenliang123/dsh-context";
1448
1477
  var PLUGIN_REPO_SHORT = PLUGIN_REPO.replace(/^https?:\/\/github\.com\//, "");
1449
1478
 
@@ -1512,6 +1541,49 @@ function makeRequestDetail(kit, StackedBar) {
1512
1541
  };
1513
1542
  }
1514
1543
 
1544
+ // src/client/cost.ts
1545
+ var PRICES = {
1546
+ usd: {
1547
+ flash: { peak: { hit: 0.014, miss: 0.44, out: 1.32 }, off: { hit: 7e-3, miss: 0.22, out: 0.66 } },
1548
+ pro: { peak: { hit: 0.044, miss: 1.32, out: 3.96 }, off: { hit: 0.022, miss: 0.66, out: 1.98 } }
1549
+ },
1550
+ cny: {
1551
+ flash: { peak: { hit: 0.1, miss: 3, out: 9 }, off: { hit: 0.05, miss: 1.5, out: 4.5 } },
1552
+ pro: { peak: { hit: 0.3, miss: 9, out: 27 }, off: { hit: 0.15, miss: 4.5, out: 13.5 } }
1553
+ }
1554
+ };
1555
+ function estimateSessionCost(usage, currency) {
1556
+ if (usage === null || usage === void 0) return null;
1557
+ let total = 0;
1558
+ let any = false;
1559
+ for (const family of ["flash", "pro"]) {
1560
+ const fam = usage[family];
1561
+ if (fam === void 0) continue;
1562
+ for (const period of ["peak", "off"]) {
1563
+ const b = fam[period];
1564
+ if (b === void 0) continue;
1565
+ const p = PRICES[currency][family][period];
1566
+ total += (b.cacheRead * p.hit + (b.uncached + b.cacheWrite) * p.miss + b.output * p.out) / 1e6;
1567
+ any = true;
1568
+ }
1569
+ }
1570
+ return any ? total : null;
1571
+ }
1572
+ function formatCost(amount, currency) {
1573
+ const symbol = currency === "cny" ? "\xA5" : "$";
1574
+ return symbol + (amount >= 1 ? amount.toFixed(2) : amount.toPrecision(2));
1575
+ }
1576
+ function sessionPrices(currency) {
1577
+ return ["flash", "pro"].map((id) => ({
1578
+ family: id === "flash" ? "deepseek-v4-flash" : "deepseek-v4-pro",
1579
+ peak: PRICES[currency][id].peak,
1580
+ off: PRICES[currency][id].off
1581
+ }));
1582
+ }
1583
+ function formatPriceRate(amount, currency) {
1584
+ return formatCost(amount, currency).replace(/0+$/, "").replace(/\.$/, "");
1585
+ }
1586
+
1515
1587
  // src/client/components/statsBoard.tsx
1516
1588
  function cacheHitPercent(usage) {
1517
1589
  const billed = usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
@@ -1534,8 +1606,15 @@ function makeStatsBoard(kit) {
1534
1606
  else if (ev.kind === "inject") injects++;
1535
1607
  }
1536
1608
  const hit = props.usage !== null ? cacheHitPercent(props.usage) : null;
1537
- const cell = (label, value) => /* @__PURE__ */ React.createElement("div", { className: "lc-stat" }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-label" }, label), /* @__PURE__ */ React.createElement("b", { className: "lc-stat-value" }, value));
1538
- return /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col lc-col-stats" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("stats.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("stats.hint"))), /* @__PURE__ */ React.createElement("div", { className: "lc-stats" }, cell(t("stats.turns"), fmt3(turns.size)), cell(t("stats.steps"), fmt3(steps)), cell(t("stats.injects"), fmt3(injects)), cell(t("stats.compactions"), fmt3(compactions)), cell(t("stats.prunes"), fmt3(prunes)), cell(t("stats.cacheHit"), hit === null ? "\u2014" : hit + "%")));
1609
+ const currency = props.locale === "zh" ? "cny" : "usd";
1610
+ const cost = estimateSessionCost(props.cost, currency);
1611
+ const fmtRate = (n) => formatPriceRate(n, currency);
1612
+ const costTip = [
1613
+ t("stats.costTip"),
1614
+ /* @__PURE__ */ React.createElement("span", { key: "prices", className: "lc-stat-tip-prices" }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-tip-head" }, t("stats.costPriceHead")), sessionPrices(currency).map((r) => /* @__PURE__ */ React.createElement("span", { key: r.family, className: "lc-stat-tip-row" }, /* @__PURE__ */ React.createElement("b", { className: "lc-stat-tip-model" }, r.family), " ", t("stats.costHit"), " ", fmtRate(r.peak.hit), "/", fmtRate(r.off.hit), " \xB7 ", t("stats.costMiss"), " ", fmtRate(r.peak.miss), "/", fmtRate(r.off.miss), " \xB7 ", t("stats.costOut"), " ", fmtRate(r.peak.out), "/", fmtRate(r.off.out))))
1615
+ ];
1616
+ const cell = (label, value, tip) => /* @__PURE__ */ React.createElement("div", { className: "lc-stat" + (tip === void 0 ? "" : " lc-stat-tipped") }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-label" }, label, tip !== void 0 && /* @__PURE__ */ React.createElement("i", { className: "lc-stat-q", "aria-hidden": "true" }, "?")), /* @__PURE__ */ React.createElement("b", { className: "lc-stat-value" }, value), tip !== void 0 && /* @__PURE__ */ React.createElement("span", { className: "lc-stat-tip", role: "tooltip" }, tip));
1617
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col lc-col-stats" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("stats.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("stats.hint"))), /* @__PURE__ */ React.createElement("div", { className: "lc-stats" }, cell(t("stats.turns"), fmt3(turns.size)), cell(t("stats.steps"), fmt3(steps)), cell(t("stats.injects"), fmt3(injects)), cell(t("stats.compactions"), fmt3(compactions)), cell(t("stats.prunes"), fmt3(prunes)), cell(t("stats.cacheHit"), hit === null ? "\u2014" : hit + "%"), cell(t("stats.cost"), cost === null ? "\u2014" : formatCost(cost, currency), costTip)));
1539
1618
  };
1540
1619
  }
1541
1620
 
@@ -1777,9 +1856,9 @@ function makeContextView(ctx, kit) {
1777
1856
  };
1778
1857
  }, [sessionId]);
1779
1858
  React.useEffect(() => {
1780
- const localeSvc = ctx.get("locale");
1781
- if (!localeSvc) return void 0;
1782
- return localeSvc.subscribe(() => setTick((x) => x + 1));
1859
+ const localeSvc2 = ctx.get("locale");
1860
+ if (!localeSvc2) return void 0;
1861
+ return localeSvc2.subscribe(() => setTick((x) => x + 1));
1783
1862
  }, []);
1784
1863
  void tick;
1785
1864
  if (!data) {
@@ -1811,7 +1890,9 @@ function makeContextView(ctx, kit) {
1811
1890
  }
1812
1891
  }
1813
1892
  const head = headlineOf(data, pressure);
1814
- return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-cols lc-head" }, /* @__PURE__ */ React.createElement(StatsBoard, { requests, events, usage }), /* @__PURE__ */ React.createElement(PluginInfo, null)), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement(
1893
+ const localeSvc = ctx.get("locale");
1894
+ const activeLocale = localeSvc !== void 0 && typeof localeSvc.getLocale === "function" ? localeSvc.getLocale().active : "en";
1895
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-cols lc-head" }, /* @__PURE__ */ React.createElement(StatsBoard, { requests, events, usage, cost: data.cost, locale: activeLocale }), /* @__PURE__ */ React.createElement(PluginInfo, null)), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement(
1815
1896
  CurrentComposition,
1816
1897
  {
1817
1898
  head,
package/lib/index.js CHANGED
@@ -310,6 +310,36 @@ function applySurface(st, ev, type, data, message) {
310
310
  st.sums[cat] += node.tokens;
311
311
  return node;
312
312
  }
313
+ function costFamilyOf(model) {
314
+ if (model === void 0) return null;
315
+ const m = model.toLowerCase();
316
+ if (!m.includes("v4")) return null;
317
+ if (m.includes("flash")) return "flash";
318
+ if (m.includes("pro")) return "pro";
319
+ return null;
320
+ }
321
+ function isPeakUtc(time) {
322
+ const h = new Date(time).getUTCHours();
323
+ return h >= 1 && h < 4 || h >= 6 && h < 10;
324
+ }
325
+ function accumulateCost(st, time, usage) {
326
+ const family = costFamilyOf(st.model);
327
+ if (family === null) return;
328
+ const prev = st.cost ?? {};
329
+ const fam = prev[family] ?? {};
330
+ const period = isPeakUtc(time) ? "peak" : "off";
331
+ const b = fam[period] ?? { uncached: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
332
+ const nextFam = { ...fam };
333
+ nextFam[period] = {
334
+ uncached: b.uncached + (usage.inputTokens ?? 0),
335
+ cacheRead: b.cacheRead + (usage.cacheReadTokens ?? 0),
336
+ cacheWrite: b.cacheWrite + (usage.cacheWriteTokens ?? 0),
337
+ output: b.output + (usage.outputTokens ?? 0)
338
+ };
339
+ const next = { ...prev };
340
+ next[family] = nextFam;
341
+ st.cost = next;
342
+ }
313
343
  function applyTimeline(state, event, bounds) {
314
344
  let st;
315
345
  const ensure = () => st ??= {
@@ -405,6 +435,7 @@ function applyTimeline(state, event, bounds) {
405
435
  if (usage && typeof usage.inputTokens === "number") {
406
436
  record.prompt = usage.inputTokens + (usage.cacheReadTokens || 0) + (usage.cacheWriteTokens || 0);
407
437
  if (typeof usage.outputTokens === "number") record.output = usage.outputTokens;
438
+ accumulateCost(s, event.time, usage);
408
439
  }
409
440
  s.requests.push(record);
410
441
  const asstMsg = deriveEventMessage(event);
@@ -458,6 +489,21 @@ function buildTimelineView(state, bounds) {
458
489
  droppedNodes: 0,
459
490
  archive: state.archived.map((n) => ({ ...n }))
460
491
  };
492
+ if (state.cost !== void 0) {
493
+ const copyFam = (f) => {
494
+ if (f === void 0) return void 0;
495
+ const out = {};
496
+ if (f.peak !== void 0) out.peak = { ...f.peak };
497
+ if (f.off !== void 0) out.off = { ...f.off };
498
+ return out;
499
+ };
500
+ const cost = {};
501
+ const flash = copyFam(state.cost.flash);
502
+ if (flash !== void 0) cost.flash = flash;
503
+ const pro = copyFam(state.cost.pro);
504
+ if (pro !== void 0) cost.pro = pro;
505
+ result.cost = cost;
506
+ }
461
507
  const overflowCount = Math.max(0, state.surface.length - bounds.maxNodes);
462
508
  const overflow = state.surface.slice(0, overflowCount);
463
509
  const tail = state.surface.slice(overflowCount);
@@ -544,6 +590,16 @@ var currentSchema = z3.object({
544
590
  tool: z3.number().int().nonnegative(),
545
591
  total: z3.number().int().nonnegative()
546
592
  }).strict();
593
+ var costBucketsSchema = z3.object({
594
+ uncached: z3.number().int().nonnegative(),
595
+ cacheRead: z3.number().int().nonnegative(),
596
+ cacheWrite: z3.number().int().nonnegative(),
597
+ output: z3.number().int().nonnegative()
598
+ }).strict();
599
+ var costFamilySchema = z3.object({
600
+ peak: costBucketsSchema.optional(),
601
+ off: costBucketsSchema.optional()
602
+ }).strict();
547
603
  var contextTimelineSchema = z3.object({
548
604
  ok: z3.literal(true),
549
605
  model: z3.string().optional(),
@@ -553,6 +609,7 @@ var contextTimelineSchema = z3.object({
553
609
  toolList: z3.array(z3.object({ name: z3.string(), tokens: z3.number().int().nonnegative() }).strict()),
554
610
  requests: z3.array(requestRecordSchema),
555
611
  events: z3.array(contextEventSchema),
612
+ cost: z3.object({ flash: costFamilySchema.optional(), pro: costFamilySchema.optional() }).strict().optional(),
556
613
  nodes: z3.array(surfaceNodeSchema),
557
614
  droppedNodes: z3.number().int().nonnegative(),
558
615
  archive: z3.array(surfaceNodeSchema),
@@ -582,7 +639,10 @@ function createContextTimelineDefinition(config) {
582
639
  // is not losslessly JSON-serializable) — which also starved the `title`
583
640
  // projection row and broke the session list after a restart. The bump
584
641
  // discards old cached rows and refolds them clean.
585
- stateVersion: 4
642
+ // 5: the session-cost totals (`cost` — per-family/per-period billed
643
+ // tokens) joined the persisted state; cached rows predate the shape and
644
+ // are refolded.
645
+ stateVersion: 5
586
646
  };
587
647
  }
588
648
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/bowenliang123/dsh-context.git"
9
9
  },
10
- "homepage": "https://github.com/bowenliang123/dsh-context#readme",
10
+ "homepage": "https://github.com/bowenliang123/dsh-context",
11
11
  "bugs": {
12
12
  "url": "https://github.com/bowenliang123/dsh-context/issues"
13
13
  },