dsh-context 0.38.5 → 0.40.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
@@ -140,7 +140,7 @@ In **Settings → Plugins → Plugin configuration**, the **Context** card holds
140
140
  ## Good to know
141
141
 
142
142
  - **Estimates vs actuals** — category figures use dsh's own fixed-density heuristic (the same one as its built-in token meter); the pinned trend details and Token/Timing rings show provider-reported actuals next to them.
143
- - **Compatibility** — works on `@deepseek-ai/dsh` **0.1.0-rc7+**, **0.1.1-rc2+**, and **0.1.2-alpha1+**; multimodal rendering requires 0.1.1+.
143
+ - **Compatibility** — works on `@deepseek-ai/dsh` **0.1.1-rc2+** and **0.1.2-alpha2+**.
144
144
  - **I18n** — UI in English and 简体中文.
145
145
 
146
146
  ## Like it?
package/lib/client.js CHANGED
@@ -40,15 +40,17 @@ window.__ModuleLoader__.load({
40
40
  "timing.lm": "模型调用",
41
41
  "timing.tools": "工具执行",
42
42
  "timing.other": "其他开销",
43
- "timing.times": "{n}次",
43
+ "timing.lmTimes": "{n}次",
44
+ "timing.toolTimes": "{n}次",
44
45
  "timing.empty": "暂无耗时数据 · 对话开始后这里会显示时长分布",
45
46
  "tokens.title": "Token 统计",
46
47
  "tokens.hint": "供应商上报的累计计费用量",
47
48
  "tokens.cacheHit": "缓存命中",
48
- "tokens.cacheRead": "缓存读取",
49
+ "tokens.cacheRead": "缓存输入",
49
50
  "tokens.cacheWrite": "缓存写入",
50
- "tokens.uncached": "未命中输入",
51
- "tokens.output": "输出(含思考)",
51
+ "tokens.uncached": "未缓存输入",
52
+ "tokens.output": "输出",
53
+ "tokens.outputNote": "含思考",
52
54
  "plugin.title": "插件信息",
53
55
  "plugin.hint": "The best DSH context plugin ⭐",
54
56
  "plugin.name": "插件",
@@ -60,9 +62,9 @@ window.__ModuleLoader__.load({
60
62
  "gran.turn": "轮次",
61
63
  "settings.title": "上下文",
62
64
  "settings.desc": "dsh-context 插件中上下文面板的偏好设置",
63
- "settings.gran": "趋势图默认粒度",
64
- "settings.mode": "趋势图默认展示",
65
- "settings.fileSort": "文件活动默认排序",
65
+ "settings.gran": "趋势图 · 默认粒度",
66
+ "settings.mode": "趋势图 · 默认展示方式",
67
+ "settings.fileSort": "文件活动 · 默认排序",
66
68
  "settings.expand": "展开",
67
69
  "settings.collapse": "收起",
68
70
  "settings.readOnly": "当前环境的设置为只读",
@@ -222,6 +224,8 @@ window.__ModuleLoader__.load({
222
224
  "browser.deltaHint": "对比上轮末步的变动",
223
225
  "browser.noHeader": "此数据来自旧版插件:仅提供 token 估算,无实际内容",
224
226
  "browser.noEpoch": "该步骤的头部内容(系统提示词 / 工具定义)不在保留范围内",
227
+ "browser.headerMetaOnly": "此环境不支持按需读取头部内容,仅显示 token 估算",
228
+ "browser.noSystem": "该请求头部未包含系统提示词",
225
229
  "browser.noContent": "完整内容不在当前加载的消息窗口内(在聊天页加载更早历史后可查看)",
226
230
  "browser.search.user": "按消息内容过滤…",
227
231
  "browser.search.inject": "按注入内容过滤…",
@@ -311,15 +315,17 @@ window.__ModuleLoader__.load({
311
315
  "timing.lm": "Model calls",
312
316
  "timing.tools": "Tool runs",
313
317
  "timing.other": "Overhead",
314
- "timing.times": "× {n}",
318
+ "timing.lmTimes": "{n} calls",
319
+ "timing.toolTimes": "{n} runs",
315
320
  "timing.empty": "No timing data yet — durations appear as the conversation runs",
316
321
  "tokens.title": "Token Stats",
317
322
  "tokens.hint": "Cumulative provider-reported usage",
318
- "tokens.cacheHit": "Cache hit",
319
- "tokens.cacheRead": "Cache read",
320
- "tokens.cacheWrite": "Cache write",
321
- "tokens.uncached": "Uncached input",
322
- "tokens.output": "Output (incl. reasoning)",
323
+ "tokens.cacheHit": "Cache Hit",
324
+ "tokens.cacheRead": "Cached Input",
325
+ "tokens.cacheWrite": "Cache Write",
326
+ "tokens.uncached": "Uncached Input",
327
+ "tokens.output": "Output",
328
+ "tokens.outputNote": "incl. reasoning",
323
329
  "plugin.title": "Plugin Info",
324
330
  "plugin.hint": "The best DSH context plugin ⭐",
325
331
  "plugin.name": "Plugin",
@@ -331,9 +337,9 @@ window.__ModuleLoader__.load({
331
337
  "gran.turn": "Turn",
332
338
  "settings.title": "Context",
333
339
  "settings.desc": "Preferences for Context panel in dsh-context plugin",
334
- "settings.gran": "Default trend granularity",
335
- "settings.mode": "Default trend display",
336
- "settings.fileSort": "File Activity default sort",
340
+ "settings.gran": "Trend Chart · Default granularity",
341
+ "settings.mode": "Trend Chart · Default display",
342
+ "settings.fileSort": "File Activity · Default sort",
337
343
  "settings.expand": "Expand",
338
344
  "settings.collapse": "Collapse",
339
345
  "settings.readOnly": "Settings are read-only in this environment",
@@ -493,6 +499,8 @@ window.__ModuleLoader__.load({
493
499
  "browser.deltaHint": "vs previous turn",
494
500
  "browser.noHeader": "Served by an older plugin build: token estimates only, no content",
495
501
  "browser.noEpoch": "The header content (System Prompt / Tool Schemas) of this step is outside retention",
502
+ "browser.headerMetaOnly": "This environment cannot fetch header content on demand; token estimates only",
503
+ "browser.noSystem": "This request header carried no system prompt",
496
504
  "browser.noContent": "Full content is outside the loaded message window (load older history in Chat to view)",
497
505
  "browser.search.user": "Filter by message text…",
498
506
  "browser.search.inject": "Filter injected context…",
@@ -998,14 +1006,15 @@ window.__ModuleLoader__.load({
998
1006
  }
999
1007
  /**
1000
1008
  * Narrow a delivered projection value to the plugin's `contextHeaders`
1001
- * (request-header content epochs). Absent key = an older Host half without
1002
- * the companion unit — the Context browser degrades its system/tools
1003
- * sections to tokens-only with a note.
1009
+ * (request-header epoch METADATA boundaries, token prices, attribution).
1010
+ * Absent key = an older Host half without the companion unit — the Context
1011
+ * browser degrades its system/tools sections to a metadata-only note.
1004
1012
  *
1005
1013
  * Entry-level shape is checked too: a malformed epoch (corrupt payload with
1006
- * a missing tools list or wrong-typed system prompt) would crash the
1014
+ * a missing tools list or wrong-typed systemTokens) would crash the
1007
1015
  * browser's tools/sections reads, so the WHOLE projection degrades to null
1008
- * and the card falls back to its tokens-only note.
1016
+ * and the card falls back to its metadata-only note. The epoch CONTENT is
1017
+ * not part of this value — the browser fetches it per epoch on demand.
1009
1018
  */
1010
1019
  function headersOf(value) {
1011
1020
  const headers = asRecord$1(value);
@@ -1014,7 +1023,7 @@ window.__ModuleLoader__.load({
1014
1023
  if (h === null || typeof h !== "object") return null;
1015
1024
  const entry = h;
1016
1025
  if (!Array.isArray(entry.tools)) return null;
1017
- if (entry.system !== void 0 && typeof entry.system !== "string") return null;
1026
+ if (entry.systemTokens !== void 0 && (typeof entry.systemTokens !== "number" || !Number.isFinite(entry.systemTokens))) return null;
1018
1027
  }
1019
1028
  return headers;
1020
1029
  }
@@ -1217,6 +1226,37 @@ window.__ModuleLoader__.load({
1217
1226
  return rows;
1218
1227
  }
1219
1228
  /**
1229
+ * The session's history readers over whichever face the running harness
1230
+ * serves — the 0.1.2+ gateway remotes (`remote.session.page`) first, the
1231
+ * pre-0.1.2 api client verb (`connection.api.sessions.history`) beneath it.
1232
+ * Both cut message-aligned pages, so the returned read covers `seq` whenever
1233
+ * the durable log still holds it: the newer face pins the inclusive cut to
1234
+ * the seq itself, the legacy reader uses the exclusive bound one past it.
1235
+ * Undefined when no face exists (older hosts) — callers keep their static
1236
+ * degradation.
1237
+ */
1238
+ function pageReadersOf(ctx, sessionId) {
1239
+ const pageFace = serviceOf(ctx, "remote.session");
1240
+ const page = pageFace !== void 0 && typeof pageFace.page === "function" ? pageFace.page.bind(pageFace) : void 0;
1241
+ const historyFace = serviceOf(ctx, "connection")?.api?.sessions;
1242
+ const history = historyFace !== void 0 && typeof historyFace.history === "function" ? historyFace.history.bind(historyFace) : void 0;
1243
+ if (page === void 0 && history === void 0) return void 0;
1244
+ return (seq) => {
1245
+ if (page !== void 0) return page({
1246
+ address: {
1247
+ kind: "session",
1248
+ sessionId
1249
+ },
1250
+ throughSeq: seq,
1251
+ beforeSeq: seq + 1
1252
+ }, new AbortController().signal);
1253
+ return history({
1254
+ sessionId,
1255
+ beforeSeq: seq + 1
1256
+ });
1257
+ };
1258
+ }
1259
+ /**
1220
1260
  * Build the browser's per-session fetcher over whichever history face the
1221
1261
  * running harness serves — the 0.1.2+ gateway remotes (`remote.session.page`)
1222
1262
  * first, the pre-0.1.2 api client verb (`connection.api.sessions.history`)
@@ -1227,32 +1267,73 @@ window.__ModuleLoader__.load({
1227
1267
  * nodes cache in the closure: one mount re-reading a row never re-fetches.
1228
1268
  */
1229
1269
  function makeContentFetcher(ctx, sessionId) {
1230
- const pageFace = serviceOf(ctx, "remote.session");
1231
- const page = pageFace !== void 0 && typeof pageFace.page === "function" ? pageFace.page.bind(pageFace) : void 0;
1232
- const historyFace = serviceOf(ctx, "connection")?.api?.sessions;
1233
- const history = historyFace !== void 0 && typeof historyFace.history === "function" ? historyFace.history.bind(historyFace) : void 0;
1234
- if (page === void 0 && history === void 0) return void 0;
1235
- const pageRequest = (seq) => ({
1236
- address: {
1237
- kind: "session",
1238
- sessionId
1239
- },
1240
- throughSeq: seq,
1241
- beforeSeq: seq + 1
1242
- });
1243
- const historyCall = (seq) => history({
1244
- sessionId,
1245
- beforeSeq: seq + 1
1246
- });
1270
+ const read = pageReadersOf(ctx, sessionId);
1271
+ if (read === void 0) return void 0;
1247
1272
  const cache = /* @__PURE__ */ new Map();
1248
1273
  return async (seq) => {
1249
1274
  const hit = cache.get(seq);
1250
1275
  if (hit !== void 0) return hit;
1251
- const node = pageNodesOf(rowsOf(page !== void 0 ? await page(pageRequest(seq), new AbortController().signal) : await historyCall(seq))).get(seq) ?? null;
1276
+ const node = pageNodesOf(rowsOf(await read(seq))).get(seq) ?? null;
1252
1277
  if (node !== null) cache.set(seq, node);
1253
1278
  return node;
1254
1279
  };
1255
1280
  }
1281
+ /**
1282
+ * Map one raw `request/header` event into the epoch content the browser
1283
+ * renders — the full system prompt text plus each tool's producer
1284
+ * description and raw schema, mirroring the host fold's per-entry guards
1285
+ * (a null or primitive tool entry degrades to an unnamed row instead of
1286
+ * throwing the read). Null when the envelope carries no usable header.
1287
+ */
1288
+ function headerContentOf(data) {
1289
+ const rawHeader = data.header !== null && typeof data.header === "object" ? data.header : null;
1290
+ if (rawHeader === null) return null;
1291
+ const toolsRaw = Array.isArray(rawHeader.tools) ? rawHeader.tools : [];
1292
+ const tools = [];
1293
+ for (const t of toolsRaw) {
1294
+ const tool = t !== null && typeof t === "object" ? t : null;
1295
+ tools.push({
1296
+ name: tool !== null && typeof tool.name === "string" ? tool.name : "?",
1297
+ ...tool !== null && typeof tool.description === "string" && tool.description !== "" ? { description: tool.description } : {},
1298
+ schema: t
1299
+ });
1300
+ }
1301
+ return {
1302
+ ...typeof rawHeader.system === "string" && rawHeader.system !== "" ? { system: rawHeader.system } : {},
1303
+ tools
1304
+ };
1305
+ }
1306
+ /**
1307
+ * The on-demand CONTENT fetch for `contextHeaders` epochs — the lazy
1308
+ * counterpart of the node fetcher above. One seq-anchored history read off
1309
+ * the epoch's `seq` returns the page holding that epoch's `request/header`
1310
+ * event (non-message events ride the page verbatim); the raw header is
1311
+ * mapped client-side into the renderable content. Epochs cache per session
1312
+ * (history is immutable), and OLDER epochs sharing the page cache for free —
1313
+ * stepping back through epochs walks the same pages. Undefined when no
1314
+ * history face exists (older hosts) — the browser keeps a metadata-only
1315
+ * degradation instead.
1316
+ */
1317
+ function makeHeaderFetcher(ctx, sessionId) {
1318
+ const read = pageReadersOf(ctx, sessionId);
1319
+ if (read === void 0) return void 0;
1320
+ const cache = /* @__PURE__ */ new Map();
1321
+ return async (seq) => {
1322
+ const hit = cache.get(seq);
1323
+ if (hit !== void 0) return hit;
1324
+ const rows = rowsOf(await read(seq));
1325
+ let picked = null;
1326
+ for (const entry of rows) {
1327
+ const ev = eventOf(entry);
1328
+ if (ev === null || ev.type !== "request/header") continue;
1329
+ const content = headerContentOf(ev.data);
1330
+ if (content === null) continue;
1331
+ cache.set(ev.seq, content);
1332
+ if (ev.seq === seq) picked = content;
1333
+ }
1334
+ return picked;
1335
+ };
1336
+ }
1256
1337
  //#endregion
1257
1338
  //#region src/client/assemble.ts
1258
1339
  /** The header epoch in force at `seq` (last logged before it), or the newest. */
@@ -1898,7 +1979,7 @@ window.__ModuleLoader__.load({
1898
1979
  }
1899
1980
  return out;
1900
1981
  }, [params]);
1901
- const schemaJson = React.useMemo(() => props.schema !== void 0 ? JSON.stringify(props.schema, null, 2) : "", [props.schema]);
1982
+ const schemaJson = React.useMemo(() => jsonOpen ? JSON.stringify(props.schema, null, 2) : "", [props.schema, jsonOpen]);
1902
1983
  return /* @__PURE__ */ React.createElement(React.Fragment, null, props.description !== void 0 ? /* @__PURE__ */ React.createElement(TextSection, {
1903
1984
  label: props.labels.desc,
1904
1985
  text: props.description,
@@ -1912,13 +1993,13 @@ window.__ModuleLoader__.load({
1912
1993
  name: r.name,
1913
1994
  schema: r.schema,
1914
1995
  required: r.required
1915
- }))) : params !== null ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-params-empty" }, props.labels.empty) : null, schemaJson !== "" ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-json" }, /* @__PURE__ */ React.createElement("button", {
1996
+ }))) : params !== null ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-params-empty" }, props.labels.empty) : null, /* @__PURE__ */ React.createElement("div", { className: "lc-ts-json" }, /* @__PURE__ */ React.createElement("button", {
1916
1997
  type: "button",
1917
1998
  className: "lc-ts-json-toggle",
1918
1999
  onClick: () => {
1919
2000
  setJsonOpen((o) => !o);
1920
2001
  }
1921
- }, (jsonOpen ? "▾ " : "▸ ") + (jsonOpen ? props.labels.hide : props.labels.show)), jsonOpen ? /* @__PURE__ */ React.createElement("pre", { className: "lc-ts-desc-body lc-br-dim" }, schemaJson) : null) : null);
2002
+ }, (jsonOpen ? "▾ " : "▸ ") + (jsonOpen ? props.labels.hide : props.labels.show)), jsonOpen ? /* @__PURE__ */ React.createElement("pre", { className: "lc-ts-desc-body lc-br-dim" }, schemaJson) : null));
1922
2003
  }
1923
2004
  /**
1924
2005
  * Both block vocabularies normalize here — raw durable blocks (`type`: text/reasoning/tool-call/tool-result/image) and snapshot assistant
@@ -2139,7 +2220,7 @@ window.__ModuleLoader__.load({
2139
2220
  return m;
2140
2221
  }
2141
2222
  function countOf(asm, byCat, c) {
2142
- if (c === "system") return asm.header !== null && asm.header.system !== void 0 ? 1 : 0;
2223
+ if (c === "system") return asm.header !== null && asm.header.systemTokens !== void 0 ? 1 : 0;
2143
2224
  if (c === "tools") return asm.header !== null ? asm.header.tools.length : 0;
2144
2225
  return byCat[c]?.length ?? 0;
2145
2226
  }
@@ -2237,6 +2318,48 @@ window.__ModuleLoader__.load({
2237
2318
  const linked = req === null && props.onHoverKey !== void 0;
2238
2319
  const linkKey = linked && props.hoverKey !== null && props.hoverKey !== "free" ? props.hoverKey : null;
2239
2320
  const view = assemble(data, headers, seq);
2321
+ const [headerContent, setHeaderContent] = React.useState(() => /* @__PURE__ */ new Map());
2322
+ const [headerState, setHeaderState] = React.useState("idle");
2323
+ const [headerRetry, setHeaderRetry] = React.useState(0);
2324
+ const fetchHeader = props.fetchHeader;
2325
+ const headerSeq = view.header !== null ? view.header.seq : null;
2326
+ const headerNeeded = view.header !== null && headerSeq !== null && !headerContent.has(headerSeq) && (openCat === "system" || openCat === "tools");
2327
+ React.useEffect(() => {
2328
+ if (!headerNeeded || fetchHeader === void 0) return;
2329
+ let live = true;
2330
+ setHeaderState("loading");
2331
+ fetchHeader(headerSeq).then((content) => {
2332
+ if (!live) return;
2333
+ if (content === null) {
2334
+ setHeaderState("absent");
2335
+ return;
2336
+ }
2337
+ setHeaderContent((prev) => {
2338
+ const next = new Map(prev);
2339
+ next.set(headerSeq, content);
2340
+ return next;
2341
+ });
2342
+ setHeaderState("idle");
2343
+ }, (error) => {
2344
+ console.warn("dsh-context: header content fetch failed", error);
2345
+ if (live) setHeaderState("failed");
2346
+ });
2347
+ return () => {
2348
+ live = false;
2349
+ };
2350
+ }, [
2351
+ headerNeeded,
2352
+ headerSeq,
2353
+ fetchHeader,
2354
+ headerRetry
2355
+ ]);
2356
+ const headerNote = fetchHeader === void 0 ? t("browser.headerMetaOnly") : headerState === "loading" || headerState === "idle" ? t("browser.loading") : headerState === "absent" ? t("browser.notInLog") : /* @__PURE__ */ React.createElement("button", {
2357
+ type: "button",
2358
+ className: "lc-br-retry",
2359
+ onClick: () => {
2360
+ setHeaderRetry((r) => r + 1);
2361
+ }
2362
+ }, t("browser.loadFailed"));
2240
2363
  const breakdown = req !== null ? req : data.current;
2241
2364
  const parts = partsOf(breakdown);
2242
2365
  const total = breakdown.total;
@@ -2251,7 +2374,7 @@ window.__ModuleLoader__.load({
2251
2374
  const byCat = byCatOf(view);
2252
2375
  const toolCount = (c) => countOf(view, byCat, c);
2253
2376
  const singleKeyOf = (c) => {
2254
- if (c === "system") return view.header?.system !== void 0 ? "sys" : null;
2377
+ if (c === "system") return view.header?.systemTokens !== void 0 ? "sys" : null;
2255
2378
  if (c === "tools") {
2256
2379
  const tools = view.header?.tools;
2257
2380
  return tools !== void 0 && tools.length === 1 ? "tool:" + tools[0].name : null;
@@ -2298,13 +2421,12 @@ window.__ModuleLoader__.load({
2298
2421
  const catBody = (c) => {
2299
2422
  if (c === "system") {
2300
2423
  if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
2301
- const system = view.header.system;
2302
- /* v8 ignore next 1 -- the category opens only when its count > 0
2303
- (countOf system defined); the undefined arm is defensive. */
2304
- if (system === void 0) return null;
2305
- return elemRow("sys", null, system.replace(/\s+/g, " ").trim().slice(0, 80), breakdown.system, void 0, /* @__PURE__ */ React.createElement(TextSection, {
2424
+ const content = headerContent.get(view.header.seq);
2425
+ if (content === void 0) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, headerNote);
2426
+ if (content.system === void 0) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t("browser.noSystem"));
2427
+ return elemRow("sys", null, content.system.replace(/\s+/g, " ").trim().slice(0, 80), breakdown.system, void 0, /* @__PURE__ */ React.createElement(TextSection, {
2306
2428
  label: catLabel("system"),
2307
- text: system,
2429
+ text: content.system,
2308
2430
  rich,
2309
2431
  lines: lineLabel
2310
2432
  }));
@@ -2318,8 +2440,17 @@ window.__ModuleLoader__.load({
2318
2440
  show: t("tool.jsonToggle"),
2319
2441
  hide: t("tool.jsonHide")
2320
2442
  };
2443
+ const content = headerContent.get(view.header.seq);
2444
+ const contentByName = new Map(content?.tools.map((t) => [t.name, t]) ?? []);
2321
2445
  const q = rowQuery.trim().toLowerCase();
2322
- const shown = view.header.tools.filter((tool) => q === "" || tool.name.toLowerCase().includes(q) || (tool.description ?? "").toLowerCase().includes(q) || (tool.plugin ?? "").toLowerCase().includes(q) || schemaTextOf(tool.schema).toLowerCase().includes(q)).sort((a, b) => toolSort === "size" ? b.tokens - a.tokens : a.name < b.name ? -1 : 1);
2446
+ const shown = view.header.tools.filter((tool) => {
2447
+ if (q === "") return true;
2448
+ if (tool.name.toLowerCase().includes(q)) return true;
2449
+ if ((tool.plugin ?? "").toLowerCase().includes(q)) return true;
2450
+ const row = contentByName.get(tool.name);
2451
+ if (row === void 0) return false;
2452
+ return (row.description ?? "").toLowerCase().includes(q) || schemaTextOf(row.schema).toLowerCase().includes(q);
2453
+ }).sort((a, b) => toolSort === "size" ? b.tokens - a.tokens : a.name < b.name ? -1 : 1);
2323
2454
  const toolctl = /* @__PURE__ */ React.createElement(RowToolbar, {
2324
2455
  value: rowQuery,
2325
2456
  placeholder: t("tool.search"),
@@ -2334,18 +2465,23 @@ window.__ModuleLoader__.load({
2334
2465
  }
2335
2466
  }, t("tool.sort." + k))));
2336
2467
  if (shown.length === 0) return /* @__PURE__ */ React.createElement("div", null, toolctl, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t("tool.noMatch")));
2468
+ const toolBody = (tool) => {
2469
+ if (content === void 0) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, headerNote);
2470
+ const row = contentByName.get(tool.name);
2471
+ return row === void 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t("browser.notInLog")) : /* @__PURE__ */ React.createElement(ToolSchema, {
2472
+ description: row.description,
2473
+ schema: row.schema,
2474
+ rich,
2475
+ lines: lineLabel,
2476
+ labels
2477
+ });
2478
+ };
2337
2479
  return /* @__PURE__ */ React.createElement("div", null, toolctl, shown.map((tool) => {
2338
2480
  const trailing = tool.plugin !== void 0 ? /* @__PURE__ */ React.createElement("span", {
2339
2481
  className: "lc-br-tag lc-br-tool-plugin",
2340
2482
  title: tool.plugin === "<unknown-plugin>" ? t("tool.unknownTitle") : t("tool.plugin")
2341
2483
  }, tool.plugin === "<unknown-plugin>" ? t("tool.unknown") : tool.plugin) : null;
2342
- return elemRow("tool:" + tool.name, null, tool.name, tool.tokens, void 0, /* @__PURE__ */ React.createElement(ToolSchema, {
2343
- description: tool.description,
2344
- schema: tool.schema,
2345
- rich,
2346
- lines: lineLabel,
2347
- labels
2348
- }), false, trailing);
2484
+ return elemRow("tool:" + tool.name, null, tool.name, tool.tokens, void 0, toolBody(tool), false, trailing);
2349
2485
  }));
2350
2486
  }
2351
2487
  const rows = (byCat[c] ?? []).slice().reverse().map((n) => {
@@ -2653,6 +2789,7 @@ window.__ModuleLoader__.load({
2653
2789
  const convNodes = conversationNodesOf(props);
2654
2790
  const [hoverCat, setHoverCat] = React.useState(null);
2655
2791
  const fetchContent = React.useMemo(() => sessionId !== "" ? makeContentFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
2792
+ const fetchHeader = React.useMemo(() => sessionId !== "" ? makeHeaderFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
2656
2793
  const close = React.useCallback(() => {
2657
2794
  if (sessionId === "") return;
2658
2795
  modalStoreOf(sessionId).set(false);
@@ -2702,6 +2839,7 @@ window.__ModuleLoader__.load({
2702
2839
  headers,
2703
2840
  convNodes,
2704
2841
  fetchContent,
2842
+ fetchHeader,
2705
2843
  hoverKey: hoverCat,
2706
2844
  onHoverKey: setHoverCat
2707
2845
  }))));
@@ -4462,11 +4600,15 @@ window.__ModuleLoader__.load({
4462
4600
  });
4463
4601
  acc += pct;
4464
4602
  }
4603
+ const hovering = props.hoverKey !== null && props.hoverKey !== void 0 && arcs.some((a) => a.key === props.hoverKey);
4465
4604
  return /* @__PURE__ */ React.createElement("div", {
4466
- className: "lc-donut",
4605
+ className: "lc-donut" + (hovering ? " lc-donut-dim" : ""),
4467
4606
  style: {
4468
4607
  width: size,
4469
4608
  height: size
4609
+ },
4610
+ onMouseLeave: () => {
4611
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
4470
4612
  }
4471
4613
  }, /* @__PURE__ */ React.createElement("svg", {
4472
4614
  viewBox: "0 0 42 42",
@@ -4482,6 +4624,7 @@ window.__ModuleLoader__.load({
4482
4624
  strokeWidth: "4"
4483
4625
  }) : arcs.map((a) => /* @__PURE__ */ React.createElement("circle", {
4484
4626
  key: a.key,
4627
+ className: "lc-donut-seg" + (props.hoverKey === a.key ? " lc-donut-seg-on" : ""),
4485
4628
  cx: "21",
4486
4629
  cy: "21",
4487
4630
  r: "15.9155",
@@ -4489,7 +4632,10 @@ window.__ModuleLoader__.load({
4489
4632
  stroke: a.color,
4490
4633
  strokeWidth: "4",
4491
4634
  strokeDasharray: `${a.pct} ${100 - a.pct}`,
4492
- strokeDashoffset: a.offset
4635
+ strokeDashoffset: a.offset,
4636
+ onMouseEnter: () => {
4637
+ if (props.onHoverKey !== void 0) props.onHoverKey(a.key);
4638
+ }
4493
4639
  }))), /* @__PURE__ */ React.createElement("div", { className: "lc-donut-center" }, /* @__PURE__ */ React.createElement("b", { style: { fontSize: Math.max(11, Math.round(size * .13)) } }, props.centerTop), props.centerSub !== void 0 ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: Math.max(9, Math.round(size * .105)) } }, props.centerSub) : null));
4494
4640
  };
4495
4641
  }
@@ -4833,7 +4979,7 @@ window.__ModuleLoader__.load({
4833
4979
  return function PluginInfo() {
4834
4980
  const [latest, setLatest] = React.useState(null);
4835
4981
  React.useEffect(() => {
4836
- if ("0.38.5".includes("-dev")) return;
4982
+ if ("0.40.0".includes("-dev")) return;
4837
4983
  let on = true;
4838
4984
  fetchLatestVersion().then((v) => {
4839
4985
  if (on && v) setLatest(v);
@@ -4842,8 +4988,8 @@ window.__ModuleLoader__.load({
4842
4988
  on = false;
4843
4989
  };
4844
4990
  }, []);
4845
- const update = latest !== null && isNewerVersion(latest, "0.38.5") ? latest : null;
4846
- const nameText = "dsh-context (v0.38.5)";
4991
+ const update = latest !== null && isNewerVersion(latest, "0.40.0") ? latest : null;
4992
+ const nameText = "dsh-context (v0.40.0)";
4847
4993
  const nameValue = [nameText];
4848
4994
  if (update) nameValue.push(/* @__PURE__ */ React.createElement("span", {
4849
4995
  key: "update",
@@ -5215,16 +5361,27 @@ window.__ModuleLoader__.load({
5215
5361
  //#region src/client/components/sliceList.tsx
5216
5362
  function makeSliceList(kit) {
5217
5363
  return function SliceList(props) {
5218
- return /* @__PURE__ */ React.createElement("div", { className: "lc-sl" }, props.rows.map((r) => /* @__PURE__ */ React.createElement("div", {
5364
+ return /* @__PURE__ */ React.createElement("div", {
5365
+ className: "lc-sl",
5366
+ onMouseLeave: () => {
5367
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
5368
+ }
5369
+ }, props.rows.map((r) => /* @__PURE__ */ React.createElement("div", {
5219
5370
  key: r.key,
5220
- className: "lc-sl-row"
5221
- }, /* @__PURE__ */ React.createElement("i", {
5371
+ className: "lc-sl-row" + (r.dim ? " lc-sl-row-dim" : "") + (props.hoverKey !== void 0 && props.hoverKey === r.key ? " lc-sl-row-on" : ""),
5372
+ onMouseEnter: () => {
5373
+ if (props.onHoverKey !== void 0) props.onHoverKey(r.key);
5374
+ }
5375
+ }, /* @__PURE__ */ React.createElement("div", { className: "lc-sl-main" }, /* @__PURE__ */ React.createElement("i", {
5222
5376
  className: "lc-sl-dot",
5223
5377
  style: { background: r.color }
5224
- }), /* @__PURE__ */ React.createElement("span", { className: "lc-sl-label" }, r.label), r.count !== "" ? /* @__PURE__ */ React.createElement("span", {
5225
- className: "lc-sl-count",
5378
+ }), /* @__PURE__ */ React.createElement("span", {
5379
+ className: "lc-sl-label",
5380
+ title: r.label
5381
+ }, r.label), /* @__PURE__ */ React.createElement("span", { className: "lc-sl-pct" }, r.pct)), r.count !== "" ? /* @__PURE__ */ React.createElement("div", {
5382
+ className: "lc-sl-sub",
5226
5383
  title: r.count
5227
- }, r.count) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-sl-pct" }, r.pct))));
5384
+ }, r.count) : null)));
5228
5385
  };
5229
5386
  }
5230
5387
  //#endregion
@@ -5234,6 +5391,7 @@ window.__ModuleLoader__.load({
5234
5391
  const SliceList = makeSliceList(kit);
5235
5392
  return function StatsTiming(props) {
5236
5393
  const lang = props.locale === "zh" ? "zh" : "en";
5394
+ const [hoverKey, setHoverKey] = React.useState(null);
5237
5395
  const timing = props.timing;
5238
5396
  const wall = timing !== null && Number.isFinite(timing.wallMs) && timing.wallMs > 0 ? timing.wallMs : 0;
5239
5397
  let segments = [];
@@ -5243,6 +5401,11 @@ window.__ModuleLoader__.load({
5243
5401
  const toolRing = Math.max(0, Math.min(timing.toolsMs, wall - lm));
5244
5402
  const other = Math.max(0, wall - lm - toolRing);
5245
5403
  const share = (ms) => wall > 0 ? ms / wall : 0;
5404
+ const countOf = (ms, times) => {
5405
+ const dur = fmtDuration(ms, lang);
5406
+ if (times === void 0) return dur;
5407
+ return ms > 0 ? `${dur} · ${times}` : times;
5408
+ };
5246
5409
  segments = [
5247
5410
  {
5248
5411
  key: "lm",
@@ -5264,23 +5427,26 @@ window.__ModuleLoader__.load({
5264
5427
  {
5265
5428
  key: "lm",
5266
5429
  color: "#3b82f6",
5267
- label: `${t("timing.lm")} ${t("timing.times", { n: fmt(timing.calls) })}`,
5430
+ label: t("timing.lm"),
5431
+ dim: timing.lmMs === 0,
5268
5432
  pct: fmtShare(timing.lmMs, wall),
5269
- count: fmtDuration(timing.lmMs, lang)
5433
+ count: countOf(timing.lmMs, t("timing.lmTimes", { n: fmt(timing.calls) }))
5270
5434
  },
5271
5435
  {
5272
5436
  key: "tools",
5273
5437
  color: "#14b8a6",
5274
- label: `${t("timing.tools")} ${t("timing.times", { n: fmt(timing.toolCalls) })}`,
5438
+ label: t("timing.tools"),
5439
+ dim: timing.toolsMs === 0,
5275
5440
  pct: fmtShare(timing.toolsMs, wall),
5276
- count: fmtDuration(timing.toolsMs, lang)
5441
+ count: countOf(timing.toolsMs, t("timing.toolTimes", { n: fmt(timing.toolCalls) }))
5277
5442
  },
5278
5443
  {
5279
5444
  key: "other",
5280
5445
  color: "#94a3b8",
5281
5446
  label: t("timing.other"),
5447
+ dim: other === 0,
5282
5448
  pct: fmtShare(other, wall),
5283
- count: fmtDuration(other, lang)
5449
+ count: countOf(other)
5284
5450
  }
5285
5451
  ];
5286
5452
  }
@@ -5288,8 +5454,14 @@ window.__ModuleLoader__.load({
5288
5454
  segments,
5289
5455
  size: 96,
5290
5456
  centerTop: wall > 0 ? fmtDuration(wall, lang) : "—",
5291
- centerSub: t("timing.total")
5292
- }), /* @__PURE__ */ React.createElement(SliceList, { rows })));
5457
+ centerSub: t("timing.total"),
5458
+ hoverKey,
5459
+ onHoverKey: setHoverKey
5460
+ }), /* @__PURE__ */ React.createElement(SliceList, {
5461
+ rows,
5462
+ hoverKey,
5463
+ onHoverKey: setHoverKey
5464
+ })));
5293
5465
  };
5294
5466
  }
5295
5467
  //#endregion
@@ -5298,6 +5470,7 @@ window.__ModuleLoader__.load({
5298
5470
  const { t, fmt, fmtShare } = kit;
5299
5471
  const SliceList = makeSliceList(kit);
5300
5472
  return function StatsTokens(props) {
5473
+ const [hoverKey, setHoverKey] = React.useState(null);
5301
5474
  const reads = props.usage !== null ? numOf(props.usage.cacheReadTokens) : 0;
5302
5475
  const writes = props.usage !== null ? numOf(props.usage.cacheWriteTokens) : 0;
5303
5476
  const uncached = props.usage !== null ? numOf(props.usage.uncachedInputTokens) : 0;
@@ -5319,7 +5492,7 @@ window.__ModuleLoader__.load({
5319
5492
  },
5320
5493
  {
5321
5494
  key: "uncached",
5322
- color: "#6366f1",
5495
+ color: "#a855f7",
5323
5496
  label: t("tokens.uncached"),
5324
5497
  value: uncached
5325
5498
  },
@@ -5327,6 +5500,7 @@ window.__ModuleLoader__.load({
5327
5500
  key: "output",
5328
5501
  color: "#3b82f6",
5329
5502
  label: t("tokens.output"),
5503
+ note: t("tokens.outputNote"),
5330
5504
  value: output
5331
5505
  }
5332
5506
  ];
@@ -5336,7 +5510,7 @@ window.__ModuleLoader__.load({
5336
5510
  color: b.color,
5337
5511
  label: b.label,
5338
5512
  pct: fmtShare(b.value, billed),
5339
- count: fmt(b.value)
5513
+ count: b.note === void 0 ? fmt(b.value) : `${fmt(b.value)} · ${b.note}`
5340
5514
  }));
5341
5515
  return /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col-stats lc-col-donut" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("tokens.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("tokens.hint"))), /* @__PURE__ */ React.createElement("div", { className: "lc-donut-row" }, /* @__PURE__ */ React.createElement(Donut, {
5342
5516
  segments: shown.map((b) => ({
@@ -5346,8 +5520,14 @@ window.__ModuleLoader__.load({
5346
5520
  })),
5347
5521
  size: 96,
5348
5522
  centerTop: hit === null ? "—" : `${hit}%`,
5349
- centerSub: t("tokens.cacheHit")
5350
- }), /* @__PURE__ */ React.createElement(SliceList, { rows })));
5523
+ centerSub: t("tokens.cacheHit"),
5524
+ hoverKey,
5525
+ onHoverKey: setHoverKey
5526
+ }), /* @__PURE__ */ React.createElement(SliceList, {
5527
+ rows,
5528
+ hoverKey,
5529
+ onHoverKey: setHoverKey
5530
+ })));
5351
5531
  };
5352
5532
  }
5353
5533
  //#endregion
@@ -5822,6 +6002,7 @@ window.__ModuleLoader__.load({
5822
6002
  }, []);
5823
6003
  const loadImage = React.useMemo(() => imageLoaderOf(ctx, typeof sessionId === "string" ? sessionId : void 0), [ctx, sessionId]);
5824
6004
  const fetchContent = React.useMemo(() => typeof sessionId === "string" && sessionId !== "" ? makeContentFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
6005
+ const fetchHeader = React.useMemo(() => typeof sessionId === "string" && sessionId !== "" ? makeHeaderFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
5825
6006
  const rootRef = React.useRef(null);
5826
6007
  const scrollerRef = React.useRef(null);
5827
6008
  const restoredRef = React.useRef(null);
@@ -6041,6 +6222,7 @@ window.__ModuleLoader__.load({
6041
6222
  headers,
6042
6223
  convNodes,
6043
6224
  fetchContent,
6225
+ fetchHeader,
6044
6226
  previewSeq: hoveredSeq,
6045
6227
  pinSeq: pinnedReq !== null ? pinnedReq.seq : null,
6046
6228
  hoverKey: hoverCat,
@@ -6180,7 +6362,7 @@ window.__ModuleLoader__.load({
6180
6362
  }
6181
6363
  //#endregion
6182
6364
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/stats.css.mjs
6183
- const css$12 = ".lc-stats{grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;display:grid}.lc-stat{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:2px;min-width:0;padding:6px;display:flex}.lc-stats .lc-stat{padding:6px 3px}.lc-stats .lc-stat-q{margin:0;position:absolute;top:3px;right:4px}.lc-stat-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;font-weight:600;overflow:hidden}.lc-stat-value{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:center;font-size:14px;font-weight:600;overflow:hidden}.lc-stat-sub{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;overflow:hidden}.lc-stat-tipped{position:relative}.lc-stat-q{text-align:center;width:11px;height:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:help;border-radius:50%;margin-left:4px;font-size:9px;font-style:normal;font-weight:700;line-height:11px;display:inline-block}.lc-stat-tipped:hover .lc-stat-q{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}.lc-stat-tip{max-width:280px;font-weight:400;line-height:1.5;top:calc(100% + 6px);left:0}.lc-stat-tipped:hover .lc-stat-tip{opacity:1}.lc-stat-tip-prices{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:5px;padding-top:5px;display:block}.lc-stat-tip-head{font-weight:600;display:block}.lc-stat-tip-row{margin-top:2px;display:block}.lc-stat-tip-model{color:var(--dsw-alias-label-primary)}.lc-card-sub{color:var(--dsw-alias-label-secondary);margin-left:auto;font-size:12px;font-weight:400}.lc-overview-num{align-items:baseline;gap:6px;margin-bottom:8px;display:flex}.lc-overview-num>b{font-size:20px}.lc-overview-num span{color:var(--dsw-alias-label-secondary)}.lc-overview-pct{margin-left:auto;font-size:11px}.lc-overview-pct b{color:var(--dsw-alias-label-primary);margin-right:4px;font-size:20px}.lc-stat-head{color:var(--dsw-alias-label-secondary);margin:12px 0 8px;font-size:11px;font-weight:600}.lc-donut-row{justify-content:flex-start;align-items:center;gap:12px;min-width:0;display:flex}.lc-col-donut{flex-direction:column;display:flex}.lc-col-donut .lc-donut-row{flex:1}.lc-donut-row .lc-sl{flex:auto}.lc-donut{flex:none;position:relative}.lc-donut svg{display:block}.lc-donut-track{stroke:#8080802e}.lc-donut-center{pointer-events:none;text-align:center;box-sizing:border-box;flex-direction:column;justify-content:center;align-items:center;gap:1px;padding:0 7px;display:flex;position:absolute;inset:0}.lc-donut-center b{white-space:nowrap;text-overflow:ellipsis;max-width:100%;line-height:1.15;overflow:hidden}.lc-donut-center span{color:var(--dsw-alias-label-secondary);white-space:nowrap}.lc-sl{flex-direction:column;gap:5px;min-width:0;display:flex;overflow:hidden}.lc-sl-row{align-items:baseline;gap:6px;min-width:0;font-size:12px;display:flex}.lc-sl-dot{border-radius:2px;flex:none;align-self:flex-start;width:8px;height:8px;margin-top:2px}.lc-sl-label{flex:0 auto;min-width:80px;line-height:1.25;overflow:hidden}.lc-sl-count{text-align:right;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;text-overflow:ellipsis;flex:auto;overflow:hidden}.lc-sl-pct{text-align:right;font-variant-numeric:tabular-nums;flex:none;min-width:30px;font-weight:700}.lc-pi-grid{grid-template-columns:1fr;gap:8px;display:grid}.lc-pi-row-btn{appearance:none;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;width:100%;padding:0}.lc-pi-row{min-width:0;color:inherit;flex-direction:row;justify-content:space-between;align-items:baseline;gap:12px;text-decoration:none;display:flex}.lc-pi-row:hover .lc-pi-value{text-decoration:underline}.lc-pi-hint{text-decoration:none}.lc-pi-hint:hover{text-decoration:underline}.lc-pi-update{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;margin-left:6px;font-size:11px}.lc-pi-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:none;font-size:11px;font-weight:600;overflow:hidden}.lc-pi-value{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:right;font-size:13px;font-weight:600;overflow:hidden}.lc-pi-row-btn .lc-pi-value{font-weight:400}@container lc-head-card (width<=320px){.lc-donut-row{flex-wrap:wrap}.lc-donut{margin:0 auto}.lc-donut-row .lc-sl{flex-basis:100%}.lc-sl-label{min-width:40px}.lc-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}";
6365
+ const css$12 = ".lc-stats{grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;display:grid}.lc-stat{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:2px;min-width:0;padding:6px;display:flex}.lc-stats .lc-stat{padding:6px 3px}.lc-stats .lc-stat-q{margin:0;position:absolute;top:3px;right:4px}.lc-stat-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;font-weight:600;overflow:hidden}.lc-stat-value{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:center;font-size:14px;font-weight:600;overflow:hidden}.lc-stat-sub{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;overflow:hidden}.lc-stat-tipped{position:relative}.lc-stat-q{text-align:center;width:11px;height:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:help;border-radius:50%;margin-left:4px;font-size:9px;font-style:normal;font-weight:700;line-height:11px;display:inline-block}.lc-stat-tipped:hover .lc-stat-q{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}.lc-stat-tip{max-width:280px;font-weight:400;line-height:1.5;top:calc(100% + 6px);left:0}.lc-stat-tipped:hover .lc-stat-tip{opacity:1}.lc-stat-tip-prices{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:5px;padding-top:5px;display:block}.lc-stat-tip-head{font-weight:600;display:block}.lc-stat-tip-row{margin-top:2px;display:block}.lc-stat-tip-model{color:var(--dsw-alias-label-primary)}.lc-card-sub{color:var(--dsw-alias-label-secondary);margin-left:auto;font-size:12px;font-weight:400}.lc-overview-num{align-items:baseline;gap:6px;margin-bottom:8px;display:flex}.lc-overview-num>b{font-size:20px}.lc-overview-num span{color:var(--dsw-alias-label-secondary)}.lc-overview-pct{margin-left:auto;font-size:11px}.lc-overview-pct b{color:var(--dsw-alias-label-primary);margin-right:4px;font-size:20px}.lc-stat-head{color:var(--dsw-alias-label-secondary);margin:12px 0 8px;font-size:11px;font-weight:600}.lc-donut-row{justify-content:flex-start;align-items:center;gap:12px;min-width:0;display:flex}.lc-col-donut{flex-direction:column;display:flex}.lc-col-donut .lc-donut-row{flex:1}.lc-donut-row .lc-sl{flex:auto}.lc-donut{flex:none;position:relative}.lc-donut svg{display:block}.lc-donut-track{stroke:#8080802e}.lc-donut-seg{transition:opacity .12s}.lc-donut-dim .lc-donut-seg{opacity:.35}.lc-donut-dim .lc-donut-seg-on{opacity:1}.lc-donut-center{pointer-events:none;text-align:center;box-sizing:border-box;flex-direction:column;justify-content:center;align-items:center;gap:1px;padding:0 7px;display:flex;position:absolute;inset:0}.lc-donut-center b{white-space:nowrap;text-overflow:ellipsis;max-width:100%;line-height:1.15;overflow:hidden}.lc-donut-center span{color:var(--dsw-alias-label-secondary);white-space:nowrap}.lc-sl{flex-direction:column;gap:6px;min-width:0;display:flex;overflow:hidden}.lc-sl-row{border-radius:6px;flex-direction:column;gap:1px;min-width:0;margin:0 -6px;padding:1px 6px;font-size:12px;transition:background-color .12s;display:flex}.lc-sl-row-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-sl-row-dim{opacity:.55}.lc-sl-main{align-items:center;gap:6px;min-width:0;display:flex}.lc-sl-dot{border-radius:2px;flex:none;width:8px;height:8px}.lc-sl-label{white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;font-weight:600;line-height:1.25;overflow:hidden}.lc-sl-pct{text-align:right;font-variant-numeric:tabular-nums;flex:none;min-width:34px;font-weight:700}.lc-sl-sub{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;text-overflow:ellipsis;margin-left:14px;font-size:11px;line-height:1.3;overflow:hidden}.lc-pi-grid{grid-template-columns:1fr;gap:8px;display:grid}.lc-pi-row-btn{appearance:none;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;width:100%;padding:0}.lc-pi-row{min-width:0;color:inherit;flex-direction:row;justify-content:space-between;align-items:baseline;gap:12px;text-decoration:none;display:flex}.lc-pi-row:hover .lc-pi-value{text-decoration:underline}.lc-pi-hint{text-decoration:none}.lc-pi-hint:hover{text-decoration:underline}.lc-pi-update{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;margin-left:6px;font-size:11px}.lc-pi-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:none;font-size:11px;font-weight:600;overflow:hidden}.lc-pi-value{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:right;font-size:13px;font-weight:600;overflow:hidden}.lc-pi-row-btn .lc-pi-value{font-weight:400}@container lc-head-card (width<=320px){.lc-donut-row{gap:8px}.lc-stats{grid-template-columns:repeat(4,minmax(0,1fr))}}@container lc-head-card (width<=240px){.lc-donut-row{flex-wrap:wrap}.lc-donut{margin:0 auto}.lc-donut-row .lc-sl{flex-basis:100%}.lc-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}";
6184
6366
  const tagId$12 = "dsh-context/stats.css";
6185
6367
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$12) + "]") === null) {
6186
6368
  const tag = document.createElement("style");
package/lib/index.d.ts CHANGED
@@ -30,8 +30,27 @@ declare const Config: z.ZodPreprocess<z.ZodObject<{
30
30
  }, z.core.$strict>>;
31
31
  //#endregion
32
32
  //#region src/host/headers.d.ts
33
+ /**
34
+ * One stored tool: the v1 row shape carried the producer description and the
35
+ * raw schema; folds since the #37 slim-down append metadata-only entries.
36
+ */
37
+ interface StoredHeaderTool {
38
+ name: string;
39
+ tokens: number;
40
+ description?: string;
41
+ plugin?: string;
42
+ schema?: unknown;
43
+ }
44
+ /** One stored epoch: v1 rows carried `system`; folds since carry `systemTokens`. */
45
+ interface StoredHeaderRecord {
46
+ seq: number;
47
+ time: number;
48
+ system?: string;
49
+ systemTokens?: number;
50
+ tools: StoredHeaderTool[];
51
+ }
33
52
  interface HeadersState {
34
- headers: HeaderRecord[];
53
+ headers: StoredHeaderRecord[];
35
54
  }
36
55
  //#endregion
37
56
  //#region src/host/fold.d.ts
@@ -382,12 +401,10 @@ interface ContextEventRecord {
382
401
  turn?: number;
383
402
  step?: number;
384
403
  }
385
- /** One tool schema as assembled into a request header, with its display price. */
404
+ /** One tool of a request-header epoch, with its display price. */
386
405
  interface HeaderTool {
387
406
  name: string;
388
407
  tokens: number;
389
- /** Producer-declared description (may be long; the browser truncates). */
390
- description?: string;
391
408
  /**
392
409
  * The registering plugin's label, when attribution is known: either a
393
410
  * `plugin` field carried by the raw header entry (harness-provided) or the
@@ -397,16 +414,21 @@ interface HeaderTool {
397
414
  * the browser shows no tag.
398
415
  */
399
416
  plugin?: string;
400
- /** The raw JSON schema object the model received (plain JSON). */
401
- schema?: unknown;
402
417
  }
403
418
  /**
404
- * One request-header epoch: the full system prompt and tool schemas in force from this event's seq until the next epoch.
419
+ * One request-header epoch's METADATA: the epoch boundaries and token prices
420
+ * in force from this event's seq until the next epoch. The epoch CONTENT
421
+ * (full system prompt text, tool descriptions/schemas) is not projected —
422
+ * every session.list row, control baseline, push frame, and projection-cache
423
+ * checkpoint would otherwise carry it per session × epoch. The client
424
+ * fetches one epoch's content on demand (a seq-anchored history read off
425
+ * `seq`) as a {@link HeaderEpochContent}.
405
426
  */
406
427
  interface HeaderRecord {
407
428
  seq: number;
408
429
  time: number;
409
- system?: string;
430
+ /** The epoch's estimated system-prompt tokens; absent when it logged no system prompt. */
431
+ systemTokens?: number;
410
432
  tools: HeaderTool[];
411
433
  }
412
434
  /** The `contextHeaders` projection value: the bounded epoch list (newest last). */
package/lib/index.js CHANGED
@@ -549,42 +549,72 @@ function isInjection(source) {
549
549
  //#endregion
550
550
  //#region src/host/headers.ts
551
551
  /**
552
- * The `contextHeaders` session projection unit — the request-header CONTENT
553
- * epochs behind the timeline's envelope figures.
552
+ * The `contextHeaders` session projection unit — the request-header EPOCH
553
+ * METADATA behind the timeline's envelope figures.
554
554
  *
555
555
  * The hot `contextTimeline` unit carries only token prices of the system
556
- * prompt and tool schemas; this companion unit keeps the CONTENT (full
557
- * system prompt text, full tool JSON schemas) so the Context browser card
558
- * can show what a picked step's request was actually assembled from. It is
559
- * a separate unit on purpose: the agent loop logs `request/header` only
560
- * when the header changes, so this state (and its pushes to the browser)
561
- * moves rarely carrying full content costs nothing on the per-event hot
562
- * path.
556
+ * prompt and tool schemas; this companion unit keeps the per-epoch METADATA
557
+ * (epoch seq/time boundaries, per-tool token prices and plugin attribution)
558
+ * so the Context browser can pick the header epoch in force at any step and
559
+ * size its sections immediately. The epoch CONTENT (full system prompt text,
560
+ * full tool JSON schemas) deliberately does NOT ride the projection VALUE:
561
+ * session projections are served whole in every `session.list` row, control
562
+ * baseline, push frame, and change notification, so carrying content here
563
+ * multiplied it by sessions × epochs across every channel. The client
564
+ * fetches one epoch's `request/header` event on demand — a seq-anchored
565
+ * history read off the epoch's `seq`, the same targeted read the browser
566
+ * already uses for message content — and caches it per session (history is
567
+ * immutable).
568
+ *
569
+ * Read-compat over the persisted state (the pinned decision behind keeping
570
+ * `stateVersion` at 1): the harness serves a cold session's projections from
571
+ * its CACHED checkpoint rows and has no refresh channel for an idle session
572
+ * — a version bump invalidates every row and orphans the key until the
573
+ * session goes live again (the #37 regression). The state therefore still
574
+ * ACCEPTS the v1 content-bearing record shape, current folds append
575
+ * metadata-only records alongside any seeded legacy ones, and the view
576
+ * normalizes BOTH to the metadata-only wire shape (pricing the legacy system
577
+ * text at read time). Cached v1 rows keep working, new checkpoint writes
578
+ * shrink as legacy epochs age out of the capped list, and the wire — the
579
+ * part every delivery channel carries — is metadata-only from day one.
563
580
  *
564
581
  * Same projection contract as the timeline unit: pure init/apply/view,
565
582
  * `Object.is` reference stability for uninteresting events, plain-JSON
566
583
  * bounded state (epoch list capped — see HEADERS_MAX).
567
584
  */
568
- /** Retention cap on header epochs (changes are rare; 50 is generous). */
585
+ /** Retention cap on header epochs (metadata only; changes are rare; 50 is generous). */
569
586
  const HEADERS_MAX = 50;
570
- const headerToolSchema = z.object({
587
+ /**
588
+ * The persisted-state schema: the SUPERSET of both record generations, so a
589
+ * cached v1 row (content-bearing) seeds the fold instead of being discarded.
590
+ */
591
+ const storedToolSchema = z.object({
571
592
  name: z.string(),
572
593
  tokens: z.number().int().nonnegative(),
573
594
  description: z.string().optional(),
574
595
  plugin: z.string().optional(),
575
596
  schema: z.unknown().optional()
576
597
  }).strict();
577
- const contextHeadersSchema = z.object({ headers: z.array(z.object({
598
+ const storedEpochSchema = z.object({
578
599
  seq: z.number(),
579
600
  time: z.number(),
580
601
  system: z.string().optional(),
581
- tools: z.array(headerToolSchema)
602
+ systemTokens: z.number().int().nonnegative().optional(),
603
+ tools: z.array(storedToolSchema)
604
+ }).strict();
605
+ const contextHeadersStateSchema = z.object({ headers: z.array(storedEpochSchema) }).strict();
606
+ /** The wire schema: strict metadata — the shape every delivery channel carries. */
607
+ const headerToolWireSchema = z.object({
608
+ name: z.string(),
609
+ tokens: z.number().int().nonnegative(),
610
+ plugin: z.string().optional()
611
+ }).strict();
612
+ const contextHeadersSchema = z.object({ headers: z.array(z.object({
613
+ seq: z.number(),
614
+ time: z.number(),
615
+ systemTokens: z.number().int().nonnegative().optional(),
616
+ tools: z.array(headerToolWireSchema)
582
617
  }).strict()) }).strict();
583
- /**
584
- * State and wire are the same shape (the view only shallow-copies each record), so one schema validates both under the dsh 0.1.1-rc.1+
585
- * `stateSchema`/`wire` contract.
586
- */
587
- const contextHeadersStateSchema = contextHeadersSchema;
588
618
  function recordOf(event) {
589
619
  if (event.type !== "request/header") return null;
590
620
  const rawHeader = event.data.header;
@@ -598,40 +628,43 @@ function recordOf(event) {
598
628
  const tool = t !== null && typeof t === "object" ? t : {};
599
629
  const entry = {
600
630
  name: typeof tool.name === "string" ? tool.name : "?",
601
- tokens: estimateToolSchema(t),
602
- schema: t
631
+ tokens: estimateToolSchema(t)
603
632
  };
604
- if (typeof tool.description === "string" && tool.description !== "") entry.description = tool.description;
605
633
  if (typeof tool.plugin === "string" && tool.plugin !== "") entry.plugin = tool.plugin;
606
634
  return entry;
607
635
  })
608
636
  };
609
- if (typeof header.system === "string" && header.system.length > 0) record.system = header.system;
637
+ if (typeof header.system === "string" && header.system.length > 0) record.systemTokens = estimateSystem(header.system);
610
638
  return record;
611
639
  }
612
640
  /**
613
641
  * The context-headers projection unit; registered alongside the timeline unit (host/index.ts); clients read it through
614
- * `useProjection('contextHeaders')` and degrade to tokens-only header sections when the key is absent. Dual-contract definition (see
615
- * compat.ts).
642
+ * `useProjection('contextHeaders')` and fetch an epoch's full content on demand via the session history (historyPage.ts).
643
+ * Contract mirror with a REQUIRED `wire` block (see compat.ts).
616
644
  * @param resolve - best-effort tool-to-plugin attribution (see toolSources.ts); fills a missing `plugin` at view time so
617
645
  * epochs folded without attribution still render a tag when the source is known.
618
646
  */
619
647
  function createContextHeadersDefinition(resolve) {
620
- const view = (state) => ({ headers: state.headers.map((h) => ({
621
- ...h,
622
- tools: h.tools.map((t) => {
623
- if (resolve === void 0 || t.plugin !== void 0) return { ...t };
624
- const plugin = resolve(t.name);
625
- return plugin === void 0 ? { ...t } : {
626
- ...t,
627
- plugin
628
- };
629
- })
630
- })) });
648
+ const view = (state) => ({ headers: state.headers.map((h) => {
649
+ const record = {
650
+ seq: h.seq,
651
+ time: h.time,
652
+ tools: h.tools.map((t) => {
653
+ const entry = {
654
+ name: t.name,
655
+ tokens: t.tokens
656
+ };
657
+ const plugin = t.plugin ?? (resolve !== void 0 ? resolve(t.name) : void 0);
658
+ if (plugin !== void 0) entry.plugin = plugin;
659
+ return entry;
660
+ })
661
+ };
662
+ const systemTokens = h.systemTokens ?? (typeof h.system === "string" && h.system !== "" ? estimateSystem(h.system) : void 0);
663
+ if (systemTokens !== void 0) record.systemTokens = systemTokens;
664
+ return record;
665
+ }) });
631
666
  return {
632
667
  key: "contextHeaders",
633
- schema: contextHeadersSchema,
634
- view,
635
668
  stateSchema: contextHeadersStateSchema,
636
669
  wire: {
637
670
  viewSchema: contextHeadersSchema,
@@ -1432,23 +1465,20 @@ const timelineStateSchema = z.object({
1432
1465
  * or fold semantics change (invalidation of cached rows); config-only
1433
1466
  * changes never require it (bounds tune retention, not state shape).
1434
1467
  *
1435
- * The definition carries BOTH session-projection contracts (see compat.ts):
1436
- * `schema`/`view` for dsh <= 0.1.0-rc.8, `stateSchema`/`wire` for
1437
- * dsh >= 0.1.1-rc.1 each registry reads its own fields off the same unit.
1438
- * (The return type is the mirrored dual contract, not the installed dts
1439
- * `ProjectionDefinition`: the 0.1.1+ registry's wired-register overload
1440
- * demands `wire` PRESENT, which the dts's optional `wire?` fails.)
1441
- * Without the `wire` block the 0.1.1-rc.1+ registry treats the unit as
1442
- * host-only and never delivers `contextTimeline` to the browser (the Context
1443
- * tab would stay on its loading screen forever).
1468
+ * The definition carries the session-projection contract served by every
1469
+ * supported harness (see compat.ts): `stateSchema` + a REQUIRED `wire` block.
1470
+ * (The return type is the mirrored contract, not the installed dts
1471
+ * `ProjectionDefinition`: the registry's wired-register overload demands
1472
+ * `wire` PRESENT, which the dts's optional `wire?` fails.) Without the
1473
+ * `wire` block the registry treats the unit as host-only and never delivers
1474
+ * `contextTimeline` to the browser (the Context tab would stay on its
1475
+ * loading screen forever).
1444
1476
  */
1445
1477
  function createContextTimelineDefinition(config) {
1446
1478
  const bounds = resolveBounds(config);
1447
1479
  const view = (state) => buildTimelineView(state, bounds);
1448
1480
  return {
1449
1481
  key: "contextTimeline",
1450
- schema: contextTimelineSchema,
1451
- view,
1452
1482
  stateSchema: timelineStateSchema,
1453
1483
  wire: {
1454
1484
  viewSchema: contextTimelineSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.38.5",
3
+ "version": "0.40.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": {
@@ -38,7 +38,6 @@
38
38
  "register": "bash scripts/register.sh",
39
39
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.tests.json",
40
40
  "test": "pnpm run typecheck && vitest run --coverage",
41
- "test:bundle": "node scripts/bundle-smoke.mjs",
42
41
  "coverage": "vitest run --coverage",
43
42
  "release": "bash scripts/publish.sh",
44
43
  "hooks": "bash scripts/install-hooks.sh",
@@ -73,9 +72,9 @@
73
72
  },
74
73
  "peerDependencies": {
75
74
  "@deepseek-ai/cordis": "^4.0.1",
76
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.7",
77
- "@deepseek-ai/dsh-session": ">=0.1.0-rc.7",
78
- "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7",
75
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.1-rc.2",
76
+ "@deepseek-ai/dsh-session": ">=0.1.1-rc.2",
77
+ "@deepseek-ai/dsh-settings": ">=0.1.1-rc.2",
79
78
  "@deepseek-ai/schemastery": "^3.18.1",
80
79
  "react": "^18.3.1"
81
80
  },
@@ -89,12 +88,11 @@
89
88
  },
90
89
  "devDependencies": {
91
90
  "@deepseek-ai/cordis": "^4.0.1",
92
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
93
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
94
- "@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
95
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
91
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
92
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
93
+ "@deepseek-ai/dsh-session-projection": "0.1.2-alpha.2",
94
+ "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
96
95
  "@deepseek-ai/schemastery": "^3.18.1",
97
- "zod": "^4.4.3",
98
96
  "@stylistic/eslint-plugin": "^5.10.0",
99
97
  "@types/node": "^26.2.0",
100
98
  "@types/react": "^18.3.31",
@@ -110,7 +108,8 @@
110
108
  "react-dom": "^18.3.1",
111
109
  "tsdown": "^0.22.14",
112
110
  "typescript": "^7.0.2",
113
- "vitest": "^4.1.11"
111
+ "vitest": "^4.1.11",
112
+ "zod": "^4.4.3"
114
113
  },
115
114
  "packageManager": "pnpm@11.9.0"
116
115
  }