dsh-context 0.52.0 → 0.52.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,13 +59,22 @@ Open any session and click the **Context / 上下文** tab:
59
59
 
60
60
  The headline occupancy and composition read the **same official token-meter projections as the chat composer's context ring** (`contextPressure` / `contextBreakdown`), so the figures always match what the ring tells you.
61
61
 
62
- ### 🧱 Current Context — who ate the budget
62
+ ### Context Stats
63
+
64
+ #### Token Stats and Timing Stats
65
+ ![Token_Stats_and_Timing_Stats](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/token-stats-and-timing-stats.png)
66
+
67
+ #### Context Stats
68
+ ![Context_Stats](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-stats.png)
69
+
70
+
71
+ ### 🧱 Current Context — what's in the context window now
63
72
 
64
73
  ![Current Context card](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/current-context.png)
65
74
 
66
75
  A six-color stacked bar against the model's full window (hatching = free headroom): system prompt, tool schemas, user messages, injected context, assistant replies, tool results — each with its ≈token figure and share. When a conversation starts degrading, this is where you see *which part* is responsible.
67
76
 
68
- ### 📈 Context Trend — every request, sized and explained
77
+ ### 📈 Context Trend — how the context grew and evolved by turn or steps
69
78
 
70
79
  ![Context Trend with the step brief](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-trend.png)
71
80
 
package/lib/client.js CHANGED
@@ -211,7 +211,7 @@ window.__ModuleLoader__.load({
211
211
  "gate.minimum": "最低要求",
212
212
  "gate.orNewer": "及更新的版本",
213
213
  "gate.ok": "知道了",
214
- "footer": "估算口径:与 dsh 内置 tokenMeter 相同的固定密度启发式(约 4 字符 ≈ 1 token);「实际」为供应商上报用量。",
214
+ "footer": "Token 数量统计:「估算」与 DeepSeek Harness 内置 tokenMeter 相同的固定密度启发式(约 4 字符 ≈ 1 token);「实际」为供应商上报用量。",
215
215
  "tip.step": "第 {t} 轮 · 第 {s} 步 (共 {n} 步)",
216
216
  "tip.turn": "第 {t} 轮 · 共 {n} 步",
217
217
  "tip.turn1": "第 {t} 轮 · 共 1 步",
@@ -237,6 +237,7 @@ window.__ModuleLoader__.load({
237
237
  "node.nonText": "(非文本消息)",
238
238
  "node.snapshot": "快照: ",
239
239
  "node.skillTag": "技能 · {name}",
240
+ "cmd.label": "上下文",
240
241
  "cmd.section": "上下文",
241
242
  "cmd.desc": "查看当前上下文构成,浏览各步骤组成",
242
243
  "cmd.close": "关闭",
@@ -514,7 +515,7 @@ window.__ModuleLoader__.load({
514
515
  "gate.minimum": "Minimum required",
515
516
  "gate.orNewer": "or newer",
516
517
  "gate.ok": "Got it",
517
- "footer": "Estimate: same fixed-density heuristic as dsh’s built-in tokenMeter (~4 chars ≈ 1 token); “actual” is provider-reported usage.",
518
+ "footer": "Token amount: “Estimated” uses the same fixed-density heuristic as DeepSeek Harness’s built-in tokenMeter (~4 chars ≈ 1 token); “Actual” is provider-reported usage.",
518
519
  "tip.step": "Turn {t} · Step {s} of {n}",
519
520
  "tip.turn": "Turn {t} · {n} steps",
520
521
  "tip.turn1": "Turn {t} · 1 step",
@@ -540,6 +541,7 @@ window.__ModuleLoader__.load({
540
541
  "node.nonText": "(non-text message)",
541
542
  "node.snapshot": "Snapshot: ",
542
543
  "node.skillTag": "Skill · {name}",
544
+ "cmd.label": "Context",
543
545
  "cmd.section": "Context",
544
546
  "cmd.desc": "View current context makeup, browse per-step composition",
545
547
  "cmd.close": "Close",
@@ -615,6 +617,58 @@ window.__ModuleLoader__.load({
615
617
  "attach.tokensTip": "Estimated token cost via DeepSeek's official image-size conversion (384-token cap per image)"
616
618
  };
617
619
  //#endregion
620
+ //#region src/client/icon.tsx
621
+ /** The sheet's strokes, in paint order, as `[path, fill]` pairs. */
622
+ const SHEET_PATHS = [
623
+ ["M899.984 19.873h-3.452c-26.123 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.173 47.298 47.296 47.298h3.452c26.119 0 47.297-21.171 47.297-47.298V67.169c0-26.124-21.177-47.296-47.297-47.296z", "#4A5699"],
624
+ ["M132.643 19.873h-3.449c-26.12 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.177 47.298 47.296 47.298h3.449c26.123 0 47.299-21.171 47.299-47.298V67.169c0-26.124-21.176-47.296-47.299-47.296z", "#C45FA0"],
625
+ ["M899.463 19.873H129.194c-26.12 0-47.296 21.172-47.296 47.296v3.377c0 26.12 21.177 47.299 47.296 47.299h770.269c26.123 0 47.296-21.179 47.296-47.299v-3.377c0-26.124-21.173-47.296-47.296-47.296z", "#6277BA"],
626
+ ["M899.463 905.006H129.194c-26.12 0-47.296 21.17-47.296 47.29v3.381c0 26.127 21.177 47.298 47.296 47.298h770.269c26.123 0 47.296-21.171 47.296-47.298v-3.381c0-26.12-21.173-47.29-47.296-47.29z", "#C45FA0"],
627
+ ["M717.962 543.153H542.047c-26.121 0-47.298 21.175-47.298 47.297v3.724c0 26.123 21.177 47.293 47.298 47.293h175.915c26.121 0 47.297-21.17 47.297-47.293v-3.724c0-26.122-21.176-47.297-47.297-47.297z", "#E5594F"],
628
+ ["M689.268 198.849H513.355c-26.122 0-47.298 21.175-47.298 47.297v3.722c0 26.12 21.176 47.297 47.298 47.297h175.912c26.122 0 47.298-21.177 47.298-47.297v-3.722c0-26.122-21.175-47.297-47.297-47.297z", "#F0D043"],
629
+ ["M757.789 353.081H261.17c-26.121 0-47.297 21.172-47.297 47.296v3.377c0 26.121 21.177 47.299 47.297 47.299h496.619c26.121 0 47.296-21.178 47.296-47.299v-3.377c0-26.125-21.175-47.296-47.296-47.296z", "#E5594F"],
630
+ ["M762.638 726.225h-496.62c-26.12 0-47.294 21.18-47.294 47.301v3.377c0 26.12 21.174 47.3 47.294 47.3h496.62c26.122 0 47.296-21.18 47.296-47.3v-3.377c0-26.122-21.174-47.301-47.296-47.301z", "#6277BA"],
631
+ ["M355.734 543.328H281.41c-26.122 0-47.297 21.17-47.297 47.293v3.378c0 26.118 21.175 47.297 47.297 47.297h74.324c26.123 0 47.296-21.179 47.296-47.297v-3.378c0-26.123-21.174-47.293-47.296-47.293z", "#F39A2B"],
632
+ ["M334.85 248.006m-48.986 0a48.986 48.986 0 1 0 97.972 0 48.986 48.986 0 1 0-97.972 0Z", "#F39A2B"]
633
+ ];
634
+ /** The colourful document sheet at the requested square edge. */
635
+ function ContextIcon({ size = 20, className }) {
636
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
637
+ width: size,
638
+ height: size,
639
+ viewBox: "0 0 1024 1024",
640
+ className,
641
+ "aria-hidden": "true",
642
+ xmlns: "http://www.w3.org/2000/svg",
643
+ children: SHEET_PATHS.map(([d, fill]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
644
+ d,
645
+ fill
646
+ }, d))
647
+ });
648
+ }
649
+ /**
650
+ * The tab chip's title seat (`sidebar.right.pane.tab.title`): the emblem before
651
+ * the label, so the chip reads as the files chip does. The label comes from the
652
+ * plugin's own bound translate — read at render, so the chip follows the active
653
+ * locale — rather than the tab-information hook, which a foreign or
654
+ * not-yet-committed tab record can throw on. It carries a trailing gutter
655
+ * (`.lc-title-label`) so the active chip's fade lands past the text, never on
656
+ * the last glyphs.
657
+ * @param t - the plugin-namespace translate bound in `apply`.
658
+ * @returns the title component to register under the tab type's id.
659
+ */
660
+ function makeContextTabTitle(t) {
661
+ return function ContextTabTitle() {
662
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextIcon, {
663
+ size: 16,
664
+ className: "lc-title-icon"
665
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
666
+ className: "lc-title-label",
667
+ children: t("tab")
668
+ })] });
669
+ };
670
+ }
671
+ //#endregion
618
672
  //#region src/client/modalStore.ts
619
673
  const stores$1 = /* @__PURE__ */ new Map();
620
674
  function modalStoreOf(sessionId) {
@@ -677,6 +731,8 @@ window.__ModuleLoader__.load({
677
731
  if (query !== "" && !COMMAND.startsWith(query)) return Promise.resolve([]);
678
732
  return Promise.resolve([{
679
733
  name: COMMAND,
734
+ label: kit.t("cmd.label"),
735
+ icon: ContextIcon,
680
736
  section: kit.t("cmd.section"),
681
737
  description: kit.t("cmd.desc")
682
738
  }]);
@@ -1849,10 +1905,10 @@ window.__ModuleLoader__.load({
1849
1905
  * - INLINE (older hosts, channel-less deployments, the baseline-gate
1850
1906
  * fallback): the wire value carries the collections in place. It passes
1851
1907
  * through untouched — no fetch ever happens.
1852
- * - SPLIT (current host with the detail channel live): the wire value is the
1908
+ * - SPLIT (current host with the detail route live): the wire value is the
1853
1909
  * slim head (~1KB — every session.list row, control baseline, and push
1854
- * frame carries it whole). The collections arrive from the host's
1855
- * `/dsh-context` `detail` endpoint (host/detail.ts): one targeted read
1910
+ * frame carries it whole). The collections arrive from the plugin's
1911
+ * `/api/dsh-context/detail` fetch route (host/detail.ts): one targeted read
1856
1912
  * when the tab/modal first opens, then a debounced refetch whenever the
1857
1913
  * pushed `detailRev` outruns the served detail. Closed tabs fetch nothing.
1858
1914
  *
@@ -1867,8 +1923,7 @@ window.__ModuleLoader__.load({
1867
1923
  * failure surfaces as a retryable note on the detail cards instead of an
1868
1924
  * empty chart.
1869
1925
  */
1870
- const DETAIL_CHANNEL = "/dsh-context";
1871
- const DETAIL_ENDPOINT = "detail";
1926
+ const DETAIL_ROUTE = "/api/dsh-context/detail";
1872
1927
  /**
1873
1928
  * Narrow the detail endpoint's payload to a render-safe value (the same
1874
1929
  * boundary rigor as `timelineOf`): collections re-proved per item, scalars
@@ -1879,8 +1934,10 @@ window.__ModuleLoader__.load({
1879
1934
  const data = asRecord(value);
1880
1935
  if (data === null) return null;
1881
1936
  if (typeof data.rev !== "number" || !Number.isFinite(data.rev) || data.rev < 0) return null;
1937
+ const head = timelineOf(data.head);
1882
1938
  return {
1883
1939
  rev: data.rev,
1940
+ ...head !== null ? { head } : {},
1884
1941
  requests: objectsOf(data.requests),
1885
1942
  events: objectsOf(data.events),
1886
1943
  nodes: objectsOf(data.nodes),
@@ -1893,24 +1950,26 @@ window.__ModuleLoader__.load({
1893
1950
  };
1894
1951
  }
1895
1952
  /**
1896
- * The detail reader over the harness's generic Connection RPC
1897
- * (`ctx.connection.rpc.call`) — resolved through the shared `rpcCallOf`
1898
- * reflect read (the same seam openPathVia/canOpenPathsOf use), so a hostile
1899
- * or absent connection service degrades to `undefined` instead of throwing.
1900
- * The returned thunk resolves the session's current detail, `null` when the
1901
- * session is not live anymore, and rejects on transport failure or a
1902
- * malformed payload (the store turns the two into the retryable state).
1953
+ * The detail reader over the plugin's own `/api` fetch route (host/detail.ts).
1954
+ * Same-origin POST through the harness's authenticated fence; the returned
1955
+ * thunk resolves the session's current detail, `null` when the session is not
1956
+ * live anymore, and rejects on transport failure or a malformed payload (the
1957
+ * store turns the two into the retryable state).
1903
1958
  */
1904
1959
  function makeDetailFetcher(ctx, sessionId) {
1905
1960
  if (sessionId === "") return void 0;
1906
- const call = rpcCallOf(ctx);
1907
- if (call === void 0) return void 0;
1908
1961
  return async () => {
1909
- const r = asRecord(await call(DETAIL_CHANNEL, DETAIL_ENDPOINT, { sessionId }));
1910
- if (r === null || r.ok !== true) throw new Error("dsh-context: detail rpc failed");
1962
+ const response = await fetch(DETAIL_ROUTE, {
1963
+ method: "POST",
1964
+ headers: { "content-type": "application/json" },
1965
+ body: JSON.stringify({ sessionId })
1966
+ });
1967
+ if (!response.ok) throw new Error(`dsh-context: detail route HTTP ${response.status}`);
1968
+ const r = asRecord(await response.json());
1969
+ if (r === null || r.ok !== true) throw new Error("dsh-context: detail read failed");
1911
1970
  if (r.value === null) return null;
1912
1971
  const detail = detailOf(r.value);
1913
- if (detail === null) throw new Error("dsh-context: detail rpc malformed");
1972
+ if (detail === null) throw new Error("dsh-context: detail read malformed");
1914
1973
  return detail;
1915
1974
  };
1916
1975
  }
@@ -4114,6 +4173,14 @@ window.__ModuleLoader__.load({
4114
4173
  for (const n of asm.nodes) (m[n.cat] ??= []).push(n);
4115
4174
  return m;
4116
4175
  }
4176
+ /**
4177
+ * The producer identity a fold-stamped injection node carries
4178
+ * (shared/types.ts SurfaceNode.name): '' when absent or drift-typed — the
4179
+ * delivered payload is untrusted, so rows re-prove it before rendering.
4180
+ */
4181
+ function nodeNameOf(n) {
4182
+ return typeof n.name === "string" ? n.name : "";
4183
+ }
4117
4184
  function countOf(asm, byCat, c) {
4118
4185
  if (c === "system") return asm.system !== null ? 1 : 0;
4119
4186
  if (c === "tools") return asm.header !== null ? asm.header.tools.length : 0;
@@ -4422,12 +4489,13 @@ window.__ModuleLoader__.load({
4422
4489
  const rowErr = (n.cat === "tool" || n.cat === "skill") && toolErrOf(n, conv).err;
4423
4490
  let tag = null;
4424
4491
  let preview = nodeText(n);
4492
+ const id = nodeNameOf(n);
4425
4493
  if (n.cat === "tool") {
4426
4494
  tag = n.tool ?? "?";
4427
4495
  preview = callSummaryOf(conv) ?? t("node.toolResult");
4428
4496
  } else if (n.cat === "skill") {
4429
4497
  tag = n.skill !== void 0 ? t("node.skillTag", { name: n.skill }) : t("form." + (n.form || "context"));
4430
- preview = (n.text !== void 0 && n.text !== "" ? n.text : null) ?? callSummaryOf(conv) ?? preview;
4498
+ preview = (n.skill === void 0 && id !== "" ? id : null) ?? (n.text !== void 0 && n.text !== "" ? n.text : null) ?? callSummaryOf(conv) ?? preview;
4431
4499
  } else if (n.cat === "assistant" && Array.isArray(n.calls) && n.calls.length > 0) {
4432
4500
  tag = n.calls.join(" › ");
4433
4501
  preview = (n.text !== void 0 && n.text !== "" ? n.text : null) ?? blockSummaryOf(conv) ?? t("node.empty");
@@ -4437,7 +4505,8 @@ window.__ModuleLoader__.load({
4437
4505
  if (imgCount > 0 && openElem !== `n${n.seq}`) tag = t("attach.image") + (imgCount > 1 ? " ×" + String(imgCount) : "");
4438
4506
  } else if (n.cat === "inject") {
4439
4507
  tag = t("form." + (n.form || "context"));
4440
- if (n.text !== void 0 && n.text !== "") preview = n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
4508
+ if (id !== "") preview = id;
4509
+ else if (n.text !== void 0 && n.text !== "") preview = n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
4441
4510
  }
4442
4511
  return {
4443
4512
  n,
@@ -4448,7 +4517,7 @@ window.__ModuleLoader__.load({
4448
4517
  };
4449
4518
  });
4450
4519
  const q = rowQuery.trim().toLowerCase();
4451
- const shown = q === "" ? rows : rows.filter((r) => (r.tag ?? "").toLowerCase().includes(q) || r.preview.toLowerCase().includes(q));
4520
+ const shown = q === "" ? rows : rows.filter((r) => (r.tag ?? "").toLowerCase().includes(q) || r.preview.toLowerCase().includes(q) || typeof r.n.text === "string" && r.n.text.toLowerCase().includes(q));
4452
4521
  const rowctl = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RowToolbar, {
4453
4522
  value: rowQuery,
4454
4523
  placeholder: t("browser.search." + c),
@@ -4591,7 +4660,7 @@ window.__ModuleLoader__.load({
4591
4660
  className: "lc-br-cat" + (openable ? "" : " lc-br-cat-empty"),
4592
4661
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4593
4662
  type: "button",
4594
- className: "lc-br-cat-row hover:bg-(--dsw-alias-interactive-bg-hover)" + (linked && props.hoverKey === c.key ? " lc-br-cat-on" : ""),
4663
+ className: "lc-br-cat-row hover:bg-(--dsw-alias-interactive-bg-hover)" + (open ? " lc-br-cat-open" : "") + (linked && props.hoverKey === c.key ? " lc-br-cat-on" : ""),
4595
4664
  /* v8 ignore start -- the handlers exist only when linked,
4596
4665
  and linked already requires onHoverKey defined (above). */
4597
4666
  onMouseEnter: linked ? () => {
@@ -6305,8 +6374,15 @@ window.__ModuleLoader__.load({
6305
6374
  * no current session, or a snapshot without a `byId` table (older harness).
6306
6375
  * The current session synthesizes a row when the list has not delivered it
6307
6376
  * yet, so the card can still show its live self stats.
6308
- */
6309
- function agentForestOf(snapshot, currentId, self) {
6377
+ * @param fetched - detail-channel heads for COLD rows, keyed by session id.
6378
+ * The host's list block serves projection values only from the projection
6379
+ * cache, so a relative that never attached since the timeline unit last
6380
+ * changed lists pressure-only (no composition row) and its ring would
6381
+ * degrade to the occupancy arc; the card fetches those heads on demand.
6382
+ * A row's own value always wins — the injection lands only on rows whose
6383
+ * `contextTimeline` is absent.
6384
+ */
6385
+ function agentForestOf(snapshot, currentId, self, fetched) {
6310
6386
  const byId = asRecord(asRecord(snapshot)?.byId);
6311
6387
  if (byId === null || currentId === void 0 || currentId === "") return null;
6312
6388
  const rows = /* @__PURE__ */ new Map();
@@ -6361,7 +6437,12 @@ window.__ModuleLoader__.load({
6361
6437
  const visit = (id, row, parentId, depth, seen, family) => {
6362
6438
  if (seen.has(id) || nodes.length >= 25) return;
6363
6439
  seen.add(id);
6364
- const stats = agentStatsOf(row.projections);
6440
+ const values = row.projections;
6441
+ const fetchedHead = fetched !== void 0 ? fetched.get(id) : void 0;
6442
+ const stats = agentStatsOf(fetchedHead !== void 0 && values?.contextTimeline === void 0 ? {
6443
+ ...values,
6444
+ contextTimeline: fetchedHead
6445
+ } : values);
6365
6446
  const identity = stats.identity;
6366
6447
  const node = {
6367
6448
  ...stats,
@@ -6620,9 +6701,13 @@ window.__ModuleLoader__.load({
6620
6701
  *
6621
6702
  * Data rides the harness's existing planes end to end — the session-list
6622
6703
  * snapshot (`ctx.sessions.list`: lineage rows + per-session projection
6623
- * values) and the tab's own projections for the current node — so the card
6624
- * adds no RPC of its own beyond one direct-child catalog refresh per
6625
- * session. A harness without the outward sessions service hides the card.
6704
+ * values) and the tab's own projections for the current node. The list block
6705
+ * serves projection values only from the host's projection cache, so a
6706
+ * relative that never attached since the timeline unit last changed lists
6707
+ * pressure-only (occupancy without composition); those nodes fetch their slim
6708
+ * head from the plugin's `/api` detail route (the one call below) and
6709
+ * re-render composed. A harness without the outward sessions service hides
6710
+ * the card.
6626
6711
  */
6627
6712
  /** Caption box height under a node (3 wrapped label lines + the tokens line). */
6628
6713
  const CAPTION_H = 60;
@@ -6635,6 +6720,7 @@ window.__ModuleLoader__.load({
6635
6720
  }
6636
6721
  function makeAgentGraph(ctx, kit) {
6637
6722
  const { t, fmt, catLabel } = kit;
6723
+ const heads = /* @__PURE__ */ new Map();
6638
6724
  function AgentGraph(props) {
6639
6725
  const face = (0, react.useMemo)(() => sessionsFaceOf(ctx), []);
6640
6726
  const subscribe = (0, react.useCallback)((fn) => {
@@ -6680,8 +6766,9 @@ window.__ModuleLoader__.load({
6680
6766
  if (typeof face.refreshSubagents !== "function") return;
6681
6767
  face.refreshSubagents(sessionId).catch(() => {});
6682
6768
  }, [face, sessionId]);
6769
+ const [landed, setLanded] = (0, react.useState)(/* @__PURE__ */ new Map());
6683
6770
  const built = (0, react.useMemo)(() => {
6684
- const forest = agentForestOf(snapshot, sessionId, props.self);
6771
+ const forest = agentForestOf(snapshot, sessionId, props.self, landed);
6685
6772
  return forest !== null ? {
6686
6773
  forest,
6687
6774
  layout: layoutForest(forest, stageWidth)
@@ -6690,8 +6777,29 @@ window.__ModuleLoader__.load({
6690
6777
  snapshot,
6691
6778
  sessionId,
6692
6779
  props.self,
6693
- stageWidth
6780
+ stageWidth,
6781
+ landed
6694
6782
  ]);
6783
+ (0, react.useEffect)(() => {
6784
+ if (built === null) return;
6785
+ const attach = (pending, id) => {
6786
+ pending.then((head) => {
6787
+ if (head !== null) setLanded((prev) => prev.get(id) === head ? prev : new Map(prev).set(id, head));
6788
+ }).catch(() => {});
6789
+ };
6790
+ for (const n of built.forest.nodes) {
6791
+ if (n.isCurrent || n.head !== null && n.head.parts.length > 0) continue;
6792
+ const cached = heads.get(n.id);
6793
+ if (cached !== void 0) {
6794
+ attach(cached, n.id);
6795
+ continue;
6796
+ }
6797
+ const fetcher = makeDetailFetcher(ctx, n.id);
6798
+ const pending = fetcher !== void 0 ? fetcher().then((d) => d?.head ?? null) : Promise.resolve(null);
6799
+ heads.set(n.id, pending);
6800
+ attach(pending, n.id);
6801
+ }
6802
+ }, [built]);
6695
6803
  if (built === null) return null;
6696
6804
  const { forest, layout } = built;
6697
6805
  const byId = new Map(forest.nodes.map((n) => [n.id, n]));
@@ -7533,7 +7641,7 @@ window.__ModuleLoader__.load({
7533
7641
  return function PluginInfo() {
7534
7642
  const [latest, setLatest] = (0, react.useState)(null);
7535
7643
  (0, react.useEffect)(() => {
7536
- if ("0.52.0".includes("-dev")) return;
7644
+ if ("0.52.2".includes("-dev")) return;
7537
7645
  let on = true;
7538
7646
  fetchLatestVersion().then((v) => {
7539
7647
  if (on && v) setLatest(v);
@@ -7542,8 +7650,8 @@ window.__ModuleLoader__.load({
7542
7650
  on = false;
7543
7651
  };
7544
7652
  }, []);
7545
- const update = latest !== null && isNewerVersion(latest, "0.52.0") ? latest : null;
7546
- const nameText = "dsh-context (v0.52.0)";
7653
+ const update = latest !== null && isNewerVersion(latest, "0.52.2") ? latest : null;
7654
+ const nameText = "dsh-context (v0.52.2)";
7547
7655
  const nameValue = [nameText];
7548
7656
  if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7549
7657
  className: "lc-pi-update",
@@ -9382,58 +9490,6 @@ window.__ModuleLoader__.load({
9382
9490
  };
9383
9491
  }
9384
9492
  //#endregion
9385
- //#region src/client/icon.tsx
9386
- /** The sheet's strokes, in paint order, as `[path, fill]` pairs. */
9387
- const SHEET_PATHS = [
9388
- ["M899.984 19.873h-3.452c-26.123 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.173 47.298 47.296 47.298h3.452c26.119 0 47.297-21.171 47.297-47.298V67.169c0-26.124-21.177-47.296-47.297-47.296z", "#4A5699"],
9389
- ["M132.643 19.873h-3.449c-26.12 0-47.296 21.172-47.296 47.296v888.508c0 26.127 21.177 47.298 47.296 47.298h3.449c26.123 0 47.299-21.171 47.299-47.298V67.169c0-26.124-21.176-47.296-47.299-47.296z", "#C45FA0"],
9390
- ["M899.463 19.873H129.194c-26.12 0-47.296 21.172-47.296 47.296v3.377c0 26.12 21.177 47.299 47.296 47.299h770.269c26.123 0 47.296-21.179 47.296-47.299v-3.377c0-26.124-21.173-47.296-47.296-47.296z", "#6277BA"],
9391
- ["M899.463 905.006H129.194c-26.12 0-47.296 21.17-47.296 47.29v3.381c0 26.127 21.177 47.298 47.296 47.298h770.269c26.123 0 47.296-21.171 47.296-47.298v-3.381c0-26.12-21.173-47.29-47.296-47.29z", "#C45FA0"],
9392
- ["M717.962 543.153H542.047c-26.121 0-47.298 21.175-47.298 47.297v3.724c0 26.123 21.177 47.293 47.298 47.293h175.915c26.121 0 47.297-21.17 47.297-47.293v-3.724c0-26.122-21.176-47.297-47.297-47.297z", "#E5594F"],
9393
- ["M689.268 198.849H513.355c-26.122 0-47.298 21.175-47.298 47.297v3.722c0 26.12 21.176 47.297 47.298 47.297h175.912c26.122 0 47.298-21.177 47.298-47.297v-3.722c0-26.122-21.175-47.297-47.297-47.297z", "#F0D043"],
9394
- ["M757.789 353.081H261.17c-26.121 0-47.297 21.172-47.297 47.296v3.377c0 26.121 21.177 47.299 47.297 47.299h496.619c26.121 0 47.296-21.178 47.296-47.299v-3.377c0-26.125-21.175-47.296-47.296-47.296z", "#E5594F"],
9395
- ["M762.638 726.225h-496.62c-26.12 0-47.294 21.18-47.294 47.301v3.377c0 26.12 21.174 47.3 47.294 47.3h496.62c26.122 0 47.296-21.18 47.296-47.3v-3.377c0-26.122-21.174-47.301-47.296-47.301z", "#6277BA"],
9396
- ["M355.734 543.328H281.41c-26.122 0-47.297 21.17-47.297 47.293v3.378c0 26.118 21.175 47.297 47.297 47.297h74.324c26.123 0 47.296-21.179 47.296-47.297v-3.378c0-26.123-21.174-47.293-47.296-47.293z", "#F39A2B"],
9397
- ["M334.85 248.006m-48.986 0a48.986 48.986 0 1 0 97.972 0 48.986 48.986 0 1 0-97.972 0Z", "#F39A2B"]
9398
- ];
9399
- /** The colourful document sheet at the requested square edge. */
9400
- function ContextIcon({ size = 20, className }) {
9401
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
9402
- width: size,
9403
- height: size,
9404
- viewBox: "0 0 1024 1024",
9405
- className,
9406
- "aria-hidden": "true",
9407
- xmlns: "http://www.w3.org/2000/svg",
9408
- children: SHEET_PATHS.map(([d, fill]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
9409
- d,
9410
- fill
9411
- }, d))
9412
- });
9413
- }
9414
- /**
9415
- * The tab chip's title seat (`sidebar.right.pane.tab.title`): the emblem before
9416
- * the label, so the chip reads as the files chip does. The label comes from the
9417
- * plugin's own bound translate — read at render, so the chip follows the active
9418
- * locale — rather than the tab-information hook, which a foreign or
9419
- * not-yet-committed tab record can throw on. It carries a trailing gutter
9420
- * (`.lc-title-label`) so the active chip's fade lands past the text, never on
9421
- * the last glyphs.
9422
- * @param t - the plugin-namespace translate bound in `apply`.
9423
- * @returns the title component to register under the tab type's id.
9424
- */
9425
- function makeContextTabTitle(t) {
9426
- return function ContextTabTitle() {
9427
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextIcon, {
9428
- size: 16,
9429
- className: "lc-title-icon"
9430
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9431
- className: "lc-title-label",
9432
- children: t("tab")
9433
- })] });
9434
- };
9435
- }
9436
- //#endregion
9437
9493
  //#region src/client/sidebar.ts
9438
9494
  /**
9439
9495
  * The right Sidebar's Context tab (dsh 0.1.5-rc.1+).
@@ -9665,7 +9721,7 @@ window.__ModuleLoader__.load({
9665
9721
  }
9666
9722
  //#endregion
9667
9723
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/browser.css.mjs
9668
- const css$3 = ".lc-br-dna-ctl{flex:none;margin-left:0}.lc-br-bar-dna .lc-stacked-seg{background-image:linear-gradient(to right, transparent calc(100% - min(.5px, 12.5%)), var(--dsw-alias-bg-layer-1) 0)}.lc-br-hint{color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto;font-size:11px;font-weight:400}.lc-br-pick{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;max-width:240px;padding:3px 6px;font-size:12px}.lc-br-meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:6px 16px;margin-bottom:8px;display:flex}.lc-br-meta b{color:var(--dsw-alias-label-primary)}.lc-br-meta .lc-est{margin-left:auto}.lc-br-note{color:var(--dsw-alias-label-secondary);margin-top:8px;font-size:12px}.lc-br-retry{color:var(--dsw-alias-brand-primary);font:inherit;cursor:pointer;text-underline-offset:2px;transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;padding:0;font-size:12px;text-decoration:underline}.lc-br-cats{flex-direction:column;gap:4px;margin-top:10px;display:flex}.lc-br-cat{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}.lc-br-cat-empty{opacity:.55}.lc-br-cat-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;align-items:center;gap:8px;padding:7px 10px;display:flex}.lc-br-cat-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-cat-on i{box-shadow:0 0 0 1px var(--dsw-alias-brand-primary)}.lc-br-cat-row i{width:8px;height:8px;transition:box-shadow var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:2px;flex:none;display:inline-block}.lc-br-cat-label{font-weight:600}.lc-br-cat-count{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:12px}.lc-br-count-grp{flex:1;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-chev{width:12px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:flex}.lc-br-chev:before{content:\"\";width:5px;height:5px;transition:transform var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-bottom:1px solid;border-right:1px solid;transform:rotate(-45deg)}.lc-br-chev-on:before{transform:rotate(45deg)}.lc-br-tokens{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-delta,.lc-br-tdelta{white-space:nowrap;border-radius:4px;flex:none;padding:0 5px;font-size:11px;font-weight:600}.lc-br-delta-up,.lc-br-tdelta-up{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent)}.lc-br-delta-down,.lc-br-tdelta-down{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent)}.lc-br-tokens-grp{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-pct{text-align:right;width:36px;color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-body{border-top:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:2px;padding:4px 6px;display:flex}.lc-br-elem{border-radius:6px}.lc-br-elem-on{background:var(--dsw-alias-interactive-bg-active)}.lc-br-elem-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:6px;align-items:center;gap:8px;padding:5px 6px;font-size:12px;display:flex}.lc-br-err-dot{background:var(--dsw-alias-state-error-primary);width:6px;height:6px;box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border-radius:50%;flex:none}.lc-br-tag{min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);text-overflow:ellipsis;white-space:nowrap;border-radius:4px;max-width:220px;padding:0 6px;font-size:11px;overflow:hidden}.lc-br-tool-plugin{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent);border-color:#0000;border-radius:6px;font-size:12px}.lc-br-preview{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.lc-br-time,.lc-br-hits{color:var(--dsw-alias-label-secondary);flex:none;font-size:11px}.lc-br-content{flex-direction:column;gap:6px;padding:2px 6px 8px 26px;display:flex}.lc-br-dim{color:var(--dsw-alias-label-secondary)}";
9724
+ const css$3 = ".lc-br-dna-ctl{flex:none;margin-left:0}.lc-br-bar-dna .lc-stacked-seg{background-image:linear-gradient(to right, transparent calc(100% - min(.5px, 12.5%)), var(--dsw-alias-bg-layer-1) 0)}.lc-br-hint{color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto;font-size:11px;font-weight:400}.lc-br-pick{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;max-width:240px;padding:3px 6px;font-size:12px}.lc-br-meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:6px 16px;margin-bottom:8px;display:flex}.lc-br-meta b{color:var(--dsw-alias-label-primary)}.lc-br-meta .lc-est{margin-left:auto}.lc-br-note{color:var(--dsw-alias-label-secondary);margin-top:8px;font-size:12px}.lc-br-retry{color:var(--dsw-alias-brand-primary);font:inherit;cursor:pointer;text-underline-offset:2px;transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;padding:0;font-size:12px;text-decoration:underline}.lc-br-cats{flex-direction:column;gap:4px;margin-top:10px;display:flex}.lc-br-cat{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}.lc-br-cat-empty{opacity:.55}.lc-br-cat-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;align-items:center;gap:8px;padding:7px 10px;display:flex}.lc-br-cat-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-cat-on i{box-shadow:0 0 0 1px var(--dsw-alias-brand-primary)}.lc-br-cat-open{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-cat-row i{width:8px;height:8px;transition:box-shadow var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:2px;flex:none;display:inline-block}.lc-br-cat-label{font-weight:600}.lc-br-cat-count{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:12px}.lc-br-count-grp{flex:1;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-chev{width:12px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:flex}.lc-br-chev:before{content:\"\";width:5px;height:5px;transition:transform var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-bottom:1px solid;border-right:1px solid;transform:rotate(-45deg)}.lc-br-chev-on:before{transform:rotate(45deg)}.lc-br-tokens{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-delta,.lc-br-tdelta{white-space:nowrap;border-radius:4px;flex:none;padding:0 5px;font-size:11px;font-weight:600}.lc-br-delta-up,.lc-br-tdelta-up{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent)}.lc-br-delta-down,.lc-br-tdelta-down{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent)}.lc-br-tokens-grp{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-pct{text-align:right;width:36px;color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-body{border-top:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:2px;padding:4px 6px;display:flex}.lc-br-elem{border-radius:6px}.lc-br-elem-on{background:var(--dsw-alias-interactive-bg-active)}.lc-br-elem-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:6px;align-items:center;gap:8px;padding:5px 6px;font-size:12px;display:flex}.lc-br-err-dot{background:var(--dsw-alias-state-error-primary);width:6px;height:6px;box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border-radius:50%;flex:none}.lc-br-tag{min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);text-overflow:ellipsis;white-space:nowrap;border-radius:4px;max-width:220px;padding:0 6px;font-size:11px;overflow:hidden}.lc-br-tool-plugin{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent);border-color:#0000;border-radius:6px;font-size:12px}.lc-br-preview{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.lc-br-time,.lc-br-hits{color:var(--dsw-alias-label-secondary);flex:none;font-size:11px}.lc-br-content{flex-direction:column;gap:6px;padding:2px 6px 8px 26px;display:flex}.lc-br-dim{color:var(--dsw-alias-label-secondary)}";
9669
9725
  const tagId$3 = "dsh-context/browser.css";
9670
9726
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
9671
9727
  const tag = document.createElement("style");
package/lib/index.d.ts CHANGED
@@ -446,6 +446,17 @@ interface Snapshot {
446
446
  */
447
447
  interface ContextTimelineDetail {
448
448
  rev: number;
449
+ /**
450
+ * The slim wire head at the SAME fold cut as the collections: the
451
+ * composition scalars (`current`), the window/model envelope, and the
452
+ * precomputed counts. Sessions listed cold (never attached since the
453
+ * requesting unit last changed) carry no `contextTimeline` projection row
454
+ * for the browser's list reads, so the Agent network card fetches this
455
+ * head per node to render their composition rings. The host always serves
456
+ * it; optional so a payload missing it still serves the collections (the
457
+ * detail cards) and only the ring composition degrades.
458
+ */
459
+ head?: ContextTimeline;
449
460
  requests: RequestRecord[];
450
461
  events: ContextEventRecord[];
451
462
  nodes: SurfaceNode[];
@@ -620,6 +631,14 @@ interface SurfaceNode {
620
631
  */
621
632
  gone?: number;
622
633
  form?: string;
634
+ /**
635
+ * The producer identity the matching inject event names (host pricing.ts
636
+ * `injectionSourceName`: the plugin id, the reconciled instruction files,
637
+ * or the durable kind). Stamped on injection nodes alongside the event, so
638
+ * the browser rows label them the way the events card does; absent when the
639
+ * source carries no readable identity or the node predates the stamp.
640
+ */
641
+ name?: string;
623
642
  text?: string;
624
643
  tool?: string;
625
644
  err?: boolean;
package/lib/index.js CHANGED
@@ -594,10 +594,10 @@ function toolCallNames(blocks) {
594
594
  * context provenance (client-runtime context-provenance.ts): workspace
595
595
  * instructions name the files they were reconciled from, a plugin source its
596
596
  * plugin id, and any other producer its own durable kind. Returns '' when
597
- * the source carries no readable identity at all.
597
+ * the source is missing or carries no readable identity at all.
598
598
  */
599
599
  function injectionSourceName(source) {
600
- if (source.kind === "agent-instructions" && Array.isArray(source.changes)) {
600
+ if (source?.kind === "agent-instructions" && Array.isArray(source.changes)) {
601
601
  const paths = [];
602
602
  for (const change of source.changes) {
603
603
  const path = change?.path;
@@ -605,8 +605,10 @@ function injectionSourceName(source) {
605
605
  }
606
606
  if (paths.length > 0) return paths.join(", ");
607
607
  }
608
- if (typeof source.plugin === "string" && source.plugin !== "") return source.plugin;
609
- return typeof source.kind === "string" && source.kind !== "" ? source.kind : "";
608
+ const plugin = source?.plugin;
609
+ if (typeof plugin === "string" && plugin !== "") return plugin;
610
+ const kind = source?.kind;
611
+ return typeof kind === "string" && kind !== "" ? kind : "";
610
612
  }
611
613
  function isInjection(source) {
612
614
  return source !== null && source !== void 0 && (typeof source.kind === "string" && source.kind !== "" && source.kind !== "user" || typeof source.form === "string");
@@ -1627,7 +1629,10 @@ function applyTimeline(state, event, bounds) {
1627
1629
  rec.name = typeof source.name === "string" ? source.name : "?";
1628
1630
  } else {
1629
1631
  const label = injectionSourceName(source);
1630
- if (label !== "") rec.name = label;
1632
+ if (label !== "") {
1633
+ rec.name = label;
1634
+ node.name = label;
1635
+ }
1631
1636
  if (source.form === "notice" && typeof source.summary === "string" && source.summary !== "") rec.detail = source.summary;
1632
1637
  }
1633
1638
  s.events.push(rec);
@@ -1928,11 +1933,14 @@ function buildTimelineHead(state) {
1928
1933
  }
1929
1934
  /**
1930
1935
  * The on-demand detail payload (host/detail.ts serves it off the live fold
1931
- * state): the heavy collections plus the revision marker the head carries.
1936
+ * state): the heavy collections, the slim head at the SAME cut (the Agent
1937
+ * network's cold-node ring fetch renders composition off it), and the
1938
+ * revision marker the head carries.
1932
1939
  */
1933
1940
  function buildTimelineDetail(state, bounds) {
1934
1941
  return {
1935
1942
  rev: state.detailRev ?? 0,
1943
+ head: buildTimelineHead(state),
1936
1944
  ...detailCollectionsOf(state, bounds)
1937
1945
  };
1938
1946
  }
@@ -1951,63 +1959,71 @@ function buildTimelineView(state, bounds) {
1951
1959
  }
1952
1960
  //#endregion
1953
1961
  //#region src/host/detail.ts
1954
- /** The plugin's generic Connection RPC channel (the pre-v0.9 name, kept). */
1955
- const DETAIL_CHANNEL = "/dsh-context";
1956
- /** The RPC failure envelope the transport expects (ConnectionRpcFailure). */
1962
+ /** The plugin's detail route, under the authenticated `/api` fence. */
1963
+ const DETAIL_ROUTE = "/api/dsh-context/detail";
1964
+ /** One JSON reply of the detail route. */
1965
+ function reply(value) {
1966
+ return Response.json(value, { headers: { "cache-control": "no-store" } });
1967
+ }
1968
+ /** The typed failure envelope (the same shape the Connection RPC carried). */
1957
1969
  function failure(code, message) {
1958
- return {
1970
+ return reply({
1959
1971
  ok: false,
1960
1972
  error: {
1961
1973
  code,
1962
- message,
1963
- details: {}
1974
+ message
1964
1975
  }
1965
- };
1976
+ });
1966
1977
  }
1967
1978
  /**
1968
- * Arm the detail endpoint whenever the connection and sessions services are
1979
+ * Serve the detail route whenever the connection and sessions services are
1969
1980
  * both composed (see the module header for the load-order contract). The
1970
1981
  * registration rides the injected fiber: either service unloading withdraws
1971
- * the channel and closes the gate.
1982
+ * the route and closes the gate.
1972
1983
  */
1973
1984
  function watchDetailChannel(ctx, bounds) {
1974
1985
  const gate = { live: false };
1975
1986
  ctx.inject(["connection", "sessions"], (c) => {
1976
1987
  const connection = c.get("connection");
1977
1988
  const sessions = c.get("sessions");
1978
- const handle = typeof connection?.rpc?.handle === "function" ? connection.rpc.handle.bind(connection.rpc) : void 0;
1989
+ const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
1979
1990
  const getSession = typeof sessions?.get === "function" ? sessions.get.bind(sessions) : void 0;
1980
- if (handle === void 0 || getSession === void 0) return;
1991
+ if (register === void 0 || getSession === void 0) return;
1981
1992
  const projections = ctx.sessionProjections;
1982
- const handler = async (endpoint, payload) => {
1983
- if (endpoint !== "detail") return failure("dsh-context/unknown-endpoint", `unknown endpoint: ${endpoint}`);
1984
- const sessionId = payload !== null && typeof payload === "object" ? payload.sessionId : void 0;
1993
+ const handler = async (request) => {
1994
+ let sessionId;
1995
+ try {
1996
+ const body = await request.json();
1997
+ sessionId = body !== null && typeof body === "object" ? body.sessionId : void 0;
1998
+ } catch {
1999
+ return failure("dsh-context/bad-request", "body is not JSON");
2000
+ }
1985
2001
  if (typeof sessionId !== "string" || sessionId === "") return failure("dsh-context/bad-request", "missing sessionId");
1986
2002
  try {
1987
2003
  const session = getSession(sessionId);
1988
2004
  if (session !== void 0 && session !== null) {
1989
2005
  const state = projections.stateOf(session, "contextTimeline");
1990
- if (state === void 0) return {
2006
+ if (state === void 0) return reply({
1991
2007
  ok: true,
1992
2008
  value: null
1993
- };
1994
- return {
2009
+ });
2010
+ return reply({
1995
2011
  ok: true,
1996
2012
  value: buildTimelineDetail(state, bounds)
1997
- };
2013
+ });
1998
2014
  }
1999
2015
  const query = ctx.get("sessionQuery");
2000
2016
  const observe = typeof query?.observeSession === "function" ? query.observeSession.bind(query) : void 0;
2001
- if (observe === void 0) return {
2017
+ if (observe === void 0) return reply({
2002
2018
  ok: true,
2003
2019
  value: null
2004
- };
2020
+ });
2005
2021
  const observation = await observe(sessionId, { projectionMode: "none" });
2006
2022
  const events = observation?.events;
2007
- if (!Array.isArray(events)) return {
2023
+ if (!Array.isArray(events)) return reply({
2008
2024
  ok: true,
2009
2025
  value: null
2010
- };
2026
+ });
2011
2027
  let state = createTimelineState();
2012
2028
  try {
2013
2029
  for (const ev of events) state = applyTimeline(state, ev, bounds);
@@ -2015,21 +2031,21 @@ function watchDetailChannel(ctx, bounds) {
2015
2031
  const dispose = observation?.[Symbol.dispose];
2016
2032
  if (typeof dispose === "function") dispose.call(observation);
2017
2033
  }
2018
- return {
2034
+ return reply({
2019
2035
  ok: true,
2020
2036
  value: buildTimelineDetail(state, bounds)
2021
- };
2037
+ });
2022
2038
  } catch (err) {
2023
2039
  return failure("gateway/internal", err instanceof Error ? err.message : String(err));
2024
2040
  }
2025
2041
  };
2026
2042
  try {
2027
- c.effect(() => {
2028
- const unregister = handle(DETAIL_CHANNEL, handler);
2029
- return () => {
2030
- unregister();
2031
- };
2032
- }, "dsh-context: detail channel");
2043
+ c.effect(() => register({
2044
+ path: DETAIL_ROUTE,
2045
+ methods: ["POST"],
2046
+ requestBody: "buffered",
2047
+ fetch: handler
2048
+ }), "dsh-context: detail route");
2033
2049
  } catch {
2034
2050
  return;
2035
2051
  }
@@ -2276,6 +2292,7 @@ const surfaceNodeSchema = z.object({
2276
2292
  imgs: z.number().int().nonnegative().optional(),
2277
2293
  gone: z.number().int().nonnegative().optional(),
2278
2294
  form: z.string().optional(),
2295
+ name: z.string().optional(),
2279
2296
  text: z.string().optional(),
2280
2297
  tool: z.string().optional(),
2281
2298
  err: z.boolean().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.52.0",
3
+ "version": "0.52.2",
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": {
@@ -45,7 +45,7 @@
45
45
  "coverage": "vitest run --coverage",
46
46
  "release": "bash scripts/publish.sh",
47
47
  "social-preview": "node docs/social-preview/generate.ts",
48
- "prepare": "husky"
48
+ "prepare": "husky && tsdown"
49
49
  },
50
50
  "dsh": {
51
51
  "bundle": {