dsh-context 0.41.3 → 0.43.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
@@ -91,7 +91,7 @@ Pick **Live (next request)** or any retained step, and browse what that request
91
91
 
92
92
  ![A tool result expanded with Raw/Markdown toggle](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-browser-tool-result.png)
93
93
 
94
- - **Image payloads render as cards** — thumbnails with name, dimensions, stored size, and the official DeepSeek image-token estimate (dsh 0.1.1+ multimodal pipeline, e.g. `read_image` results and image attachments):
94
+ - **Image payloads render as cards** — thumbnails with name, dimensions, stored size, and the official DeepSeek image-token estimate (the dsh multimodal pipeline, e.g. `read_image` results and image attachments):
95
95
 
96
96
  ![An image payload rendered as a thumbnail card](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-browser-images.png)
97
97
 
@@ -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.1-rc2+** and **0.1.2-alpha2+**. The per-release matrix and how it is verified: [docs/compatibility.md](docs/compatibility.md).
143
+ - **Compatibility** — works on `@deepseek-ai/dsh` **0.1.2-rc1+**. The per-release matrix and how it is verified: [docs/compatibility.md](docs/compatibility.md).
144
144
  - **I18n** — UI in English and 简体中文.
145
145
 
146
146
  ## Like it?
package/lib/client.js CHANGED
@@ -88,6 +88,7 @@ window.__ModuleLoader__.load({
88
88
  "brief.noInputs": "(无新增)",
89
89
  "brief.locate": "在上下文浏览器中查看",
90
90
  "jump.title": "在上下文标签页中查看此轮",
91
+ "jump.statsLine": "在上下文标签页中查看上下文构成",
91
92
  "brief.turnTip": "这一轮开场时用户发送的消息;回合内的每一步都会回显同一条",
92
93
  "brief.inputTip": "上一步回复之后、本次请求发出前新进入上下文的内容——通常是上一步所调工具的结果;每轮的第一步无新增",
93
94
  "brief.replyTip": "这一步模型返回的内容(文本回复或工具调用);此步发起的调用,其结果会出现在下一柱的「输入」行",
@@ -189,6 +190,12 @@ window.__ModuleLoader__.load({
189
190
  "loading": "正在读取会话日志…",
190
191
  "error": "上下文数据读取失败:",
191
192
  "error.retry": "重试",
193
+ "gate.title": "请更新 DeepSeek Harness 版本",
194
+ "gate.body": "dsh-context 插件支持 DeepSeek Harness v{minimum} 或更新版本。",
195
+ "gate.current": "当前版本",
196
+ "gate.minimum": "最低要求",
197
+ "gate.orNewer": "及更新的版本",
198
+ "gate.ok": "知道了",
192
199
  "footer": "估算口径:与 dsh 内置 tokenMeter 相同的固定密度启发式(约 4 字符 ≈ 1 token);「实际」为供应商上报用量。",
193
200
  "tip.step": "第 {t} 轮 · 第{s}步",
194
201
  "tip.turn": "第 {t} 轮 · 共 {n} 步",
@@ -364,6 +371,7 @@ window.__ModuleLoader__.load({
364
371
  "brief.noInputs": "(nothing new)",
365
372
  "brief.locate": "Reveal in Context Browser",
366
373
  "jump.title": "View this turn in the Context tab",
374
+ "jump.statsLine": "View the context composition in the Context tab",
367
375
  "brief.turnTip": "The user message that opened this turn — every step of the turn recalls it",
368
376
  "brief.inputTip": "What entered after the previous reply and before this request went out — usually the previous tool-call results; a turn opener has nothing new",
369
377
  "brief.replyTip": "What the model returned on this step (a text reply or tool calls) — results of calls made here appear in the In row of the next bar",
@@ -465,6 +473,12 @@ window.__ModuleLoader__.load({
465
473
  "loading": "Reading the session log…",
466
474
  "error": "Failed to read context data: ",
467
475
  "error.retry": "Retry",
476
+ "gate.title": "Update DeepSeek Harness",
477
+ "gate.body": "dsh-context supports DeepSeek Harness v{minimum} or newer.",
478
+ "gate.current": "Current",
479
+ "gate.minimum": "Minimum required",
480
+ "gate.orNewer": "or newer",
481
+ "gate.ok": "Got it",
468
482
  "footer": "Estimate: same fixed-density heuristic as dsh’s built-in tokenMeter (~4 chars ≈ 1 token); “actual” is provider-reported usage.",
469
483
  "tip.step": "Turn {t} · Step {s}",
470
484
  "tip.turn": "Turn {t} · {n} steps",
@@ -814,55 +828,36 @@ window.__ModuleLoader__.load({
814
828
  //#endregion
815
829
  //#region src/client/services.ts
816
830
  /**
817
- * A session-authorized durable-image loader over whichever conversation
818
- * face the running harness provides (`resolveImage` pre-0.1.2, `imageUrl`
819
- * since), or undefined when neither service is composed the caller
820
- * degrades to metadata-only cards. Hostile snapshots and throwing service
821
- * reads are caught: this helper can never take a render down.
831
+ * A session-authorized durable-image loader over the harness conversation
832
+ * face (`uiConversation.imageUrl`), or undefined when the service is not
833
+ * composed the caller degrades to metadata-only cards. Hostile snapshots
834
+ * and throwing service reads are caught: this helper can never take a
835
+ * render down.
822
836
  */
823
837
  function imageLoaderOf(ctx, sessionId) {
824
838
  if (typeof sessionId !== "string" || sessionId === "") return void 0;
825
839
  try {
826
- const legacy = ctx.get("conversation");
827
- if (legacy !== void 0 && typeof legacy.resolveImage === "function") {
828
- const resolveImage = legacy.resolveImage.bind(legacy);
829
- return (attachment) => resolveImage(sessionId, attachment);
830
- }
831
- const modern = ctx.get("uiConversation");
832
- if (modern !== void 0 && typeof modern.imageUrl === "function") {
833
- const imageUrl = modern.imageUrl.bind(modern);
840
+ const conversation = ctx.get("uiConversation");
841
+ if (conversation !== void 0 && typeof conversation.imageUrl === "function") {
842
+ const imageUrl = conversation.imageUrl.bind(conversation);
834
843
  return (attachment) => imageUrl(sessionId, attachment);
835
844
  }
836
845
  } catch {}
837
846
  }
838
847
  /**
839
- * The conversation-window nodes this plugin joins on, from whichever seat
840
- * the running harness provides — `useChat` (`ChatSnapshot.legacy.nodes`) on
841
- * dsh 0.1.2+, the session snapshot's own `nodes` before that. Returns
842
- * undefined when neither seat delivers a real array (absent seat, older or
843
- * foreign harness, hostile snapshot) — callers render without the join,
844
- * never an error.
845
- *
846
- * Hook-order contract: the seats are real React hooks, so BOTH are invoked
847
- * on every call whenever present (the chat seat first, the session seat
848
- * second) and only the RESULT is picked conditionally — a stable call order
849
- * across renders. Selectors return stable slice references so the
850
- * framework's snapshot equality can gate re-renders.
848
+ * The conversation-window nodes this plugin joins on, from the `useChat`
849
+ * seat (`ChatSnapshot.legacy.nodes`). Returns undefined when the seat does
850
+ * not deliver a real array (absent seat, foreign harness, hostile snapshot)
851
+ * callers render without the join, never an error.
851
852
  */
852
853
  function conversationNodesOf(props) {
853
854
  const useChat = props.useChat;
854
- let chatNodes;
855
- if (typeof useChat === "function") try {
855
+ if (typeof useChat !== "function") return void 0;
856
+ try {
856
857
  const slice = useChat((s) => s !== null && typeof s === "object" ? s.legacy : void 0);
857
- chatNodes = slice !== null && typeof slice === "object" ? slice.nodes : void 0;
858
+ const nodes = slice !== null && typeof slice === "object" ? slice.nodes : void 0;
859
+ return Array.isArray(nodes) ? nodes : void 0;
858
860
  } catch {}
859
- const useSession = props.useSession;
860
- let sessionNodes;
861
- if (typeof useSession === "function") try {
862
- sessionNodes = useSession((s) => s.nodes);
863
- } catch {}
864
- if (Array.isArray(chatNodes)) return chatNodes;
865
- if (Array.isArray(sessionNodes)) return sessionNodes;
866
861
  }
867
862
  /**
868
863
  * Narrow an unknown projection value to a string-keyed record, or null when
@@ -886,6 +881,20 @@ window.__ModuleLoader__.load({
886
881
  return value.filter((v) => v !== null && typeof v === "object");
887
882
  }
888
883
  /**
884
+ * Narrow a delivered `unsupported` gate record (the host's baseline gate —
885
+ * see host/fallback.ts): both version strings re-proved, anything else
886
+ * degrades to null (no gate shown) instead of rendering garbage.
887
+ */
888
+ function unsupportedOf(value) {
889
+ const data = asRecord$1(value);
890
+ if (data === null) return null;
891
+ if (typeof data.current !== "string" || typeof data.minimum !== "string") return null;
892
+ return {
893
+ current: data.current,
894
+ minimum: data.minimum
895
+ };
896
+ }
897
+ /**
889
898
  * Narrow a delivered projection value to a RENDER-SAFE context timeline —
890
899
  * the client's no-white-screen guarantee against backend/parse failures.
891
900
  *
@@ -914,8 +923,10 @@ window.__ModuleLoader__.load({
914
923
  const safeCurrent = current !== null && typeof current === "object" ? current : {};
915
924
  const cost = typeof data.cost === "object" && data.cost !== null && !Array.isArray(data.cost) ? data.cost : void 0;
916
925
  const timing = timingOf(data.timing);
926
+ const unsupported = unsupportedOf(data.unsupported);
917
927
  return {
918
928
  ok: true,
929
+ ...unsupported !== null ? { unsupported } : {},
919
930
  ...typeof data.model === "string" ? { model: data.model } : {},
920
931
  ...typeof data.provider === "string" ? { provider: data.provider } : {},
921
932
  ...typeof data.contextWindow === "number" ? { contextWindow: data.contextWindow } : {},
@@ -1263,109 +1274,74 @@ window.__ModuleLoader__.load({
1263
1274
  }
1264
1275
  return nodes;
1265
1276
  }
1266
- /** Read a service off the client context without letting an absent/foreign
1267
- * cordis service (a throwing `ctx.get`) escape: returns undefined instead. */
1268
- function serviceOf(ctx, name) {
1277
+ /** The `page` verb of a history face, re-proved and bound to its owner.
1278
+ * Hostile objects (any accessor backed by host state can throw) degrade to
1279
+ * undefined instead of escaping the read. */
1280
+ function readPageOf(face) {
1281
+ if (face === null || typeof face !== "object") return void 0;
1269
1282
  try {
1270
- return ctx.get(name);
1283
+ const fn = face.page;
1284
+ return typeof fn === "function" ? fn.bind(face) : void 0;
1271
1285
  } catch {
1272
1286
  return;
1273
1287
  }
1274
1288
  }
1275
1289
  /**
1276
- * Walk a (possibly traced/proxied) service value down a plain-property path,
1277
- * degrading at the FIRST throw. cordis's undeclared-service proxies are one
1278
- * hostile object class — any accessor backed by host state can throw too —
1279
- * and the no-white-screen contract needs the whole chain guarded, not just
1280
- * the `ctx.get` call.
1281
- */
1282
- function readKeysOf(value, ...keys) {
1283
- let current = value;
1284
- for (const key of keys) {
1285
- if (current === null || typeof current !== "object") return void 0;
1286
- try {
1287
- current = current[key];
1288
- } catch {
1289
- return;
1290
- }
1291
- }
1292
- return current;
1293
- }
1294
- /** The `page` verb of a history face, re-proved and bound to its owner. */
1295
- function readPageOf(face) {
1296
- const fn = readKeysOf(face, "page");
1297
- return typeof fn === "function" ? fn.bind(face) : void 0;
1298
- }
1299
- /** The `history` verb of the legacy api client face, re-proved and bound. */
1300
- function readHistoryOf(face) {
1301
- const sessions = readKeysOf(face, "api", "sessions");
1302
- const fn = readKeysOf(sessions, "history");
1303
- return typeof fn === "function" ? fn.bind(sessions) : void 0;
1304
- }
1305
- /**
1306
- * The 0.1.2+ gateway history page verb, resolved through the DECLARED inject
1290
+ * The gateway history page verb, resolved through the DECLARED inject
1307
1291
  * (see {@link watchHistoryFaces}) and bound up front: a method extracted
1308
1292
  * unbound loses `this`, and the traced `remote` proxy that hands it out
1309
1293
  * requires the inject to resolve at all.
1310
1294
  */
1311
1295
  let declaredPage;
1312
1296
  /**
1313
- * Register the plugin's 0.1.2+ history faces with the harness through the
1314
- * DECLARED inject — both `remote` AND `remote.session` (the ui-chat idiom)
1315
- * must be in one fiber's requirement list, because the traced `remote`
1316
- * proxy resolves `.session` through the context and each name needs the
1317
- * other's declaration. Pre-0.1.2 hosts never provide either name, so the
1318
- * callback simply never fires and the legacy `connection` face carries the
1319
- * reads. The callback re-runs on every unload/remount, so it owns the
1320
- * slot's lifetime. The face itself is re-proven (a never-fired or hostile
1321
- * invocation leaves the slot unset — nothing here can throw).
1297
+ * Register the plugin's history face with the harness through the DECLARED
1298
+ * inject — both `remote` AND `remote.session` (the ui-chat idiom) must be in
1299
+ * one fiber's requirement list, because the traced `remote` proxy resolves
1300
+ * `.session` through the context and each name needs the other's
1301
+ * declaration; an undeclared read of the traced proxy throws instead of
1302
+ * resolving. The callback re-runs on every unload/remount, so it owns the
1303
+ * slot's lifetime. The face itself is re-proven: a never-fired invocation
1304
+ * or a hostile property read leaves the slot unset nothing here can throw.
1322
1305
  */
1323
1306
  function watchHistoryFaces(ctx) {
1324
1307
  ctx.inject(["remote", "remote.session"], (c) => {
1325
- const session = c.remote?.session;
1326
- declaredPage = session !== void 0 ? readPageOf(session) : void 0;
1308
+ try {
1309
+ const session = c.remote?.session;
1310
+ declaredPage = session !== void 0 ? readPageOf(session) : void 0;
1311
+ } catch {
1312
+ declaredPage = void 0;
1313
+ }
1327
1314
  return () => {
1328
1315
  declaredPage = void 0;
1329
1316
  };
1330
1317
  });
1331
1318
  }
1332
- /** The rows array of a history/page response, under every served envelope. */
1319
+ /** The rows array of a successful history page, under the served envelope. */
1333
1320
  function rowsOf(response) {
1334
1321
  let payload = response;
1335
- if (payload !== null && typeof payload === "object") {
1336
- const nested = payload.result;
1337
- if (nested !== null && typeof nested === "object") payload = nested;
1338
- }
1339
1322
  if (payload !== null && typeof payload === "object" && "ok" in payload) {
1340
1323
  const r = payload;
1341
1324
  if (r.ok !== true || r.value === null || typeof r.value !== "object") throw new Error("history rpc failed");
1342
1325
  payload = r.value;
1343
1326
  }
1344
1327
  if (payload === null || typeof payload !== "object") throw new Error("history rpc failed");
1345
- const rows = payload.records ?? payload.events;
1328
+ const rows = payload.records;
1346
1329
  if (!Array.isArray(rows)) throw new Error("history rpc failed");
1347
1330
  return rows;
1348
1331
  }
1349
1332
  /**
1350
- * The session's history readers over whichever face the running harness
1351
- * serves the 0.1.2+ gateway remotes (`remote.session.page`) first, the
1352
- * pre-0.1.2 api client verb (`connection.api.sessions.history`) beneath it.
1353
- * Both cut message-aligned pages, so the returned read covers `seq` whenever
1354
- * the durable log still holds it: the newer face pins the inclusive cut to
1355
- * the seq itself, the legacy reader uses the exclusive bound one past it —
1356
- * and because a non-declared read of the traced remote proxy throws
1357
- * ("cannot get property … without inject"), the page face prefers the
1358
- * injection-resolved slot and only then tries the reflect read. Every
1359
- * service property access degrades at its own guard instead of taking the
1360
- * view down. Undefined when no face exists (older hosts) — callers keep
1361
- * their static degradation.
1333
+ * The session's history reader over the gateway remotes (`remote.session.page`),
1334
+ * from the declared-inject slot. The page cuts message-aligned pages, so the
1335
+ * returned read covers `seq` whenever the durable log still holds it: the
1336
+ * inclusive cut is pinned to the seq itself, the exclusive bound one past
1337
+ * it. Undefined when the slot holds no face (the inject never fired or
1338
+ * served no usable page verb) callers keep their static degradation.
1362
1339
  */
1363
- function pageReadersOf(ctx, sessionId) {
1364
- const page = declaredPage ?? readPageOf(serviceOf(ctx, "remote.session"));
1365
- const history = readHistoryOf(serviceOf(ctx, "connection"));
1366
- if (page === void 0 && history === void 0) return void 0;
1340
+ function pageReaderOf(sessionId) {
1341
+ const page = declaredPage;
1342
+ if (page === void 0) return void 0;
1367
1343
  return (seq) => {
1368
- if (page !== void 0) return page({
1344
+ return page({
1369
1345
  address: {
1370
1346
  kind: "session",
1371
1347
  sessionId
@@ -1373,24 +1349,18 @@ window.__ModuleLoader__.load({
1373
1349
  throughSeq: seq,
1374
1350
  beforeSeq: seq + 1
1375
1351
  }, new AbortController().signal);
1376
- return history({
1377
- sessionId,
1378
- beforeSeq: seq + 1
1379
- });
1380
1352
  };
1381
1353
  }
1382
1354
  /**
1383
- * Build the browser's per-session fetcher over whichever history face the
1384
- * running harness serves — the 0.1.2+ gateway remotes (`remote.session.page`)
1385
- * first, the pre-0.1.2 api client verb (`connection.api.sessions.history`)
1386
- * beneath it. Both cut message-aligned pages, so `beforeSeq: seq + 1` (with
1387
- * the inclusive cut pinned to `seq` on the newer face) covers the seq whenever
1388
- * the durable log still holds it. Undefined when no face exists (older
1389
- * hosts) — the caller keeps its static preview-plus-hint degradation. Found
1390
- * nodes cache in the closure: one mount re-reading a row never re-fetches.
1355
+ * Build the browser's per-session fetcher over the gateway history face
1356
+ * (`remote.session.page`): message-aligned pages whose inclusive cut pinned
1357
+ * to `seq` (exclusive bound one past it) cover the seq whenever the durable
1358
+ * log still holds it. Undefined when no face was resolved at build time
1359
+ * the caller keeps its static preview-plus-hint degradation. Found nodes
1360
+ * cache in the closure: one mount re-reading a row never re-fetches.
1391
1361
  */
1392
- function makeContentFetcher(ctx, sessionId) {
1393
- const read = pageReadersOf(ctx, sessionId);
1362
+ function makeContentFetcher(sessionId) {
1363
+ const read = pageReaderOf(sessionId);
1394
1364
  if (read === void 0) return void 0;
1395
1365
  const cache = /* @__PURE__ */ new Map();
1396
1366
  return async (seq) => {
@@ -1434,11 +1404,11 @@ window.__ModuleLoader__.load({
1434
1404
  * mapped client-side into the renderable content. Epochs cache per session
1435
1405
  * (history is immutable), and OLDER epochs sharing the page cache for free —
1436
1406
  * stepping back through epochs walks the same pages. Undefined when no
1437
- * history face exists (older hosts) — the browser keeps a metadata-only
1438
- * degradation instead.
1407
+ * history face exists — the browser keeps a metadata-only degradation
1408
+ * instead.
1439
1409
  */
1440
- function makeHeaderFetcher(ctx, sessionId) {
1441
- const read = pageReadersOf(ctx, sessionId);
1410
+ function makeHeaderFetcher(sessionId) {
1411
+ const read = pageReaderOf(sessionId);
1442
1412
  if (read === void 0) return void 0;
1443
1413
  const cache = /* @__PURE__ */ new Map();
1444
1414
  return async (seq) => {
@@ -1760,7 +1730,7 @@ window.__ModuleLoader__.load({
1760
1730
  //#region src/client/components/images.tsx
1761
1731
  /**
1762
1732
  * Narrow an unknown content block to a durable image ref: accepts both raw message blocks (`{ type: 'image', attachment }`) and the
1763
- * snapshot's assistant blocks (`{ kind: 'image', attachment }`); everything else null. Lenient on the optional facts — resolveImage reads
1733
+ * snapshot's assistant blocks (`{ kind: 'image', attachment }`); everything else null. Lenient on the optional facts — imageUrl reads
1764
1734
  * only attachmentId.
1765
1735
  */
1766
1736
  function imageRefOf(block) {
@@ -2913,8 +2883,8 @@ window.__ModuleLoader__.load({
2913
2883
  const [hoverCat, setHoverCat] = React.useState(null);
2914
2884
  const [dockLeft, setDockLeft] = React.useState(0);
2915
2885
  const backdropRef = React.useRef(null);
2916
- const fetchContent = React.useMemo(() => sessionId !== "" ? makeContentFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
2917
- const fetchHeader = React.useMemo(() => sessionId !== "" ? makeHeaderFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
2886
+ const fetchContent = React.useMemo(() => sessionId !== "" ? makeContentFetcher(sessionId) : void 0, [sessionId]);
2887
+ const fetchHeader = React.useMemo(() => sessionId !== "" ? makeHeaderFetcher(sessionId) : void 0, [sessionId]);
2918
2888
  const close = React.useCallback(() => {
2919
2889
  if (sessionId === "") return;
2920
2890
  modalStoreOf(sessionId).set(false);
@@ -5122,7 +5092,7 @@ window.__ModuleLoader__.load({
5122
5092
  return function PluginInfo() {
5123
5093
  const [latest, setLatest] = React.useState(null);
5124
5094
  React.useEffect(() => {
5125
- if ("0.41.3".includes("-dev")) return;
5095
+ if ("0.43.0".includes("-dev")) return;
5126
5096
  let on = true;
5127
5097
  fetchLatestVersion().then((v) => {
5128
5098
  if (on && v) setLatest(v);
@@ -5131,8 +5101,8 @@ window.__ModuleLoader__.load({
5131
5101
  on = false;
5132
5102
  };
5133
5103
  }, []);
5134
- const update = latest !== null && isNewerVersion(latest, "0.41.3") ? latest : null;
5135
- const nameText = "dsh-context (v0.41.3)";
5104
+ const update = latest !== null && isNewerVersion(latest, "0.43.0") ? latest : null;
5105
+ const nameText = "dsh-context (v0.43.0)";
5136
5106
  const nameValue = [nameText];
5137
5107
  if (update) nameValue.push(/* @__PURE__ */ React.createElement("span", {
5138
5108
  key: "update",
@@ -5156,6 +5126,53 @@ window.__ModuleLoader__.load({
5156
5126
  };
5157
5127
  }
5158
5128
  //#endregion
5129
+ //#region src/client/components/upgradeGate.tsx
5130
+ /** Sessions whose gate the user already dismissed this browser session. */
5131
+ const dismissed = /* @__PURE__ */ new Set();
5132
+ function makeUpgradeGate(kit) {
5133
+ const { t } = kit;
5134
+ return function UpgradeGate(props) {
5135
+ const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
5136
+ const [closedFor, setClosedFor] = React.useState(null);
5137
+ const closed = closedFor === sessionId || dismissed.has(sessionId);
5138
+ const close = React.useCallback(() => {
5139
+ dismissed.add(sessionId);
5140
+ setClosedFor(sessionId);
5141
+ }, [sessionId]);
5142
+ React.useEffect(() => {
5143
+ if (closed) return void 0;
5144
+ const onKey = (ev) => {
5145
+ if (ev.key !== "Escape") return;
5146
+ ev.preventDefault();
5147
+ ev.stopPropagation();
5148
+ close();
5149
+ };
5150
+ window.addEventListener("keydown", onKey, true);
5151
+ return () => {
5152
+ window.removeEventListener("keydown", onKey, true);
5153
+ };
5154
+ }, [closed, close]);
5155
+ if (closed) return null;
5156
+ return /* @__PURE__ */ React.createElement("div", {
5157
+ className: "lc-modal-backdrop",
5158
+ onClick: close
5159
+ }, /* @__PURE__ */ React.createElement("div", {
5160
+ className: "lc-modal-card lc-gate-card",
5161
+ onClick: (ev) => {
5162
+ ev.stopPropagation();
5163
+ }
5164
+ }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-head" }, /* @__PURE__ */ React.createElement("span", { className: "lc-modal-title" }, t("gate.title")), /* @__PURE__ */ React.createElement("button", {
5165
+ className: "lc-modal-close",
5166
+ "aria-label": t("cmd.close"),
5167
+ onClick: close
5168
+ }, "×")), /* @__PURE__ */ React.createElement("div", { className: "lc-gate-body" }, t("gate.body", { minimum: props.minimum })), /* @__PURE__ */ React.createElement("div", { className: "lc-gate-versions" }, /* @__PURE__ */ React.createElement("span", { className: "lc-gate-version" }, /* @__PURE__ */ React.createElement("span", { className: "lc-gate-version-label" }, t("gate.current")), /* @__PURE__ */ React.createElement("span", { className: "lc-gate-version-value" }, "v", props.current)), /* @__PURE__ */ React.createElement("span", { className: "lc-gate-version" }, /* @__PURE__ */ React.createElement("span", { className: "lc-gate-version-label" }, t("gate.minimum")), /* @__PURE__ */ React.createElement("span", { className: "lc-gate-version-value" }, "v", props.minimum, /* @__PURE__ */ React.createElement("span", { className: "lc-gate-or-newer" }, t("gate.orNewer"))))), /* @__PURE__ */ React.createElement("div", { className: "lc-gate-actions" }, /* @__PURE__ */ React.createElement("button", {
5169
+ type: "button",
5170
+ className: "lc-gate-ok",
5171
+ onClick: close
5172
+ }, t("gate.ok")))));
5173
+ };
5174
+ }
5175
+ //#endregion
5159
5176
  //#region src/client/components/requestDetail.tsx
5160
5177
  function makeRequestDetail(kit, StackedBar) {
5161
5178
  const { t, fmt, fmtTime, catLabel, eventLabel, eventAt } = kit;
@@ -6128,6 +6145,7 @@ window.__ModuleLoader__.load({
6128
6145
  const StatsTiming = makeStatsTiming(kit, Donut);
6129
6146
  const StatsTokens = makeStatsTokens(kit, Donut);
6130
6147
  const PluginInfo = makePluginInfo(kit);
6148
+ const UpgradeGate = makeUpgradeGate(kit);
6131
6149
  const ContextBrowser = makeContextBrowser(kit, StackedBar);
6132
6150
  const AgentGraph = makeAgentGraph(ctx, kit);
6133
6151
  const ErrorBoundary = makeErrorBoundary(t);
@@ -6159,8 +6177,8 @@ window.__ModuleLoader__.load({
6159
6177
  setNodeFocus(null);
6160
6178
  }, []);
6161
6179
  const loadImage = React.useMemo(() => imageLoaderOf(ctx, typeof sessionId === "string" ? sessionId : void 0), [ctx, sessionId]);
6162
- const fetchContent = React.useMemo(() => typeof sessionId === "string" && sessionId !== "" ? makeContentFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
6163
- const fetchHeader = React.useMemo(() => typeof sessionId === "string" && sessionId !== "" ? makeHeaderFetcher(ctx, sessionId) : void 0, [ctx, sessionId]);
6180
+ const fetchContent = React.useMemo(() => typeof sessionId === "string" && sessionId !== "" ? makeContentFetcher(sessionId) : void 0, [sessionId]);
6181
+ const fetchHeader = React.useMemo(() => typeof sessionId === "string" && sessionId !== "" ? makeHeaderFetcher(sessionId) : void 0, [sessionId]);
6164
6182
  const rootRef = React.useRef(null);
6165
6183
  const scrollerRef = React.useRef(null);
6166
6184
  const restoredRef = React.useRef(null);
@@ -6302,6 +6320,7 @@ window.__ModuleLoader__.load({
6302
6320
  const localeSvc = ctx.get("locale");
6303
6321
  const activeLocale = localeSvc !== void 0 && typeof localeSvc.getLocale === "function" ? localeSvc.getLocale().active : "en";
6304
6322
  const subtitle = (data.model ?? "") + (data.provider ? " · " + data.provider : "");
6323
+ const gate = unsupportedOf(data.unsupported);
6305
6324
  return /* @__PURE__ */ React.createElement("div", {
6306
6325
  className: "lc-root",
6307
6326
  ref: rootRef
@@ -6407,7 +6426,11 @@ window.__ModuleLoader__.load({
6407
6426
  billed: usage !== null ? numOf(usage.uncachedInputTokens) + numOf(usage.outputTokens) + numOf(usage.cacheReadTokens) + numOf(usage.cacheWriteTokens) : null,
6408
6427
  requests: requests.length
6409
6428
  }
6410
- }), /* @__PURE__ */ React.createElement("div", { className: "lc-foot" }, t("footer")));
6429
+ }), /* @__PURE__ */ React.createElement("div", { className: "lc-foot" }, t("footer")), gate !== null && /* @__PURE__ */ React.createElement(UpgradeGate, {
6430
+ sessionId,
6431
+ current: gate.current,
6432
+ minimum: gate.minimum
6433
+ }));
6411
6434
  }
6412
6435
  return function ContextView(props) {
6413
6436
  return h(ErrorBoundary, null, h(ContextViewBody, props));
@@ -6419,9 +6442,8 @@ window.__ModuleLoader__.load({
6419
6442
  * The assistant-message action that jumps to the Context tab at this reply's
6420
6443
  * turn. Registered on the harness `conversation.chat.assistant-actions` seat
6421
6444
  * (the icon row beside copy/branch), it receives the finalized reply's durable
6422
- * message id, resolves the matching assistant node's seq off whichever node
6423
- * seat the running harness serves (`useChat` on 0.1.2+, the session snapshot
6424
- * before it), records it in the viewFocus relay, and activates the Context
6445
+ * message id, resolves the matching assistant node's seq off the `useChat`
6446
+ * node seat, records it in the viewFocus relay, and activates the Context
6425
6447
  * tab — where the jump pins the reply's TURN (see contextView's leg 2). An
6426
6448
  * unresolvable seq still switches tabs, just without a pin; a message id that
6427
6449
  * is not a plain string renders nothing at all.
@@ -6493,6 +6515,107 @@ window.__ModuleLoader__.load({
6493
6515
  };
6494
6516
  }
6495
6517
  //#endregion
6518
+ //#region src/client/components/statsJump.tsx
6519
+ /** The plugin's invisible dock anchor, and the class it brands the stats row with. */
6520
+ const ANCHOR_CLASS = "lc-stats-jump";
6521
+ const ROW_CLASS = "lc-stats-jump-row";
6522
+ /**
6523
+ * The stats row beside `anchor`: the single element child that is neither
6524
+ * the anchor nor a tooltip bubble. Children are untrusted input — one that
6525
+ * throws on attribute access is skipped whole, and two live candidates at
6526
+ * once (another plugin's dock entry, a crash face) answer null so nothing
6527
+ * is ever decorated on a guess.
6528
+ */
6529
+ function statsRowOf(anchor) {
6530
+ const parent = anchor.parentElement;
6531
+ if (parent === null) return null;
6532
+ let row = null;
6533
+ for (const child of parent.children) {
6534
+ try {
6535
+ if (child === anchor || child.getAttribute("role") === "tooltip") continue;
6536
+ } catch {
6537
+ continue;
6538
+ }
6539
+ if (row !== null) return null;
6540
+ row = child;
6541
+ }
6542
+ return row;
6543
+ }
6544
+ /**
6545
+ * Wire the dock beside `anchor`: decorate today's stats row and re-spot it
6546
+ * on dock churn. Returns the teardown that disconnects the observer and
6547
+ * strips every mark the wiring left on the row.
6548
+ */
6549
+ function wireDock(anchor, label, activate) {
6550
+ if (anchor === null) return () => {};
6551
+ let wired = null;
6552
+ const unwire = () => {
6553
+ wired?.off();
6554
+ wired = null;
6555
+ };
6556
+ const scan = () => {
6557
+ try {
6558
+ const row = statsRowOf(anchor);
6559
+ if (wired?.row === row) return;
6560
+ unwire();
6561
+ if (row === null || !(row instanceof HTMLElement)) return;
6562
+ row.classList.add(ROW_CLASS);
6563
+ row.setAttribute("role", "button");
6564
+ row.setAttribute("tabindex", "0");
6565
+ row.setAttribute("aria-label", label);
6566
+ const onKey = (e) => {
6567
+ if (e.key !== "Enter" && e.key !== " ") return;
6568
+ e.preventDefault();
6569
+ activate();
6570
+ };
6571
+ const onClick = () => {
6572
+ const selection = anchor.ownerDocument.getSelection();
6573
+ if (selection !== null && !selection.isCollapsed) return;
6574
+ activate();
6575
+ };
6576
+ row.addEventListener("click", onClick);
6577
+ row.addEventListener("keydown", onKey);
6578
+ wired = {
6579
+ row,
6580
+ off() {
6581
+ row.removeEventListener("click", onClick);
6582
+ row.removeEventListener("keydown", onKey);
6583
+ row.classList.remove(ROW_CLASS);
6584
+ row.removeAttribute("role");
6585
+ row.removeAttribute("tabindex");
6586
+ row.removeAttribute("aria-label");
6587
+ }
6588
+ };
6589
+ } catch {}
6590
+ };
6591
+ scan();
6592
+ const parent = anchor.parentElement;
6593
+ if (parent === null) return unwire;
6594
+ const observer = new MutationObserver(scan);
6595
+ observer.observe(parent, { childList: true });
6596
+ return () => {
6597
+ observer.disconnect();
6598
+ unwire();
6599
+ };
6600
+ }
6601
+ /** The dock entry component: the invisible anchor plus the wiring around it. */
6602
+ function makeStatsJump(kit) {
6603
+ const { t } = kit;
6604
+ return function StatsJump() {
6605
+ const anchorRef = React.useRef(null);
6606
+ const label = t("jump.statsLine");
6607
+ React.useEffect(() => wireDock(anchorRef.current, label, () => {
6608
+ activateContextTab(t("tab"));
6609
+ }), [label]);
6610
+ return h("span", {
6611
+ ref: anchorRef,
6612
+ className: ANCHOR_CLASS,
6613
+ hidden: true,
6614
+ "aria-hidden": "true"
6615
+ });
6616
+ };
6617
+ }
6618
+ //#endregion
6496
6619
  //#region src/client/viewkit.ts
6497
6620
  function makeViewKit(t) {
6498
6621
  const { eventLabel, eventAt } = makeEventText(t);
@@ -6509,7 +6632,7 @@ window.__ModuleLoader__.load({
6509
6632
  }
6510
6633
  //#endregion
6511
6634
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/base.css.mjs
6512
- const css$13 = ".lc-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}.lc-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:14px;padding:14px 16px}.lc-root .lc-card{margin-bottom:7px;padding:7px 8px}.lc-root .lc-cols{gap:7px;margin-bottom:7px}.lc-card-title{flex-wrap:wrap;align-items:baseline;gap:8px;margin-bottom:10px;font-weight:600;display:flex}.lc-card-title-text{white-space:nowrap;flex:none}.lc-gran,.lc-kinds{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;gap:2px;margin-left:auto;padding:1px;display:flex}.lc-trend-ctl{align-items:center;gap:6px;margin-left:auto;display:flex}.lc-trend-ctl .lc-gran{margin-left:0}.lc-gran-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-gran-btn:hover{color:var(--dsw-alias-label-primary)}.lc-gran-on,.lc-gran-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-tip{z-index:6;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:max-content;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3,0 2px 8px #0000002e);pointer-events:none;opacity:0;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:6px 10px;font-size:12px;position:absolute}.lc-events,.lc-fa-list{flex-direction:column;gap:2px;height:320px;display:flex;overflow-y:auto}.lc-cols{flex-wrap:wrap;gap:14px;margin-bottom:14px;display:flex}.lc-col{flex:1;min-width:280px}.lc-cols>.lc-card,.lc-col>.lc-card:last-child{margin-bottom:0}.lc-col-browser{flex-direction:column;display:flex}.lc-col-browser>.lc-card{flex:1}.lc-head>.lc-card{flex:1 1 0;min-width:0;container:lc-head-card/inline-size}.lc-head>.lc-card:first-child{flex-direction:column;display:flex}.lc-head>.lc-card:first-child .lc-stats{flex:1;align-content:stretch}.lc-head>.lc-card:first-child .lc-stat{justify-content:center}.lc-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:18px 0}.lc-error{flex-direction:column;align-items:center;gap:8px;padding:40px 16px;display:flex}.lc-error-msg{font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow-wrap:anywhere;border-radius:6px;max-width:100%;padding:4px 8px;font-size:12px}.lc-error-retry{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:4px 14px;font-size:12px}.lc-error-retry:hover{border-color:var(--dsw-alias-label-primary)}.lc-foot{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:12px}[data-conversation-scroll]:has(.lc-root)>[data-composer-seat]:not(:has([data-approval-key],[data-question-key],[data-plan-review-key])){display:none}";
6635
+ const css$13 = ".lc-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}.lc-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:14px;padding:14px 16px}.lc-root .lc-card{margin-bottom:7px;padding:7px 8px;container:lc-card/inline-size}.lc-root .lc-cols{gap:7px;margin-bottom:7px}.lc-card-title{flex-wrap:wrap;align-items:baseline;gap:8px;margin-bottom:10px;font-weight:600;display:flex}.lc-card-title-text{white-space:nowrap;flex:none}.lc-gran,.lc-kinds{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;gap:2px;margin-left:auto;padding:1px;display:flex}.lc-trend-ctl{align-items:center;gap:6px;margin-left:auto;display:flex}.lc-trend-ctl .lc-gran{margin-left:0}.lc-gran-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-gran-btn:hover{color:var(--dsw-alias-label-primary)}.lc-gran-on,.lc-gran-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-tip{z-index:6;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:max-content;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3,0 2px 8px #0000002e);pointer-events:none;opacity:0;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:6px 10px;font-size:12px;position:absolute}.lc-events,.lc-fa-list{flex-direction:column;gap:2px;height:320px;display:flex;overflow-y:auto}.lc-cols{flex-wrap:wrap;gap:14px;margin-bottom:14px;display:flex}.lc-col{flex:1;min-width:min(280px,100%)}.lc-cols>.lc-card,.lc-col>.lc-card:last-child{margin-bottom:0}.lc-col-browser{flex-direction:column;display:flex}.lc-col-browser>.lc-card{flex:1}.lc-head{container:lc-head-row/inline-size}.lc-head>.lc-card{flex:1 1 0;min-width:0}@container lc-head-row (width<=1045px){.lc-head>.lc-card{flex-basis:40%}}@container lc-head-row (width<=480px){.lc-head>.lc-card{flex-basis:100%}}.lc-head>.lc-card:first-child{flex-direction:column;display:flex}.lc-head>.lc-card:first-child .lc-stats{flex:1;align-content:stretch}.lc-head>.lc-card:first-child .lc-stat{justify-content:center}.lc-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:18px 0}.lc-error{flex-direction:column;align-items:center;gap:8px;padding:40px 16px;display:flex}.lc-error-msg{font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow-wrap:anywhere;border-radius:6px;max-width:100%;padding:4px 8px;font-size:12px}.lc-error-retry{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:4px 14px;font-size:12px}.lc-error-retry:hover{border-color:var(--dsw-alias-label-primary)}.lc-foot{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:12px}[data-conversation-scroll]:has(.lc-root)>[data-composer-seat]:not(:has([data-approval-key],[data-question-key],[data-plan-review-key])){display:none}";
6513
6636
  const tagId$13 = "dsh-context/base.css";
6514
6637
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$13) + "]") === null) {
6515
6638
  const tag = document.createElement("style");
@@ -6520,7 +6643,7 @@ window.__ModuleLoader__.load({
6520
6643
  }
6521
6644
  //#endregion
6522
6645
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/stats.css.mjs
6523
- 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;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);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 var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out)}.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{min-width:0;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;flex-direction:column;gap:1px;margin:0 -6px;padding:1px 6px;font-size:12px;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))}}";
6646
+ 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;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);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 var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out)}.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{min-width:0;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;flex-direction:column;gap:1px;margin:0 -6px;padding:1px 6px;font-size:12px;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-card (width<=320px){.lc-donut-row{gap:8px}.lc-stats{grid-template-columns:repeat(2,minmax(0,1fr))}}@container lc-card (width<=240px){.lc-donut-row{flex-wrap:wrap}.lc-donut{margin:0 auto}.lc-donut-row .lc-sl{flex-basis:100%}}";
6524
6647
  const tagId$12 = "dsh-context/stats.css";
6525
6648
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$12) + "]") === null) {
6526
6649
  const tag = document.createElement("style");
@@ -6531,7 +6654,7 @@ window.__ModuleLoader__.load({
6531
6654
  }
6532
6655
  //#endregion
6533
6656
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/jump.css.mjs
6534
- const css$11 = ".lc-jump{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);cursor:pointer;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}.lc-jump svg{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px))}.lc-jump:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}";
6657
+ const css$11 = ".lc-jump{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);cursor:pointer;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}.lc-jump svg{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px))}.lc-jump:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.lc-stats-jump{display:none}.lc-stats-jump-row{cursor:pointer}.lc-stats-jump-row:hover,.lc-stats-jump-row:focus-visible{text-decoration:underline}";
6535
6658
  const tagId$11 = "dsh-context/jump.css";
6536
6659
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$11) + "]") === null) {
6537
6660
  const tag = document.createElement("style");
@@ -6586,7 +6709,7 @@ window.__ModuleLoader__.load({
6586
6709
  }
6587
6710
  //#endregion
6588
6711
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/events.css.mjs
6589
- const css$6 = ".lc-event{align-items:center;gap:8px;padding:3px 0;display:flex}.lc-event-icon{text-align:center;width:18px;color:var(--dsw-alias-state-warn-primary)}.lc-event-icon.lc-event-inject{color:#a855f7}.lc-event-icon.lc-event-model{color:var(--dsw-alias-brand-primary)}.lc-event-icon.lc-event-mode{color:var(--dsw-alias-label-secondary)}.lc-kind{white-space:nowrap;border-radius:4px;flex:none;padding:1px 6px;font-size:10px;font-weight:600}.lc-kind-inject{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent);color:var(--dsw-alias-state-success-primary)}.lc-kind-compaction{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent);color:var(--dsw-alias-state-error-primary)}.lc-kind-prune{color:#8b5cf6;background:#8b5cf626}.lc-kind-model{background:color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent);color:var(--dsw-alias-brand-primary)}.lc-kind-mode{background:color-mix(in srgb, var(--dsw-alias-label-secondary) 15%, transparent);color:var(--dsw-alias-label-secondary)}.lc-event-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.lc-event-at{color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none;font-size:11px}.lc-event-tokens{color:var(--dsw-alias-state-success-primary);white-space:nowrap;font-weight:600}.lc-event-tokens.lc-up{color:var(--dsw-alias-state-warn-primary)}.lc-event-time{color:var(--dsw-alias-label-secondary);font-size:12px}";
6712
+ const css$6 = ".lc-event{align-items:center;gap:8px;padding:3px 0;display:flex}.lc-event-icon{text-align:center;width:18px;color:var(--dsw-alias-state-warn-primary)}.lc-event-icon.lc-event-inject{color:#a855f7}.lc-event-icon.lc-event-model{color:var(--dsw-alias-brand-primary)}.lc-event-icon.lc-event-mode{color:var(--dsw-alias-label-secondary)}.lc-kind{white-space:nowrap;border-radius:4px;flex:none;padding:1px 6px;font-size:10px;font-weight:600}.lc-kind-inject{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent);color:var(--dsw-alias-state-success-primary)}.lc-kind-compaction{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent);color:var(--dsw-alias-state-error-primary)}.lc-kind-prune{color:#8b5cf6;background:#8b5cf626}.lc-kind-model{background:color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent);color:var(--dsw-alias-brand-primary)}.lc-kind-mode{background:color-mix(in srgb, var(--dsw-alias-label-secondary) 15%, transparent);color:var(--dsw-alias-label-secondary)}.lc-event-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.lc-event-at{color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none;font-size:11px}.lc-event-tokens{color:var(--dsw-alias-state-success-primary);white-space:nowrap;font-weight:600}.lc-event-tokens.lc-up{color:var(--dsw-alias-state-warn-primary)}.lc-event-time{color:var(--dsw-alias-label-secondary);font-size:12px}@container lc-card (width<=380px){.lc-event{flex-wrap:wrap}.lc-event-label{flex-basis:calc(100% - 92px)}}";
6590
6713
  const tagId$6 = "dsh-context/events.css";
6591
6714
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
6592
6715
  const tag = document.createElement("style");
@@ -6597,7 +6720,7 @@ window.__ModuleLoader__.load({
6597
6720
  }
6598
6721
  //#endregion
6599
6722
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/fileCard.css.mjs
6600
- const css$5 = ".lc-fa-ctl,.lc-br-toolctl{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:8px;display:flex}.lc-fa-ctl .lc-gran{margin-left:0}.lc-fa-n{opacity:.75;margin-left:4px;font-weight:700}.lc-fa-search,.lc-br-tool-search{min-width:80px;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;outline:none;flex:1;padding:3px 8px;font-size:12px}.lc-fa-search:focus,.lc-br-tool-search:focus{border-color:var(--dsw-alias-label-dimmed)}.lc-fa-meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:6px;font-size:12px;display:flex}.lc-fa-meta .lc-fa-sort{flex:none}.lc-fa-meta-delta{position:relative}.lc-fa-meta-tip{white-space:normal;max-width:260px;line-height:1.5;top:calc(100% + 6px);left:0}.lc-fa-meta-delta:hover .lc-fa-meta-tip{opacity:1}.lc-fa-item{transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border:1px solid #0000;border-radius:8px}.lc-fa-item-on{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}.lc-fa-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:7px;align-items:center;gap:7px;padding:6px 8px;font-size:12px;display:flex}.lc-fa-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-fa-form{opacity:.85;flex:none;justify-content:center;align-items:center;width:20px;height:14px;font-size:11px;display:flex}.lc-fa-lang{border-radius:4px;justify-content:center;align-items:center;min-width:16px;height:13px;padding:0 2px;font-size:8px;font-weight:700;line-height:1;display:inline-flex}.lc-fa-row .lc-br-chev{margin-right:-2px}.lc-fa-path{word-break:break-all;flex:1;min-width:0}.lc-fa-file{cursor:pointer}.lc-fa-file:hover{text-decoration:underline}.lc-fa-path em{color:var(--dsw-alias-label-secondary);font-style:normal}.lc-fa-badge{font-variant-numeric:tabular-nums;white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;padding:1px 7px;font-size:11px;font-weight:600;display:inline-flex}.lc-fa-badge i{background:currentColor;border-radius:50%;width:5px;height:5px;display:inline-block}.lc-fa-b-read{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent)}.lc-fa-b-write{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)}.lc-fa-b-search{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent)}.lc-fa-chip-read,.lc-fa-chip-read:hover{color:var(--dsw-alias-brand-primary)}.lc-fa-chip-write,.lc-fa-chip-write:hover{color:var(--dsw-alias-state-success-primary)}.lc-fa-chip-search,.lc-fa-chip-search:hover{color:var(--dsw-alias-state-warn-primary)}.lc-fa-chip-read.lc-gran-on,.lc-fa-chip-read.lc-gran-on:hover{background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent);color:var(--dsw-alias-brand-primary)}.lc-fa-chip-write.lc-gran-on,.lc-fa-chip-write.lc-gran-on:hover{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);color:var(--dsw-alias-state-success-primary)}.lc-fa-chip-search.lc-gran-on,.lc-fa-chip-search.lc-gran-on:hover{background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);color:var(--dsw-alias-state-warn-primary)}.lc-fa-delta{font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;gap:5px;font-size:11px;font-weight:600;display:inline-flex}.lc-fa-up{color:var(--dsw-alias-state-success-primary)}.lc-fa-down{color:var(--dsw-alias-state-error-primary)}.lc-fa-time{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:11px}.lc-fa-ops{border-left:2px solid var(--dsw-alias-border-l1);flex-direction:column;gap:1px;margin:0 8px 8px 15px;padding:2px 0 2px 10px;display:flex}.lc-fa-op{min-width:0;color:var(--dsw-alias-label-primary);font:inherit;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:5px;align-items:center;gap:8px;padding:3px 6px;font-size:11px;display:flex}.lc-fa-op-link{cursor:pointer}.lc-fa-op-link:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-fa-op-time{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto}.lc-fa-op-tool{text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);max-width:220px;color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:0 6px;font-size:11px;overflow:hidden}.lc-fa-op-detail{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;overflow:hidden}.lc-fa-read{color:var(--dsw-alias-brand-primary);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;font-size:11px;font-weight:600}";
6723
+ const css$5 = ".lc-fa-ctl,.lc-br-toolctl{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:8px;display:flex}.lc-fa-ctl .lc-gran{margin-left:0}.lc-fa-n{opacity:.75;margin-left:4px;font-weight:700}.lc-fa-search,.lc-br-tool-search{min-width:80px;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;outline:none;flex:1;padding:3px 8px;font-size:12px}.lc-fa-search:focus,.lc-br-tool-search:focus{border-color:var(--dsw-alias-label-dimmed)}.lc-fa-meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:6px;font-size:12px;display:flex}.lc-fa-meta .lc-fa-sort{flex:none}.lc-fa-meta-delta{position:relative}.lc-fa-meta-tip{white-space:normal;max-width:260px;line-height:1.5;top:calc(100% + 6px);left:0}.lc-fa-meta-delta:hover .lc-fa-meta-tip{opacity:1}.lc-fa-item{transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border:1px solid #0000;border-radius:8px}.lc-fa-item-on{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}.lc-fa-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:7px;align-items:center;gap:7px;padding:6px 8px;font-size:12px;display:flex}.lc-fa-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-fa-form{opacity:.85;flex:none;justify-content:center;align-items:center;width:20px;height:14px;font-size:11px;display:flex}.lc-fa-lang{border-radius:4px;justify-content:center;align-items:center;min-width:16px;height:13px;padding:0 2px;font-size:8px;font-weight:700;line-height:1;display:inline-flex}.lc-fa-row .lc-br-chev{margin-right:-2px}.lc-fa-path{word-break:break-all;flex:1;min-width:0}@container lc-card (width<=380px){.lc-fa-row{flex-wrap:wrap}.lc-fa-path{flex-basis:calc(100% - 46px)}.lc-fa-ctl .lc-gran{flex-wrap:wrap}}.lc-fa-file{cursor:pointer}.lc-fa-file:hover{text-decoration:underline}.lc-fa-path em{color:var(--dsw-alias-label-secondary);font-style:normal}.lc-fa-badge{font-variant-numeric:tabular-nums;white-space:nowrap;border-radius:999px;flex:none;align-items:center;gap:4px;padding:1px 7px;font-size:11px;font-weight:600;display:inline-flex}.lc-fa-badge i{background:currentColor;border-radius:50%;width:5px;height:5px;display:inline-block}.lc-fa-b-read{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent)}.lc-fa-b-write{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)}.lc-fa-b-search{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent)}.lc-fa-chip-read,.lc-fa-chip-read:hover{color:var(--dsw-alias-brand-primary)}.lc-fa-chip-write,.lc-fa-chip-write:hover{color:var(--dsw-alias-state-success-primary)}.lc-fa-chip-search,.lc-fa-chip-search:hover{color:var(--dsw-alias-state-warn-primary)}.lc-fa-chip-read.lc-gran-on,.lc-fa-chip-read.lc-gran-on:hover{background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent);color:var(--dsw-alias-brand-primary)}.lc-fa-chip-write.lc-gran-on,.lc-fa-chip-write.lc-gran-on:hover{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);color:var(--dsw-alias-state-success-primary)}.lc-fa-chip-search.lc-gran-on,.lc-fa-chip-search.lc-gran-on:hover{background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);color:var(--dsw-alias-state-warn-primary)}.lc-fa-delta{font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;gap:5px;font-size:11px;font-weight:600;display:inline-flex}.lc-fa-up{color:var(--dsw-alias-state-success-primary)}.lc-fa-down{color:var(--dsw-alias-state-error-primary)}.lc-fa-time{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:11px}.lc-fa-ops{border-left:2px solid var(--dsw-alias-border-l1);flex-direction:column;gap:1px;margin:0 8px 8px 15px;padding:2px 0 2px 10px;display:flex}.lc-fa-op{min-width:0;color:var(--dsw-alias-label-primary);font:inherit;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:5px;align-items:center;gap:8px;padding:3px 6px;font-size:11px;display:flex}.lc-fa-op-link{cursor:pointer}.lc-fa-op-link:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-fa-op-time{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto}.lc-fa-op-tool{text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);max-width:220px;color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:0 6px;font-size:11px;overflow:hidden}.lc-fa-op-detail{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;overflow:hidden}.lc-fa-read{color:var(--dsw-alias-brand-primary);font-variant-numeric:tabular-nums;white-space:nowrap;flex:none;font-size:11px;font-weight:600}";
6601
6724
  const tagId$5 = "dsh-context/fileCard.css";
6602
6725
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
6603
6726
  const tag = document.createElement("style");
@@ -6608,7 +6731,7 @@ window.__ModuleLoader__.load({
6608
6731
  }
6609
6732
  //#endregion
6610
6733
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/modal.css.mjs
6611
- const css$4 = ".lc-modal-backdrop{z-index:200;background:var(--dsw-alias-bg-mask-1,#00000073);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.lc-modal-card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);width:min(720px,100vw - 48px);max-height:min(82vh,760px);box-shadow:var(--dsw-shadow-lv3,0 12px 32px #0006);color:var(--dsw-alias-label-primary);border-radius:12px;padding:16px 18px 18px;font-size:13px;overflow-y:auto}.lc-modal-head{align-items:baseline;gap:8px;margin-bottom:12px;display:flex}.lc-modal-title{font-size:14px;font-weight:600}.lc-modal-close{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:6px;margin-left:auto;padding:2px 6px;font-family:inherit;font-size:18px;line-height:1}.lc-modal-close:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}";
6734
+ const css$4 = ".lc-modal-backdrop{z-index:200;background:var(--dsw-alias-bg-mask-1,#00000073);justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.lc-modal-card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);width:min(720px,100vw - 48px);max-height:min(82vh,760px);box-shadow:var(--dsw-shadow-lv3,0 12px 32px #0006);color:var(--dsw-alias-label-primary);border-radius:12px;padding:16px 18px 18px;font-size:13px;overflow-y:auto}.lc-modal-head{align-items:baseline;gap:8px;margin-bottom:12px;display:flex}.lc-modal-title{font-size:14px;font-weight:600}.lc-modal-close{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:6px;margin-left:auto;padding:2px 6px;font-family:inherit;font-size:18px;line-height:1}.lc-modal-close:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.lc-gate-card{width:min(440px,100vw - 48px)}.lc-gate-body{color:var(--dsw-alias-label-secondary);line-height:1.7}.lc-gate-versions{gap:24px;margin-top:14px;display:flex}.lc-gate-version{flex-direction:column;gap:2px;display:flex}.lc-gate-version-label{color:var(--dsw-alias-label-secondary);font-size:11px}.lc-gate-version-value{font-weight:600;font-family:var(--dsw-font-family-mono,monospace);color:var(--dsw-alias-label-primary)}.lc-gate-or-newer{color:var(--dsw-alias-label-secondary);margin-left:6px;font-family:inherit;font-size:11px;font-weight:400}.lc-gate-actions{justify-content:flex-end;margin-top:16px;display:flex}.lc-gate-ok{cursor:pointer;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border:0;border-radius:6px;padding:6px 16px;font-family:inherit;font-size:12px}.lc-gate-ok:hover{filter:brightness(1.1)}";
6612
6735
  const tagId$4 = "dsh-context/modal.css";
6613
6736
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
6614
6737
  const tag = document.createElement("style");
@@ -6716,6 +6839,15 @@ window.__ModuleLoader__.load({
6716
6839
  locale: NS
6717
6840
  }, (props) => h(ContextJump, props));
6718
6841
  });
6842
+ const StatsJump = makeStatsJump(kit);
6843
+ ctx.slots.inject("conversation.composer.dock", () => {
6844
+ return ctx.slots.register({
6845
+ name: "conversation.composer.dock",
6846
+ id: "stats-jump",
6847
+ order: 10,
6848
+ locale: NS
6849
+ }, (props) => h(StatsJump, props));
6850
+ });
6719
6851
  registerContextCommand(ctx, kit);
6720
6852
  const ContextModal = makeContextModal(ctx, kit);
6721
6853
  ctx.slots.inject("conversation.input.overlay", () => {
package/lib/index.d.ts CHANGED
@@ -186,6 +186,17 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
186
186
  type Category = 'user' | 'inject' | 'assistant' | 'tool';
187
187
  interface Snapshot {
188
188
  ok: boolean;
189
+ /**
190
+ * The host's baseline-gate record, present ONLY when the running harness
191
+ * is below the plugin's supported baseline: the host then folds nothing
192
+ * and every figure in this snapshot is zero/empty. The client keeps
193
+ * rendering the (blank) cards and pops the upgrade gate modal naming
194
+ * `current` (the detected harness version) and `minimum` (the baseline).
195
+ */
196
+ unsupported?: {
197
+ current: string;
198
+ minimum: string;
199
+ };
189
200
  model?: string;
190
201
  provider?: string;
191
202
  contextWindow?: number;
@@ -414,12 +425,13 @@ interface HeaderTool {
414
425
  name: string;
415
426
  tokens: number;
416
427
  /**
417
- * The registering plugin's label, when attribution is known: either a
418
- * `plugin` field carried by the raw header entry (harness-provided) or the
419
- * host's best-effort attribution (`mcp:<server>` for MCP tools, or the
420
- * pinned first-party package map). `UNKNOWN_TOOL_SOURCE` marks a tool whose
421
- * provider predates the attribution hook; absent means nothing is known and
422
- * the browser shows no tag.
428
+ * The registering plugin's label, when attribution is known: the host's
429
+ * best-effort attribution (`mcp:<server>` for MCP tools, or the pinned
430
+ * first-party package map), or a `plugin` field carried by the raw header
431
+ * entry — no supported-baseline harness path writes one, but the read
432
+ * stays defensive for foreign/newer producers. `UNKNOWN_TOOL_SOURCE` marks
433
+ * a tool whose provider predates the attribution hook; absent means
434
+ * nothing is known and the browser shows no tag.
423
435
  */
424
436
  plugin?: string;
425
437
  }
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { createRequire } from "node:module";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { dirname, join, normalize } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { z } from "zod";
5
- import z$1 from "@deepseek-ai/schemastery";
6
6
  import { deriveEventMessage } from "@deepseek-ai/dsh-session";
7
+ import z$1 from "@deepseek-ai/schemastery";
7
8
  /**
8
9
  * Recover the MCP server display label from a proxied tool name, or undefined
9
10
  * for non-MCP names. `dsh-mcp-client` names tools `mcp__<server>__<rawName>`
@@ -30,10 +31,11 @@ function pinnedSourceOf(name) {
30
31
  }
31
32
  /**
32
33
  * Pinned first-party tool → plugin package map (see header comment). One entry
33
- * per model-facing name of the shipped tool packages at dsh 0.1.1-rc.2, from
34
- * the official tool-schema catalog; names are stable across the supported
35
- * harness range. Packages that mount under distinct names per composition
36
- * (bash/pwsh persistent variants, subagent fork) map to their primary package.
34
+ * per model-facing name of the shipped tool packages in the official
35
+ * tool-schema catalog of the supported dsh baseline; names are verified
36
+ * stable across the supported harness range. Packages that mount under
37
+ * distinct names per composition (bash/pwsh persistent variants, the
38
+ * `subagent_fork` fixed-route alias) map to their primary package.
37
39
  */
38
40
  const FIRST_PARTY_SOURCES = Object.freeze({
39
41
  read: "@deepseek-ai/dsh-tool-fs",
@@ -288,6 +290,70 @@ function resolveBounds(config) {
288
290
  return Config.parse(config ?? {});
289
291
  }
290
292
  //#endregion
293
+ //#region src/shared/version.ts
294
+ /**
295
+ * The harness-version gate's shared arithmetic — the supported dsh baseline
296
+ * and the version compare behind it. Runtime code shared by BOTH halves (the
297
+ * host probes and gates; the client displays what the wire record carries),
298
+ * so this module must stay dependency-free.
299
+ *
300
+ * The baseline mirrors the support matrix (AGENTS.md "Compatibility" and the
301
+ * package's `dsh.compatibility.dshReleases` declaration): the oldest dsh
302
+ * release this plugin works on. A harness BELOW it gets the fallback units
303
+ * (host/fallback.ts) instead of the real folds.
304
+ */
305
+ /** The oldest supported dsh release (see the matrix note above). */
306
+ const BASELINE_DSH_VERSION = "0.1.2-rc.1";
307
+ /**
308
+ * Release-channel rank at an equal X.Y.Z: a final release outranks its
309
+ * release candidates, which outrank betas, which outrank alphas
310
+ * (正式版 > RC > Beta > Alpha).
311
+ */
312
+ function channelRank(channel) {
313
+ return channel === "rc" ? 3 : channel === "beta" ? 2 : 1;
314
+ }
315
+ const RELEASE_RANK = 4;
316
+ /**
317
+ * Parse `v?[major].[minor].[patch][-(alpha|beta|rc)[.N]][+build]`, or null
318
+ * when the string is not that shape. Channels other than alpha/beta/rc
319
+ * (nightly, dev, …) do not parse — the gate fails open on them.
320
+ */
321
+ function parseVersion(version) {
322
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)(?:\.(\d+))?)?(?:\+[0-9a-z.-]+)?$/i.exec(version.trim());
323
+ if (match === null) return null;
324
+ const channel = match[4]?.toLowerCase();
325
+ return {
326
+ major: Number(match[1]),
327
+ minor: Number(match[2]),
328
+ patch: Number(match[3]),
329
+ rank: channel === void 0 ? RELEASE_RANK : channelRank(channel),
330
+ serial: match[5] ? Number(match[5]) : 0
331
+ };
332
+ }
333
+ /**
334
+ * Total order over parsed versions: X.Y.Z numerically first, then the
335
+ * channel rank, then the prerelease serial.
336
+ */
337
+ function compareParsed(a, b) {
338
+ if (a.major !== b.major) return a.major - b.major;
339
+ if (a.minor !== b.minor) return a.minor - b.minor;
340
+ if (a.patch !== b.patch) return a.patch - b.patch;
341
+ if (a.rank !== b.rank) return a.rank - b.rank;
342
+ return a.serial - b.serial;
343
+ }
344
+ /**
345
+ * Whether `version` satisfies the supported baseline. FAIL OPEN by design: a
346
+ * version that cannot be parsed (a dev/nightly harness build) must not blank
347
+ * a working deployment, so it passes — the gate trips only on a proven
348
+ * below-baseline release.
349
+ */
350
+ function meetsBaseline(version, baseline = BASELINE_DSH_VERSION) {
351
+ const v = parseVersion(version);
352
+ const b = parseVersion(baseline);
353
+ if (v === null || b === null) return true;
354
+ return compareParsed(v, b) >= 0;
355
+ }
356
+ //#endregion
291
357
  //#region src/shared/imageTokens.ts
292
358
  /**
293
359
  * Per-image token estimate for DeepSeek's vision model — a faithful port of
@@ -622,6 +688,7 @@ const headerToolWireSchema = z.object({
622
688
  tokens: z.number().int().nonnegative(),
623
689
  plugin: z.string().optional()
624
690
  }).strict();
691
+ /** Exported for the fallback unit (fallback.ts): one wire contract, one schema. */
625
692
  const contextHeadersSchema = z.object({ headers: z.array(z.object({
626
693
  seq: z.number(),
627
694
  time: z.number(),
@@ -696,26 +763,6 @@ function createContextHeadersDefinition(resolve) {
696
763
  };
697
764
  }
698
765
  //#endregion
699
- //#region src/host/settings.ts
700
- /** The namespace is the join key between the Host registration and the browser card. */
701
- const SETTINGS_NAMESPACE = "dsh-context";
702
- /** Section schema: also the wire envelope the browser scope validates against. */
703
- const SettingsSchema = z$1.object({
704
- defaultGranularity: z$1.union(["step", "turn"]).default("step"),
705
- defaultTrendMode: z$1.union(["total", "delta"]).default("total").loose(),
706
- defaultFileSort: z$1.union([
707
- "count",
708
- "latest",
709
- "path"
710
- ]).default("count").loose()
711
- });
712
- /** Serve the namespace while a settings provider is composed; inert otherwise. */
713
- function installSettings(ctx) {
714
- ctx.inject(["settings"], (sctx) => {
715
- sctx.settings.register(SETTINGS_NAMESPACE, SettingsSchema);
716
- });
717
- }
718
- //#endregion
719
766
  //#region src/host/fold.ts
720
767
  function trimToLastTurns(requests, maxTurns) {
721
768
  let runs = 0;
@@ -1470,8 +1517,15 @@ const timingTotalsSchema = z.object({
1470
1517
  toolCalls: z.number().int().nonnegative(),
1471
1518
  tools: z.record(z.string(), toolTimingSchema)
1472
1519
  }).strict();
1520
+ /** The baseline-gate record the fallback unit serves (see fallback.ts). */
1521
+ const unsupportedSchema = z.object({
1522
+ current: z.string(),
1523
+ minimum: z.string()
1524
+ }).strict();
1525
+ /** Exported for the fallback unit (fallback.ts): one wire contract, one schema. */
1473
1526
  const contextTimelineSchema = z.object({
1474
1527
  ok: z.literal(true),
1528
+ unsupported: unsupportedSchema.optional(),
1475
1529
  model: z.string().optional(),
1476
1530
  provider: z.string().optional(),
1477
1531
  contextWindow: z.number().optional(),
@@ -1492,7 +1546,7 @@ const contextTimelineSchema = z.object({
1492
1546
  archiveFloor: z.number().int().nonnegative().optional()
1493
1547
  }).strict();
1494
1548
  /**
1495
- * The persisted fold-state schema (the dsh 0.1.1-rc.1+ `stateSchema`
1549
+ * The persisted fold-state schema (the registry's `stateSchema`
1496
1550
  * contract). Validates the plain-JSON `TimelineState` before a checkpoint
1497
1551
  * row seeds a fold — the same shape guarantee the projection cache's
1498
1552
  * plain-JSON precondition already enforces at write time.
@@ -1566,10 +1620,229 @@ function createContextTimelineDefinition(config) {
1566
1620
  };
1567
1621
  }
1568
1622
  //#endregion
1623
+ //#region src/host/fallback.ts
1624
+ /**
1625
+ * The baseline gate's fallback projection units (see host/index.ts).
1626
+ *
1627
+ * On a harness below the supported baseline (shared/version.ts) the plugin
1628
+ * registers these INSTEAD of the real folds: the log's event shapes on such
1629
+ * a harness are outside the compat matrix, so nothing is parsed at all —
1630
+ * `apply` is the identity over an opaque empty state, and `view` serves a
1631
+ * fixed value whose timeline snapshot carries the gate record (`unsupported`:
1632
+ * the detected harness version and the baseline). The client's cards render
1633
+ * the blank data and its gate modal urges the upgrade; the registry pipeline
1634
+ * (fold, cache, push feed) keeps working end to end, so nothing hangs on a
1635
+ * loading screen.
1636
+ *
1637
+ * The definition carries BOTH registry contract generations: the modern
1638
+ * `stateSchema` + `wire` block (dsh 0.1.1-rc.1+, the realistic below-baseline
1639
+ * case) and the pre-0.1.1 top-level `schema` + `view` aliases that line's
1640
+ * registry reads instead. Each registry ignores the other generation's
1641
+ * fields, so one definition serves the gate on every harness that can
1642
+ * deliver projections to clients at all.
1643
+ *
1644
+ * `stateVersion` is pinned at 1. Downgrade/upgrade cache choreography: a
1645
+ * downgrade to a gated harness refolds the timeline key from scratch (no
1646
+ * ver-1 rows exist for it) and seeds the headers key from the real unit's
1647
+ * ver-1 rows — stripped to the empty state, which the gate never reads. An
1648
+ * upgrade back discards the fallback's ver-1 timeline rows and REJECTS its
1649
+ * ver-1 headers rows at the real unit's stricter state schema, refolding
1650
+ * both from the log. Nothing stale survives in either direction.
1651
+ */
1652
+ /**
1653
+ * The opaque fold state: the gate folds nothing, so any cached row seeds it
1654
+ * (strip-mode object — never a discard, never a throw), and the plain-JSON
1655
+ * cache-write gate trivially holds.
1656
+ */
1657
+ const fallbackStateSchema = z.object({});
1658
+ /** One gate unit: identity fold over the opaque state, constant view, both contract generations. */
1659
+ function fallbackDefinition(key, wireSchema, value) {
1660
+ const view = () => value;
1661
+ return {
1662
+ key,
1663
+ stateSchema: fallbackStateSchema,
1664
+ init: () => ({}),
1665
+ apply: (state) => state,
1666
+ wire: {
1667
+ viewSchema: wireSchema,
1668
+ view
1669
+ },
1670
+ schema: wireSchema,
1671
+ view,
1672
+ stateVersion: 1
1673
+ };
1674
+ }
1675
+ /**
1676
+ * The fallback `contextTimeline` unit: serves the fixed zeroed snapshot
1677
+ * naming the detected harness `current` version against the baseline.
1678
+ */
1679
+ function createFallbackTimelineDefinition(current) {
1680
+ return fallbackDefinition("contextTimeline", contextTimelineSchema, {
1681
+ ok: true,
1682
+ unsupported: {
1683
+ current,
1684
+ minimum: BASELINE_DSH_VERSION
1685
+ },
1686
+ current: {
1687
+ system: 0,
1688
+ tools: 0,
1689
+ user: 0,
1690
+ inject: 0,
1691
+ assistant: 0,
1692
+ tool: 0,
1693
+ total: 0
1694
+ },
1695
+ requests: [],
1696
+ events: [],
1697
+ nodes: [],
1698
+ droppedNodes: 0,
1699
+ archive: []
1700
+ });
1701
+ }
1702
+ /**
1703
+ * The fallback `contextHeaders` unit: an empty epoch list — the browser's
1704
+ * header sections degrade to their metadata-free rendering.
1705
+ */
1706
+ function createFallbackHeadersDefinition() {
1707
+ return fallbackDefinition("contextHeaders", contextHeadersSchema, { headers: [] });
1708
+ }
1709
+ //#endregion
1710
+ //#region src/host/settings.ts
1711
+ /** The namespace is the join key between the Host registration and the browser card. */
1712
+ const SETTINGS_NAMESPACE = "dsh-context";
1713
+ /** Section schema: also the wire envelope the browser scope validates against. */
1714
+ const SettingsSchema = z$1.object({
1715
+ defaultGranularity: z$1.union(["step", "turn"]).default("step"),
1716
+ defaultTrendMode: z$1.union(["total", "delta"]).default("total").loose(),
1717
+ defaultFileSort: z$1.union([
1718
+ "count",
1719
+ "latest",
1720
+ "path"
1721
+ ]).default("count").loose()
1722
+ });
1723
+ /** Serve the namespace while a settings provider is composed; inert otherwise. */
1724
+ function installSettings(ctx) {
1725
+ ctx.inject(["settings"], (sctx) => {
1726
+ sctx.settings.register(SETTINGS_NAMESPACE, SettingsSchema);
1727
+ });
1728
+ }
1729
+ //#endregion
1730
+ //#region src/host/version.ts
1731
+ /**
1732
+ * Runtime harness-version probe behind the baseline gate (host/index.ts).
1733
+ *
1734
+ * The probe never imports a harness package for its version; it reads
1735
+ * manifests through Node resolution from two anchors, in order:
1736
+ *
1737
+ * 1. The harness home's healed `profiles/node_modules` mirror (located via
1738
+ * the app-boot `dshHomePath` service) — it symlinks/proxies the running
1739
+ * installation's dependency closure, so it names the true release even
1740
+ * when this plugin is a `link:`-installed dev checkout.
1741
+ * 2. This module itself — in a real profile install the plugin sits inside
1742
+ * the profile's node_modules and walk-up resolution reaches the same
1743
+ * mirror; this anchor also covers harnesses too old to provide the home
1744
+ * service.
1745
+ *
1746
+ * The `@deepseek-ai/dsh` CLI package is probed ONLY through the home anchor:
1747
+ * the plugin never depends on it, so a hit from the module anchor can only
1748
+ * be an ambient install above the plugin's tree (e.g. a global copy under
1749
+ * ~/node_modules) — not necessarily the RUNNING harness.
1750
+ *
1751
+ * Every step is guarded: any failure (absent service, unresolvable package,
1752
+ * unreadable/invalid manifest, non-string version) degrades to `undefined`,
1753
+ * and the gate treats an unknown version as SATISFIED — a probe misfire must
1754
+ * never blank a working deployment.
1755
+ */
1756
+ /**
1757
+ * Library packages whose manifest version IS the harness release version
1758
+ * (the dsh monorepo versions every package in lockstep). From the plugin's
1759
+ * own tree they resolve to its pinned devDependencies in dev and to the
1760
+ * healed profiles mirror in real installs — both correct — so they are safe
1761
+ * to probe from either anchor.
1762
+ */
1763
+ const LIBRARY_PROBE_PACKAGES = ["@deepseek-ai/dsh-session-projection", "@deepseek-ai/dsh-session"];
1764
+ /** Home-anchor probe order: the user-facing CLI version first, then the libraries. */
1765
+ const HOME_PROBE_PACKAGES = ["@deepseek-ai/dsh", ...LIBRARY_PROBE_PACKAGES];
1766
+ /** One manifest's `version`, or undefined on any read/shape failure. */
1767
+ function versionOfManifest(manifestPath, expectedName) {
1768
+ try {
1769
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
1770
+ if (parsed === null || typeof parsed !== "object") return void 0;
1771
+ const record = parsed;
1772
+ if (expectedName !== void 0 && record.name !== expectedName) return void 0;
1773
+ return typeof record.version === "string" && record.version !== "" ? record.version : void 0;
1774
+ } catch {
1775
+ return;
1776
+ }
1777
+ }
1778
+ /** Probe the package's published `./package.json` subpath. */
1779
+ function versionViaManifest(resolve, packageName) {
1780
+ try {
1781
+ return versionOfManifest(resolve(packageName + "/package.json"));
1782
+ } catch {
1783
+ return;
1784
+ }
1785
+ }
1786
+ /**
1787
+ * Probe via the package's entry point, ascending to its owning manifest.
1788
+ * Covers the packaged-executable module proxies: their generated manifests
1789
+ * carry the real version but export only entry stubs, and Node's exports
1790
+ * gate refuses the direct `./package.json` subpath.
1791
+ */
1792
+ function versionViaEntry(resolve, packageName) {
1793
+ let dir;
1794
+ try {
1795
+ dir = dirname(resolve(packageName));
1796
+ } catch {
1797
+ return;
1798
+ }
1799
+ for (;;) {
1800
+ const version = versionOfManifest(join(dir, "package.json"), packageName);
1801
+ if (version !== void 0) return version;
1802
+ const parent = dirname(dir);
1803
+ if (parent === dir) return void 0;
1804
+ dir = parent;
1805
+ }
1806
+ }
1807
+ /** One anchor's answer: the first probe package that yields a version. */
1808
+ function probeAnchor(resolve, packageNames) {
1809
+ for (const packageName of packageNames) {
1810
+ const version = versionViaManifest(resolve, packageName) ?? versionViaEntry(resolve, packageName);
1811
+ if (version !== void 0) return version;
1812
+ }
1813
+ }
1814
+ /**
1815
+ * The running harness's version string, or undefined when nothing answers
1816
+ * (the gate fails open on it — see the header note).
1817
+ * @param selfUrl - the module anchor (injectable for hermetic tests).
1818
+ */
1819
+ function detectHarnessVersion(ctx, selfUrl = import.meta.url) {
1820
+ try {
1821
+ const homePath = ctx.get("dshHomePath");
1822
+ if (typeof homePath === "function") {
1823
+ const req = createRequire(homePath("profiles", "dsh-context-version-probe.cjs"));
1824
+ const home = probeAnchor((specifier) => req.resolve(specifier), HOME_PROBE_PACKAGES);
1825
+ if (home !== void 0) return home;
1826
+ }
1827
+ } catch {}
1828
+ try {
1829
+ const req = createRequire(selfUrl);
1830
+ return probeAnchor((specifier) => req.resolve(specifier), LIBRARY_PROBE_PACKAGES);
1831
+ } catch {
1832
+ return;
1833
+ }
1834
+ }
1835
+ //#endregion
1569
1836
  //#region src/host/index.ts
1570
1837
  const name = "dsh-context";
1571
1838
  const inject = ["sessionProjections"];
1572
1839
  function apply(ctx, config) {
1840
+ const harnessVersion = detectHarnessVersion(ctx);
1841
+ if (harnessVersion !== void 0 && !meetsBaseline(harnessVersion)) {
1842
+ ctx.sessionProjections.register(createFallbackTimelineDefinition(harnessVersion));
1843
+ ctx.sessionProjections.register(createFallbackHeadersDefinition());
1844
+ return;
1845
+ }
1573
1846
  const attribution = createToolAttribution(ctx);
1574
1847
  ctx.sessionProjections.register(createContextTimelineDefinition(config));
1575
1848
  ctx.sessionProjections.register(createContextHeadersDefinition((name) => attribution.ownerOf(name)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.41.3",
3
+ "version": "0.43.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": {
@@ -62,11 +62,6 @@
62
62
  },
63
63
  "compatibility": {
64
64
  "dshReleases": {
65
- "0.1.1-rc.2": "compatible",
66
- "0.1.2-alpha.2": "compatible",
67
- "0.1.2-alpha.3": "compatible",
68
- "0.1.2-alpha.4": "compatible",
69
- "0.1.2-alpha.5": "compatible",
70
65
  "0.1.2-rc.1": "compatible"
71
66
  }
72
67
  }
@@ -85,11 +80,11 @@
85
80
  "zod": "^4.4.3"
86
81
  },
87
82
  "peerDependencies": {
88
- "@deepseek-ai/cordis": "^4.0.1",
89
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.1-rc.2",
90
- "@deepseek-ai/dsh-session": ">=0.1.1-rc.2",
91
- "@deepseek-ai/dsh-settings": ">=0.1.1-rc.2",
92
- "@deepseek-ai/schemastery": "^3.18.1",
83
+ "@deepseek-ai/cordis": "^4.0.2",
84
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.2-rc.1",
85
+ "@deepseek-ai/dsh-session": ">=0.1.2-rc.1",
86
+ "@deepseek-ai/dsh-settings": ">=0.1.2-rc.1",
87
+ "@deepseek-ai/schemastery": "^3.18.2",
93
88
  "react": "^18.3.1"
94
89
  },
95
90
  "peerDependenciesMeta": {
@@ -101,12 +96,14 @@
101
96
  }
102
97
  },
103
98
  "devDependencies": {
104
- "@deepseek-ai/cordis": "^4.0.1",
105
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
106
- "@deepseek-ai/dsh-session": "0.1.2-alpha.2",
107
- "@deepseek-ai/dsh-session-projection": "0.1.2-alpha.2",
108
- "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
109
- "@deepseek-ai/schemastery": "^3.18.1",
99
+ "@deepseek-ai/cordis": "^4.0.2",
100
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
101
+ "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
102
+ "@deepseek-ai/dsh-scope": "0.1.2-rc.1",
103
+ "@deepseek-ai/dsh-session": "0.1.2-rc.1",
104
+ "@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
105
+ "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
106
+ "@deepseek-ai/schemastery": "^3.18.2",
110
107
  "@stylistic/eslint-plugin": "^5.10.0",
111
108
  "@types/node": "^26.2.0",
112
109
  "@types/react": "^18.3.31",