dsh-context 0.53.3 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -105,7 +105,7 @@ window.__ModuleLoader__.load({
105
105
  "brief.more": "+{n}",
106
106
  "brief.noInputs": "(无新增)",
107
107
  "brief.locate": "在上下文浏览器中查看",
108
- "jump.title": "在上下文标签页中查看此轮",
108
+ "jump.title": "在上下文视图中查看此轮",
109
109
  "brief.turnTip": "这一轮开场时用户发送的消息;回合内的每一步都会回显同一条",
110
110
  "brief.inputTip": "上一步回复之后、本次请求发出前新进入上下文的内容——通常是上一步所调工具的结果;每轮的第一步无新增",
111
111
  "brief.replyTip": "这一步模型返回的内容(文本回复或工具调用);此步发起的调用,其结果会出现在下一柱的「输入」行",
@@ -329,6 +329,10 @@ window.__ModuleLoader__.load({
329
329
  "ov.kpi.sessionsSub": "含 {n} 个会话",
330
330
  "ov.kpi.toolSub": "工具执行 {dur}",
331
331
  "ov.kpi.wallSub": "模型调用 {n}",
332
+ "balance.title": "DeepSeek 余额",
333
+ "balance.tip.total": "总余额",
334
+ "balance.tip.granted": "赠送余额",
335
+ "balance.tip.toppedUp": "充值余额",
332
336
  "ov.heat.title": "活跃度",
333
337
  "ov.heat.sub": "按日期筛选",
334
338
  "ov.heat.empty": "暂无活跃数据 — 会话产生模型请求后,这里会显示每日用量",
@@ -471,7 +475,7 @@ window.__ModuleLoader__.load({
471
475
  "brief.more": "+{n}",
472
476
  "brief.noInputs": "(nothing new)",
473
477
  "brief.locate": "Reveal in Context Browser",
474
- "jump.title": "View this turn in the Context tab",
478
+ "jump.title": "View this turn in the Context view",
475
479
  "brief.turnTip": "The user message that opened this turn — every step of the turn recalls it",
476
480
  "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",
477
481
  "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",
@@ -695,6 +699,10 @@ window.__ModuleLoader__.load({
695
699
  "ov.kpi.sessionsSub": "across {n} sessions",
696
700
  "ov.kpi.toolSub": "tool runs {dur}",
697
701
  "ov.kpi.wallSub": "{n} model calls",
702
+ "balance.title": "DeepSeek balance",
703
+ "balance.tip.total": "Total balance",
704
+ "balance.tip.granted": "Granted balance",
705
+ "balance.tip.toppedUp": "Topped-up balance",
698
706
  "ov.heat.title": "Activity",
699
707
  "ov.heat.sub": "Filter by date",
700
708
  "ov.heat.empty": "No activity yet — daily usage appears once sessions make model requests",
@@ -5069,6 +5077,7 @@ window.__ModuleLoader__.load({
5069
5077
  } : void 0;
5070
5078
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5071
5079
  className: "lc-card",
5080
+ "data-lc-current": true,
5072
5081
  children: [
5073
5082
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5074
5083
  className: "lc-card-title",
@@ -5383,19 +5392,19 @@ window.__ModuleLoader__.load({
5383
5392
  //#region src/client/cost.ts
5384
5393
  /** 1 CNY = 0.15 USD — the fixed CNY-display conversion rate. */
5385
5394
  const USD_PER_CNY = .15;
5386
- /** Off-peak DeepSeek rates are half the peak rates (the official list). */
5387
- const OFF_PEAK_FACTOR = .5;
5395
+ /** DeepSeek's peak rates are twice the off-peak rates (the official list). */
5396
+ const PEAK_FACTOR = 2;
5388
5397
  /** A USD amount in the display currency (CNY divides the fixed rate). */
5389
5398
  function toCurrency(usd, currency) {
5390
5399
  return currency === "cny" ? usd / USD_PER_CNY : usd;
5391
5400
  }
5392
- /** One rate triple at the half-price off-peak rate (the tooltip's `peak | off` pair). */
5393
- function offPeakOf(rate) {
5401
+ /** One rate triple at the doubled peak rate (the tooltip's `peak | off` pair). */
5402
+ function peakOf(rate) {
5394
5403
  return {
5395
- hit: rate.hit * OFF_PEAK_FACTOR,
5396
- miss: rate.miss * OFF_PEAK_FACTOR,
5397
- write: rate.write * OFF_PEAK_FACTOR,
5398
- out: rate.out * OFF_PEAK_FACTOR
5404
+ hit: rate.hit * PEAK_FACTOR,
5405
+ miss: rate.miss * PEAK_FACTOR,
5406
+ write: rate.write * PEAK_FACTOR,
5407
+ out: rate.out * PEAK_FACTOR
5399
5408
  };
5400
5409
  }
5401
5410
  /** One book branch (a provider's models), as far as runtime can prove it. */
@@ -5448,8 +5457,9 @@ window.__ModuleLoader__.load({
5448
5457
  /**
5449
5458
  * Price the session's cumulative billed-token totals. Cache reads bill at
5450
5459
  * the hit rate, uncached input at the miss rate, cache writes at the write
5451
- * rate, output (reasoning included) at the out rate; `off` buckets (the
5452
- * Host splits DeepSeek's period-based list at fold time) price at half.
5460
+ * rate, output (reasoning included) at the out rate; `peak` buckets price
5461
+ * at twice the book rate for DeepSeek (the book lists that provider's
5462
+ * off-peak rates — the Host splits the period-based list at fold time).
5453
5463
  * Null when nothing was priced (no usage folded, no book yet, or no model
5454
5464
  * the book prices), so the cell can show a dash.
5455
5465
  */
@@ -5460,7 +5470,7 @@ window.__ModuleLoader__.load({
5460
5470
  for (const provider of Object.keys(usage)) {
5461
5471
  const models = asRecord(usage[provider]);
5462
5472
  if (models === null) continue;
5463
- const offPeak = isDeepSeekProvider(provider);
5473
+ const deepseek = isDeepSeekProvider(provider);
5464
5474
  for (const model of Object.keys(models)) {
5465
5475
  const rate = priceOf(prices, provider, model);
5466
5476
  const periods = asRecord(models[model]);
@@ -5469,7 +5479,7 @@ window.__ModuleLoader__.load({
5469
5479
  const bucket = asRecord(periods[period]);
5470
5480
  if (bucket === null) continue;
5471
5481
  const price = (numOf(bucket.cacheRead) * rate.hit + numOf(bucket.uncached) * rate.miss + numOf(bucket.cacheWrite) * rate.write + numOf(bucket.output) * rate.out) / 1e6;
5472
- total += offPeak && period === "off" ? price * OFF_PEAK_FACTOR : price;
5482
+ total += deepseek && period === "peak" ? price * PEAK_FACTOR : price;
5473
5483
  any = true;
5474
5484
  }
5475
5485
  }
@@ -6197,6 +6207,141 @@ window.__ModuleLoader__.load({
6197
6207
  return t("ov.time.d", { n: Math.floor(hours / 24) });
6198
6208
  }
6199
6209
  //#endregion
6210
+ //#region src/client/balance.ts
6211
+ const BALANCE_ROUTE = "/api/dsh-context/balance";
6212
+ /** How long a settled answer (including a `null`) serves identical reads. */
6213
+ const TTL_MS$1 = 6e4;
6214
+ /** One platform amount ('110.00', or an already-numeric producer variant), or null. */
6215
+ function amountOf(value) {
6216
+ const n = typeof value === "string" ? Number(value) : value;
6217
+ return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : null;
6218
+ }
6219
+ /**
6220
+ * Narrow the delivered payload to a render-safe value (the same boundary
6221
+ * rigor as `timelineOf`): each entry's currency and amounts re-proved, an
6222
+ * entry failing the shape drops whole, and a payload with no valid entry is
6223
+ * no balance — the caller renders nothing rather than half a figure.
6224
+ */
6225
+ function platformBalanceOf(value) {
6226
+ const data = asRecord(value);
6227
+ if (data === null) return null;
6228
+ const infos = Array.isArray(data.balances) ? data.balances : [];
6229
+ const balances = [];
6230
+ for (const info of infos) try {
6231
+ const entry = asRecord(info);
6232
+ if (entry === null) continue;
6233
+ const currency = entry.currency;
6234
+ const total = amountOf(entry.total);
6235
+ const granted = amountOf(entry.granted);
6236
+ const toppedUp = amountOf(entry.toppedUp);
6237
+ if (typeof currency !== "string" || currency === "" || total === null || granted === null || toppedUp === null) continue;
6238
+ balances.push({
6239
+ currency,
6240
+ total,
6241
+ granted,
6242
+ toppedUp
6243
+ });
6244
+ } catch {
6245
+ continue;
6246
+ }
6247
+ return balances.length > 0 ? {
6248
+ isAvailable: data.isAvailable === true,
6249
+ balances
6250
+ } : null;
6251
+ }
6252
+ /**
6253
+ * The entry matching the display currency (zh → CNY, en → USD), falling back
6254
+ * to the account's first entry when the platform reports none in it — a
6255
+ * number in the account's own currency beats no number.
6256
+ */
6257
+ function balanceEntryOf(balance, currency) {
6258
+ if (balance === null || balance === void 0 || balance.balances.length === 0) return null;
6259
+ const want = currency === "cny" ? "CNY" : "USD";
6260
+ return balance.balances.find((entry) => entry.currency === want) ?? balance.balances[0];
6261
+ }
6262
+ let cached$1 = null;
6263
+ /**
6264
+ * The capsule's read: one route POST, narrowed through
6265
+ * `platformBalanceOf`, deduplicated in flight, TTL-cached. Never rejects.
6266
+ */
6267
+ function fetchPlatformBalance() {
6268
+ if (cached$1 !== null && Date.now() - cached$1.at < TTL_MS$1) return cached$1.promise;
6269
+ const at = Date.now();
6270
+ const promise = (async () => {
6271
+ try {
6272
+ const response = await fetch(BALANCE_ROUTE, {
6273
+ method: "POST",
6274
+ headers: { "content-type": "application/json" }
6275
+ });
6276
+ if (!response.ok) return null;
6277
+ const r = asRecord(await response.json());
6278
+ if (r === null || r.ok !== true || r.value === null || r.value === void 0) return null;
6279
+ return platformBalanceOf(r.value);
6280
+ } catch {
6281
+ return null;
6282
+ }
6283
+ })();
6284
+ cached$1 = {
6285
+ at,
6286
+ promise
6287
+ };
6288
+ return promise;
6289
+ }
6290
+ //#endregion
6291
+ //#region src/client/components/balanceCapsule.tsx
6292
+ /**
6293
+ * BalanceCapsule — the DeepSeek open-platform balance pill in the Context
6294
+ * Dashboard's header (the plugin's one account-level figure, beside the
6295
+ * session-level ones). Mounted only by the dashboard header; it reads the
6296
+ * plugin's balance route once per open through client/balance.ts and renders
6297
+ * NOTHING while the read is pending, absent, or failed — the pill exists
6298
+ * only when there is a live figure to show. The tooltip carries the
6299
+ * breakdown (total / granted / topped-up) in the entry's own currency; the
6300
+ * entry follows the active locale (zh → CNY) with the account's first
6301
+ * currency as the fallback.
6302
+ */
6303
+ /** The symbol prefix for the currencies the platform serves; others show their code. */
6304
+ function symbolOf(currency) {
6305
+ if (currency === "CNY") return "¥";
6306
+ if (currency === "USD") return "$";
6307
+ return currency + " ";
6308
+ }
6309
+ function makeBalanceCapsule(ctx, kit) {
6310
+ const { t } = kit;
6311
+ return function BalanceCapsule() {
6312
+ const [balance, setBalance] = (0, react.useState)(null);
6313
+ (0, react.useEffect)(() => {
6314
+ let on = true;
6315
+ fetchPlatformBalance().then((v) => {
6316
+ if (on) setBalance(v);
6317
+ });
6318
+ return () => {
6319
+ on = false;
6320
+ };
6321
+ }, []);
6322
+ const locale = ctx.locale;
6323
+ const entry = balanceEntryOf(balance, (typeof locale.getLocale === "function" ? locale.getLocale().active : "en") === "zh" ? "cny" : "usd");
6324
+ if (entry === null) return null;
6325
+ const money = (amount) => symbolOf(entry.currency) + amount.toFixed(2);
6326
+ const tip = [
6327
+ t("balance.tip.total") + ": " + money(entry.total),
6328
+ t("balance.tip.granted") + ": " + money(entry.granted),
6329
+ t("balance.tip.toppedUp") + ": " + money(entry.toppedUp)
6330
+ ].join("\n");
6331
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6332
+ className: "lc-ov-balance",
6333
+ title: tip,
6334
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6335
+ className: "lc-ov-balance-label",
6336
+ children: t("balance.title")
6337
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6338
+ className: "lc-ov-balance-value",
6339
+ children: money(entry.total)
6340
+ })]
6341
+ });
6342
+ };
6343
+ }
6344
+ //#endregion
6200
6345
  //#region src/shared/days.ts
6201
6346
  /**
6202
6347
  * The local-day key (`YYYY-MM-DD`) shared by the activity fold (host) and the
@@ -6658,6 +6803,7 @@ window.__ModuleLoader__.load({
6658
6803
  const { t, fmtDuration } = kit;
6659
6804
  const Heatmap = makeHeatmap(kit);
6660
6805
  const OverviewCard = makeOverviewCard(kit);
6806
+ const BalanceCapsule = makeBalanceCapsule(ctx, kit);
6661
6807
  const ErrorBoundary = makeErrorBoundary(t);
6662
6808
  /** The display currency follows the active locale (zh → CNY), read per render — the slot outlet re-renders on a locale switch. */
6663
6809
  function activeCurrency() {
@@ -6742,6 +6888,7 @@ window.__ModuleLoader__.load({
6742
6888
  className: "lc-ov-title",
6743
6889
  children: t("ov.title")
6744
6890
  }),
6891
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceCapsule, {}),
6745
6892
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6746
6893
  className: "lc-gran lc-ov-range",
6747
6894
  role: "group",
@@ -9492,7 +9639,7 @@ window.__ModuleLoader__.load({
9492
9639
  return function PluginInfo() {
9493
9640
  const [latest, setLatest] = (0, react.useState)(null);
9494
9641
  (0, react.useEffect)(() => {
9495
- if ("0.53.3".includes("-dev")) return;
9642
+ if ("0.54.0".includes("-dev")) return;
9496
9643
  let on = true;
9497
9644
  fetchLatestVersion().then((v) => {
9498
9645
  if (on && v) setLatest(v);
@@ -9501,8 +9648,8 @@ window.__ModuleLoader__.load({
9501
9648
  on = false;
9502
9649
  };
9503
9650
  }, []);
9504
- const update = latest !== null && isNewerVersion(latest, "0.53.3") ? latest : null;
9505
- const nameText = "dsh-context (v0.53.3)";
9651
+ const update = latest !== null && isNewerVersion(latest, "0.54.0") ? latest : null;
9652
+ const nameText = "dsh-context (v0.54.0)";
9506
9653
  const nameValue = [nameText];
9507
9654
  if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9508
9655
  className: "lc-pi-update",
@@ -9986,12 +10133,13 @@ window.__ModuleLoader__.load({
9986
10133
  const rate = priceOf(prices, provider, model);
9987
10134
  if (rate === null) continue;
9988
10135
  const periods = asRecord(models[model]);
9989
- const off = isDeepSeekProvider(provider) && periods !== null && periods.off !== void 0 ? offPeakOf(rate) : void 0;
10136
+ const deepseek = isDeepSeekProvider(provider);
10137
+ const billedOff = deepseek && periods !== null && periods.off !== void 0;
9990
10138
  rows.push({
9991
10139
  key: provider + "/" + model,
9992
10140
  label: multi && provider !== "" ? `${model} · ${provider}` : model,
9993
- rate,
9994
- ...off !== void 0 ? { offRate: off } : {}
10141
+ rate: deepseek ? peakOf(rate) : rate,
10142
+ ...billedOff ? { offRate: rate } : {}
9995
10143
  });
9996
10144
  }
9997
10145
  }
@@ -10438,17 +10586,130 @@ window.__ModuleLoader__.load({
10438
10586
  };
10439
10587
  }
10440
10588
  //#endregion
10589
+ //#region src/client/sidebar.ts
10590
+ /**
10591
+ * The right Sidebar's Context tab (dsh 0.1.5-rc.1+).
10592
+ *
10593
+ * The tab reuses the Context conversation-view component VERBATIM: the
10594
+ * `sidebar.right.pane.tab` seat is session-scoped and delivers the same
10595
+ * framework standard kit (`sessionId`, `useProjection`, `useChat`, the locale
10596
+ * `t` seat) the `conversation.view` seat does, so the panel and the tab are
10597
+ * one component with one data path. The tab type contributes a guide entry, so
10598
+ * the sidebar's guide page offers "Context" and picking it opens the panel —
10599
+ * the product's own path, exactly as the shipped Files type does: a capsule of
10600
+ * glyph, title, and description line, plus the chip-title seat that puts the
10601
+ * same glyph beside the label once the tab is open (`icon.tsx`).
10602
+ *
10603
+ * OPTIONAL BY CONTRACT. `ctx.sidebarRightTabs` and the seat ship only on the
10604
+ * 0.1.5 line (0.1.5-rc.1+ supported); the registration therefore rides a
10605
+ * DEFERRED inject (the plugin's hard injects stay `slots` + `locale`), so on
10606
+ * every older supported line the callback never fires, the plugin fiber never
10607
+ * pends, and nothing is registered. The registry is re-proved structurally and
10608
+ * the whole registration is guarded: a foreign or hostile registry (a throwing
10609
+ * `register`, a taken id/kind) leaves the sidebar without the tab instead of
10610
+ * taking the browser down.
10611
+ *
10612
+ * @module dsh-context/client/sidebar
10613
+ */
10614
+ /** The tab type's identity in the sidebar's tab system (also its body-seat key). */
10615
+ const SIDEBAR_CONTEXT_ID = "dsh-context";
10616
+ /**
10617
+ * The kind `openTab` names. Namespaced rather than the bare `context`: the
10618
+ * registry THROWS when a kind collides with another registration in a
10619
+ * non-coexisting band, and a foreign plugin may well own `context`.
10620
+ */
10621
+ const SIDEBAR_CONTEXT_KIND = "dsh-context";
10622
+ /** The guide capsule's position: after the shipped Files entry (order 10). */
10623
+ const GUIDE_ORDER = 20;
10624
+ /**
10625
+ * Register the Context tab type, its body, and its chip title on the right
10626
+ * Sidebar, if — and only if — this harness serves the sidebar tab registry.
10627
+ * @param ctx - client root context carrying `slots` and the locale service.
10628
+ * @param view - the Context view component factory result (the same one the
10629
+ * conversation tab mounts).
10630
+ * @param t - the plugin-namespace translate; the label thunks read the active
10631
+ * locale at call time, so a language switch relabels the guide entry.
10632
+ * @param ns - the plugin's locale namespace, put on the body registration so
10633
+ * the framework synthesizes the `t` seat for the panel too.
10634
+ * @returns the deferred inject's disposer (placement toggling calls it to
10635
+ * take the mount down; a no-op on faces that return no handle).
10636
+ */
10637
+ function watchSidebarContextTab(ctx, view, t, ns) {
10638
+ const handle = ctx.inject(["sidebarRightTabs"], (raw) => {
10639
+ const injected = raw;
10640
+ const disposers = [];
10641
+ const own = (result) => {
10642
+ if (typeof result === "function") disposers.push(result);
10643
+ };
10644
+ try {
10645
+ const tabs = injected.sidebarRightTabs;
10646
+ if (tabs === void 0 || typeof tabs.register !== "function") return;
10647
+ own(tabs.register({
10648
+ id: SIDEBAR_CONTEXT_ID,
10649
+ kind: SIDEBAR_CONTEXT_KIND,
10650
+ title: () => t("tab"),
10651
+ guide: [{
10652
+ order: GUIDE_ORDER,
10653
+ title: () => t("tab"),
10654
+ description: () => t("sidebar.guideDescription"),
10655
+ icon: ContextIcon
10656
+ }]
10657
+ }));
10658
+ own(injected.slots.inject("sidebar.right.pane.tab", () => injected.slots.register({
10659
+ name: "sidebar.right.pane.tab",
10660
+ key: SIDEBAR_CONTEXT_ID,
10661
+ locale: ns
10662
+ }, (props) => view({
10663
+ ...props,
10664
+ host: "sidebar"
10665
+ }))));
10666
+ own(injected.slots.inject("sidebar.right.pane.tab.title", () => injected.slots.register({
10667
+ name: "sidebar.right.pane.tab.title",
10668
+ key: SIDEBAR_CONTEXT_ID
10669
+ }, makeContextTabTitle(t))));
10670
+ } catch {
10671
+ for (const dispose of disposers) dispose();
10672
+ return;
10673
+ }
10674
+ return () => {
10675
+ for (const dispose of disposers) dispose();
10676
+ };
10677
+ });
10678
+ return () => {
10679
+ handle?.dispose?.();
10680
+ };
10681
+ }
10682
+ //#endregion
10441
10683
  //#region src/client/viewFocus.ts
10442
10684
  /**
10443
10685
  * Chat → Context jump relay. The assistant-message action records the clicked
10444
- * reply's request seq here and activates the Context tab; the Context view
10686
+ * reply's request seq here and opens the Context view — the right Sidebar's
10687
+ * tab when this harness serves one, else the conversation tab; the view
10445
10688
  * consumes the one-shot focus once its projection data is in. Module-level
10446
10689
  * per-session map — the same life pattern as the modal open-state stores.
10447
10690
  */
10448
10691
  const pendingFocus = /* @__PURE__ */ new Map();
10449
- /** Record the Context step (request seq) to reveal for `sessionId` — replaces any unconsumed request. */
10692
+ /** The mounted Context views re-pinning on later records (repeat jumps). */
10693
+ const focusListeners = /* @__PURE__ */ new Set();
10694
+ /**
10695
+ * Record the Context step (request seq) to reveal for `sessionId` — replaces
10696
+ * any unconsumed request and wakes the mounted views.
10697
+ */
10450
10698
  function requestContextFocus(sessionId, seq) {
10451
10699
  pendingFocus.set(sessionId, seq);
10700
+ for (const listener of focusListeners) listener();
10701
+ }
10702
+ /**
10703
+ * Wake on every record while subscribed: a Context view that is already
10704
+ * mounted (the sidebar landing keeps it mounted across jumps) re-pins when a
10705
+ * record lands — the listener takes its OWN session's entry, so other
10706
+ * sessions' records leave their entries pending for their views.
10707
+ */
10708
+ function subscribeContextFocus(listener) {
10709
+ focusListeners.add(listener);
10710
+ return () => {
10711
+ focusListeners.delete(listener);
10712
+ };
10452
10713
  }
10453
10714
  /** Take the pending focus request, if any — one-shot, the map entry is consumed. */
10454
10715
  function takeContextFocus(sessionId) {
@@ -10458,6 +10719,24 @@ window.__ModuleLoader__.load({
10458
10719
  return seq;
10459
10720
  }
10460
10721
  /**
10722
+ * Open the Context tab on the right Sidebar over `ctx.sidebarRight.openTab`
10723
+ * (the column expands in the same step). OPTIONAL seam, re-proved at call
10724
+ * time: a harness without the service (every line older than 0.1.5-rc.1), a
10725
+ * placement that registered no tab type, no mounted session surface, or a
10726
+ * hostile face all report false so the caller keeps its conversation-tab
10727
+ * fallback.
10728
+ */
10729
+ function openContextSidebar(ctx) {
10730
+ try {
10731
+ const face = asRecord(ctx.get("sidebarRight"));
10732
+ if (face === null || typeof face.openTab !== "function") return false;
10733
+ face.openTab.call(face, SIDEBAR_CONTEXT_KIND);
10734
+ return true;
10735
+ } catch {
10736
+ return false;
10737
+ }
10738
+ }
10739
+ /**
10461
10740
  * Activate the Context tab by clicking its own tab-bar button (the semantic
10462
10741
  * `button[role="tab"]` chrome the conversation shell renders for every view).
10463
10742
  * The harness hands `openView` only to the ACTIVE view entry, so a nested
@@ -10475,6 +10754,33 @@ window.__ModuleLoader__.load({
10475
10754
  return false;
10476
10755
  }
10477
10756
  //#endregion
10757
+ //#region src/client/revealScroll.ts
10758
+ /**
10759
+ * Scroll an element's nearest scrollable ancestor so the element tops that
10760
+ * scrollport. The jump's landing reveal needs this host-agnostic: the
10761
+ * conversation tab scrolls the shared `[data-conversation-scroll]` container
10762
+ * while the right Sidebar scrolls dockkit's pane body (a CSS-module class,
10763
+ * no stable attribute of its own), so the walk takes whichever ancestor
10764
+ * actually scrolls — the rect delta lands the anchor flush regardless of the
10765
+ * container's current position. `document.body` ends the walk: the page
10766
+ * itself never scrolls. False — never a throw — when no ancestor scrolls (the
10767
+ * layout already shows the element) or the chain turns hostile.
10768
+ *
10769
+ * @module dsh-context/client/revealScroll
10770
+ */
10771
+ function revealInScrollParent(anchor) {
10772
+ try {
10773
+ for (let el = anchor.parentElement; el !== null && el !== document.body; el = el.parentElement) {
10774
+ const oy = window.getComputedStyle(el).overflowY;
10775
+ if (oy === "auto" || oy === "scroll") {
10776
+ el.scrollTop += anchor.getBoundingClientRect().top - el.getBoundingClientRect().top;
10777
+ return true;
10778
+ }
10779
+ }
10780
+ } catch {}
10781
+ return false;
10782
+ }
10783
+ //#endregion
10478
10784
  //#region src/client/components/contextView.tsx
10479
10785
  /**
10480
10786
  * Context tab root: renders the `contextTimeline` projection and composes stats, composition, history, events and messages.
@@ -10580,8 +10886,12 @@ window.__ModuleLoader__.load({
10580
10886
  const markers = (0, react.useMemo)(() => attachMarkers(displayRequests, events), [displayRequests, events]);
10581
10887
  (0, react.useEffect)(() => {
10582
10888
  if (typeof sessionId !== "string" || sessionId === "") return;
10583
- const seq = takeContextFocus(sessionId);
10584
- if (seq !== null) setJumpSeq(seq);
10889
+ const take = () => {
10890
+ const seq = takeContextFocus(sessionId);
10891
+ if (seq !== null) setJumpSeq(seq);
10892
+ };
10893
+ take();
10894
+ return subscribeContextFocus(take);
10585
10895
  }, [sessionId]);
10586
10896
  const detailReady = source.detailState === "ready" || source.detailState === "legacy";
10587
10897
  (0, react.useEffect)(() => {
@@ -10592,7 +10902,10 @@ window.__ModuleLoader__.load({
10592
10902
  setGranularity("turn");
10593
10903
  setSelectedSeq(target.seq);
10594
10904
  setFocusTurn(target.turn ?? 0);
10595
- if (scrollerRef.current !== null) scrollerRef.current.scrollTop = 0;
10905
+ /* v8 ignore next 2 -- the effect only fires while mounted, and both render paths attach
10906
+ rootRef and render the composition card. */
10907
+ const anchor = rootRef.current?.querySelector("[data-lc-current]") ?? null;
10908
+ if (anchor !== null) revealInScrollParent(anchor);
10596
10909
  }, [
10597
10910
  jumpSeq,
10598
10911
  data,
@@ -10954,14 +11267,18 @@ window.__ModuleLoader__.load({
10954
11267
  //#endregion
10955
11268
  //#region src/client/components/contextJump.tsx
10956
11269
  /**
10957
- * The assistant-message action that jumps to the Context tab at this reply's
11270
+ * The assistant-message action that opens the Context view at this reply's
10958
11271
  * turn. Registered on the harness `conversation.chat.assistant-actions` seat
10959
11272
  * (the icon row beside copy/branch), it receives the finalized reply's durable
10960
11273
  * message id, resolves the matching assistant node's seq off the `useChat`
10961
- * node seat, records it in the viewFocus relay, and activates the Context
10962
- * tab — where the jump pins the reply's TURN (see contextView's leg 2). An
10963
- * unresolvable seq still switches tabs, just without a pin; a message id that
10964
- * is not a plain string renders nothing at all.
11274
+ * node seat AT RENDER TIME (the seat is a real React hook — render-only; a
11275
+ * click-time read throws the dispatcher guard and the jump would land
11276
+ * unpinned), records it in the viewFocus relay on click, and opens the
11277
+ * Context tab on the right Sidebar — where the jump pins the reply's TURN
11278
+ * (see contextView's leg 2). A harness or placement without the sidebar tab
11279
+ * falls back to the conversation tab; an unresolvable seq still opens the
11280
+ * view, just without a pin; a message id that is not a plain string renders
11281
+ * nothing at all.
10965
11282
  */
10966
11283
  /**
10967
11284
  * The reply's request seq by its durable message id, or null when no served node proves the pair. Join/log nodes are untrusted input: each
@@ -11012,16 +11329,17 @@ window.__ModuleLoader__.load({
11012
11329
  ]
11013
11330
  });
11014
11331
  }
11015
- function makeContextJumpButton(kit) {
11332
+ function makeContextJumpButton(ctx, kit) {
11016
11333
  const { t } = kit;
11017
11334
  return function ContextJump(props) {
11335
+ const nodes = conversationNodesOf(props);
11018
11336
  const messageId = props.messageId;
11019
11337
  if (typeof messageId !== "string" || messageId === "") return null;
11338
+ const seq = seqOfMessageId(nodes, messageId);
11020
11339
  const jump = () => {
11021
- const seq = seqOfMessageId(conversationNodesOf(props), messageId);
11022
11340
  const sessionId = props.sessionId;
11023
11341
  if (seq !== null && typeof sessionId === "string" && sessionId !== "") requestContextFocus(sessionId, seq);
11024
- activateContextTab(t("tab"));
11342
+ if (!openContextSidebar(ctx)) activateContextTab(t("tab"));
11025
11343
  };
11026
11344
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
11027
11345
  label: t("jump.title"),
@@ -11073,100 +11391,6 @@ window.__ModuleLoader__.load({
11073
11391
  };
11074
11392
  }
11075
11393
  //#endregion
11076
- //#region src/client/sidebar.ts
11077
- /**
11078
- * The right Sidebar's Context tab (dsh 0.1.5-rc.1+).
11079
- *
11080
- * The tab reuses the Context conversation-view component VERBATIM: the
11081
- * `sidebar.right.pane.tab` seat is session-scoped and delivers the same
11082
- * framework standard kit (`sessionId`, `useProjection`, `useChat`, the locale
11083
- * `t` seat) the `conversation.view` seat does, so the panel and the tab are
11084
- * one component with one data path. The tab type contributes a guide entry, so
11085
- * the sidebar's guide page offers "Context" and picking it opens the panel —
11086
- * the product's own path, exactly as the shipped Files type does: a capsule of
11087
- * glyph, title, and description line, plus the chip-title seat that puts the
11088
- * same glyph beside the label once the tab is open (`icon.tsx`).
11089
- *
11090
- * OPTIONAL BY CONTRACT. `ctx.sidebarRightTabs` and the seat ship only on the
11091
- * 0.1.5 line (0.1.5-rc.1+ supported); the registration therefore rides a
11092
- * DEFERRED inject (the plugin's hard injects stay `slots` + `locale`), so on
11093
- * every older supported line the callback never fires, the plugin fiber never
11094
- * pends, and nothing is registered. The registry is re-proved structurally and
11095
- * the whole registration is guarded: a foreign or hostile registry (a throwing
11096
- * `register`, a taken id/kind) leaves the sidebar without the tab instead of
11097
- * taking the browser down.
11098
- *
11099
- * @module dsh-context/client/sidebar
11100
- */
11101
- /** The tab type's identity in the sidebar's tab system (also its body-seat key). */
11102
- const SIDEBAR_CONTEXT_ID = "dsh-context";
11103
- /**
11104
- * The kind `openTab` names. Namespaced rather than the bare `context`: the
11105
- * registry THROWS when a kind collides with another registration in a
11106
- * non-coexisting band, and a foreign plugin may well own `context`.
11107
- */
11108
- const SIDEBAR_CONTEXT_KIND = "dsh-context";
11109
- /** The guide capsule's position: after the shipped Files entry (order 10). */
11110
- const GUIDE_ORDER = 20;
11111
- /**
11112
- * Register the Context tab type, its body, and its chip title on the right
11113
- * Sidebar, if — and only if — this harness serves the sidebar tab registry.
11114
- * @param ctx - client root context carrying `slots` and the locale service.
11115
- * @param view - the Context view component factory result (the same one the
11116
- * conversation tab mounts).
11117
- * @param t - the plugin-namespace translate; the label thunks read the active
11118
- * locale at call time, so a language switch relabels the guide entry.
11119
- * @param ns - the plugin's locale namespace, put on the body registration so
11120
- * the framework synthesizes the `t` seat for the panel too.
11121
- * @returns the deferred inject's disposer (placement toggling calls it to
11122
- * take the mount down; a no-op on faces that return no handle).
11123
- */
11124
- function watchSidebarContextTab(ctx, view, t, ns) {
11125
- const handle = ctx.inject(["sidebarRightTabs"], (raw) => {
11126
- const injected = raw;
11127
- const disposers = [];
11128
- const own = (result) => {
11129
- if (typeof result === "function") disposers.push(result);
11130
- };
11131
- try {
11132
- const tabs = injected.sidebarRightTabs;
11133
- if (tabs === void 0 || typeof tabs.register !== "function") return;
11134
- own(tabs.register({
11135
- id: SIDEBAR_CONTEXT_ID,
11136
- kind: SIDEBAR_CONTEXT_KIND,
11137
- title: () => t("tab"),
11138
- guide: [{
11139
- order: GUIDE_ORDER,
11140
- title: () => t("tab"),
11141
- description: () => t("sidebar.guideDescription"),
11142
- icon: ContextIcon
11143
- }]
11144
- }));
11145
- own(injected.slots.inject("sidebar.right.pane.tab", () => injected.slots.register({
11146
- name: "sidebar.right.pane.tab",
11147
- key: SIDEBAR_CONTEXT_ID,
11148
- locale: ns
11149
- }, (props) => view({
11150
- ...props,
11151
- host: "sidebar"
11152
- }))));
11153
- own(injected.slots.inject("sidebar.right.pane.tab.title", () => injected.slots.register({
11154
- name: "sidebar.right.pane.tab.title",
11155
- key: SIDEBAR_CONTEXT_ID
11156
- }, makeContextTabTitle(t))));
11157
- } catch {
11158
- for (const dispose of disposers) dispose();
11159
- return;
11160
- }
11161
- return () => {
11162
- for (const dispose of disposers) dispose();
11163
- };
11164
- });
11165
- return () => {
11166
- handle?.dispose?.();
11167
- };
11168
- }
11169
- //#endregion
11170
11394
  //#region src/client/viewkit.ts
11171
11395
  function makeViewKit(t) {
11172
11396
  const { eventLabel, eventAt } = makeEventText(t);
@@ -11348,7 +11572,7 @@ window.__ModuleLoader__.load({
11348
11572
  }
11349
11573
  //#endregion
11350
11574
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/overview.css.mjs
11351
- const css = ".lc-ov-entry{box-sizing:border-box;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:8px;margin:0 -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}.lc-ov-entry:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-ov-entry-rail{border-radius:50%;flex:none;justify-content:center;gap:0;width:36px;height:36px;margin:0;padding:0}.lc-ov-entry-icon{flex:none}.lc-ov-entry-label{text-align:left;white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;overflow:hidden}.lc-ov-backdrop{z-index:200;background:var(--dsw-alias-bg-mask-1,#00000073);justify-content:center;align-items:center;display:flex;position:fixed;inset:0;container:lc-ov/inline-size}.lc-ov-card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);width:min(1120px,100vw - 32px);max-height:min(86vh,900px);box-shadow:var(--dsw-shadow-lv3,0 12px 32px #0006);color:var(--dsw-alias-label-primary);border-radius:12px;flex-direction:column;padding:16px 18px 18px;font-size:13px;display:flex;overflow:hidden}.lc-ov-head{flex-wrap:wrap;flex:none;align-items:center;gap:10px;margin-bottom:14px;display:flex}.lc-ov-head-icon{flex:none}.lc-ov-title{white-space:nowrap;font-size:15px;font-weight:600}.lc-ov-head .lc-modal-close{flex:none;margin-left:0}.lc-ov-body{flex:auto;grid-template-columns:minmax(0,3fr) minmax(0,7fr);gap:10px;min-height:0;display:grid}.lc-ov-left{scrollbar-gutter:stable;flex-direction:column;gap:10px;min-height:0;padding-right:8px;display:flex;overflow-y:auto}.lc-ov-right{scrollbar-gutter:stable;min-height:0;padding-right:8px;overflow-y:auto}@container lc-ov (width<=932px){.lc-ov-card{overflow-y:auto}.lc-ov-body{display:block}.lc-ov-left{margin-bottom:12px}.lc-ov-left,.lc-ov-right{min-height:auto;padding-right:0;overflow:visible}.lc-ov-heat-card{flex:none}}.lc-ov-left>.lc-card{box-sizing:border-box;min-width:0;margin-bottom:0}.lc-ov-kpis{flex:none;grid-template-rows:repeat(3,1fr);grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.lc-ov-kpi{align-items:center;padding:10px 8px}.lc-ov-kpi .lc-stat-value{font-size:20px}.lc-ov-heat-card{flex-direction:column;flex:1 0 auto;display:flex}.lc-heat{flex:1;justify-content:center;align-items:center;gap:4px;display:flex}.lc-heat-wds,.lc-heat-col{padding-top:13px}.lc-heat-wds{flex-direction:column;gap:3px;display:flex}.lc-heat-wd{width:10px;height:20px;color:var(--dsw-alias-label-secondary);text-align:center;flex:none;justify-content:center;align-items:center;font-size:9px;display:flex}.lc-heat-cols{gap:3px;display:flex}.lc-heat-col{flex-direction:column;gap:3px;display:flex;position:relative}.lc-heat-mon{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:9px;line-height:13px;position:absolute;top:0;left:0}.lc-heat-cell{box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);cursor:default;border:0;border-radius:20%;width:20px;height:20px;padding:0}button.lc-heat-cell{cursor:pointer}button.lc-heat-cell:hover{box-shadow:0 0 0 1.5px color-mix(in srgb, var(--color-blue-500) 70%, transparent)}.lc-heat-future{background:0 0}.lc-heat-0{background:color-mix(in srgb, var(--dsw-alias-label-secondary) 16%, transparent)}.lc-heat-1{background:color-mix(in srgb, var(--color-blue-500) 28%, transparent)}.lc-heat-2{background:color-mix(in srgb, var(--color-blue-500) 50%, transparent)}.lc-heat-3{background:color-mix(in srgb, var(--color-blue-500) 72%, transparent)}.lc-heat-4{background:color-mix(in srgb, var(--color-blue-500) 92%, transparent)}.lc-heat-on{box-shadow:0 0 0 1.5px var(--color-blue-500)}.lc-ov-list-head{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:10px;display:flex}.lc-ov-list-title{white-space:nowrap;font-weight:600}.lc-ov-list-count{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;background:var(--dsw-alias-bg-layer-2);border-radius:8px;padding:1px 7px;font-size:11px}.lc-ov-day-chip{border:1px solid color-mix(in srgb, var(--color-blue-500) 45%, transparent);color:var(--color-blue-500);background:color-mix(in srgb, var(--color-blue-500) 10%, transparent);cursor:pointer;white-space:nowrap;border-radius:999px;padding:1px 10px;font-family:inherit;font-size:11px;line-height:1.4}.lc-ov-search{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:110px;font:inherit;color:var(--dsw-alias-label-primary);transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;outline:none;flex:150px;padding:4px 10px;font-size:12px}.lc-ov-search:focus{border-color:var(--dsw-alias-label-secondary)}.lc-ov-search::placeholder{color:var(--dsw-alias-label-secondary)}.lc-ov-groups{flex-wrap:wrap;gap:6px;margin:-2px 0 10px;display:flex}.lc-ov-chip{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;white-space:nowrap;text-overflow:ellipsis;max-width:220px;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), background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border-radius:999px;align-items:baseline;gap:5px;padding:2px 10px;font-size:11px;line-height:1.5;display:inline-flex;overflow:hidden}.lc-ov-chip:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary)}.lc-ov-chip-on,.lc-ov-chip-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.lc-ov-chip-n{font-variant-numeric:tabular-nums;opacity:.65;font-size:10px}.lc-ov-chip-on .lc-ov-chip-n{opacity:.85}.lc-ov-grid{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;display:grid}.lc-ov-session{text-align:left;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;font:inherit;color:inherit;transition:border-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);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}.lc-ov-session:hover{border-color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}.lc-ov-session-current{border-color:color-mix(in srgb, var(--color-blue-500) 65%, transparent)}.lc-ov-session-head{align-items:flex-start;gap:6px;min-width:0;display:flex}.lc-ov-running{background:var(--color-green-500);border-radius:50%;flex:none;width:7px;height:7px}.lc-ov-session-title{-webkit-line-clamp:2;overflow-wrap:anywhere;-webkit-box-orient:vertical;flex:auto;min-width:0;font-weight:600;display:-webkit-box;overflow:hidden}.lc-ov-current{color:var(--color-blue-500);border:1px solid color-mix(in srgb, var(--color-blue-500) 45%, transparent);border-radius:4px;flex:none;padding:0 4px;font-size:10px;line-height:1.5}.lc-ov-session-times{flex-direction:column;flex:none;align-items:flex-end;gap:1px;display:flex}.lc-ov-session-time{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:11px}.lc-ov-session-created{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-variant-numeric:tabular-nums;font-size:10px;line-height:1.4}.lc-ov-session-crumb{min-width:0;color:var(--dsw-alias-label-secondary);white-space:nowrap;align-items:center;gap:5px;font-size:11px;display:flex;overflow:hidden}.lc-ov-crumb-group{text-overflow:ellipsis;flex:none;max-width:55%;overflow:hidden}.lc-ov-crumb-sep{opacity:.55;flex:none}.lc-ov-crumb-project{text-overflow:ellipsis;min-width:0;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-label-primary);background:color-mix(in srgb, var(--dsw-alias-label-secondary) 16%, transparent);border-radius:999px;flex:0 auto;padding:0 8px;font-size:10px;line-height:1.6;overflow:hidden}.lc-ov-session-body{align-items:center;gap:14px;min-width:0;display:flex}.lc-ov-mini-stats{flex-direction:column;flex:auto;gap:3px;min-width:0;display:flex}.lc-ov-mini-stat{justify-content:space-between;align-items:baseline;gap:10px;font-size:11px;display:flex}.lc-ov-mini-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none}.lc-ov-mini-value{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.lc-ov-session-empty{color:var(--dsw-alias-label-secondary);padding:6px 0;font-size:11px;line-height:1.5}.lc-ov-session-preview{border-top:1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 60%, transparent);align-items:baseline;gap:6px;min-width:0;margin-top:auto;padding-top:6px;display:flex}.lc-ov-session-preview-label{color:var(--dsw-alias-label-secondary);flex:none;font-size:10px}.lc-ov-session-preview-text{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);flex:0 auto;font-size:11px;line-height:1.5;overflow:hidden}.lc-ov-pager{justify-content:center;align-items:center;gap:10px;margin-top:12px;display:flex}.lc-ov-pager-btn{border:1px solid var(--dsw-alias-border-l2);width:26px;height:22px;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);background:0 0;border-radius:6px;flex:none;justify-content:center;align-items:center;line-height:1;display:flex}.lc-ov-pager-btn:hover:enabled{border-color:var(--dsw-alias-label-secondary)}.lc-ov-pager-btn:disabled{opacity:.4;cursor:default}.lc-ov-pager-n{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:11px}";
11575
+ const css = ".lc-ov-entry{box-sizing:border-box;width:calc(100% + 4px);height:42px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:8px;margin:0 -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;display:flex;overflow:hidden}.lc-ov-entry:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-ov-entry-rail{border-radius:50%;flex:none;justify-content:center;gap:0;width:36px;height:36px;margin:0;padding:0}.lc-ov-entry-icon{flex:none}.lc-ov-entry-label{text-align:left;white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;overflow:hidden}.lc-ov-backdrop{z-index:200;background:var(--dsw-alias-bg-mask-1,#00000073);justify-content:center;align-items:center;display:flex;position:fixed;inset:0;container:lc-ov/inline-size}.lc-ov-card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);width:min(1120px,100vw - 32px);max-height:min(86vh,900px);box-shadow:var(--dsw-shadow-lv3,0 12px 32px #0006);color:var(--dsw-alias-label-primary);border-radius:12px;flex-direction:column;padding:16px 18px 18px;font-size:13px;display:flex;overflow:hidden}.lc-ov-head{flex-wrap:wrap;flex:none;align-items:center;gap:10px;margin-bottom:14px;display:flex}.lc-ov-head-icon{flex:none}.lc-ov-title{white-space:nowrap;font-size:15px;font-weight:600}.lc-ov-balance{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);cursor:default;border-radius:999px;flex:none;align-items:center;gap:6px;max-width:100%;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex}.lc-ov-balance-label{color:var(--dsw-alias-label-secondary);white-space:nowrap}.lc-ov-balance-value{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.lc-ov-head .lc-modal-close{flex:none;margin-left:0}.lc-ov-body{flex:auto;grid-template-columns:minmax(0,3fr) minmax(0,7fr);gap:10px;min-height:0;display:grid}.lc-ov-left{scrollbar-gutter:stable;flex-direction:column;gap:10px;min-height:0;padding-right:8px;display:flex;overflow-y:auto}.lc-ov-right{scrollbar-gutter:stable;min-height:0;padding-right:8px;overflow-y:auto}@container lc-ov (width<=932px){.lc-ov-card{overflow-y:auto}.lc-ov-body{display:block}.lc-ov-left{margin-bottom:12px}.lc-ov-left,.lc-ov-right{min-height:auto;padding-right:0;overflow:visible}.lc-ov-heat-card{flex:none}}.lc-ov-left>.lc-card{box-sizing:border-box;min-width:0;margin-bottom:0}.lc-ov-kpis{flex:none;grid-template-rows:repeat(3,1fr);grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.lc-ov-kpi{align-items:center;padding:10px 8px}.lc-ov-kpi .lc-stat-value{font-size:20px}.lc-ov-heat-card{flex-direction:column;flex:1 0 auto;display:flex}.lc-heat{flex:1;justify-content:center;align-items:center;gap:4px;display:flex}.lc-heat-wds,.lc-heat-col{padding-top:13px}.lc-heat-wds{flex-direction:column;gap:3px;display:flex}.lc-heat-wd{width:10px;height:20px;color:var(--dsw-alias-label-secondary);text-align:center;flex:none;justify-content:center;align-items:center;font-size:9px;display:flex}.lc-heat-cols{gap:3px;display:flex}.lc-heat-col{flex-direction:column;gap:3px;display:flex;position:relative}.lc-heat-mon{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:9px;line-height:13px;position:absolute;top:0;left:0}.lc-heat-cell{box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);cursor:default;border:0;border-radius:20%;width:20px;height:20px;padding:0}button.lc-heat-cell{cursor:pointer}button.lc-heat-cell:hover{box-shadow:0 0 0 1.5px color-mix(in srgb, var(--color-blue-500) 70%, transparent)}.lc-heat-future{background:0 0}.lc-heat-0{background:color-mix(in srgb, var(--dsw-alias-label-secondary) 16%, transparent)}.lc-heat-1{background:color-mix(in srgb, var(--color-blue-500) 28%, transparent)}.lc-heat-2{background:color-mix(in srgb, var(--color-blue-500) 50%, transparent)}.lc-heat-3{background:color-mix(in srgb, var(--color-blue-500) 72%, transparent)}.lc-heat-4{background:color-mix(in srgb, var(--color-blue-500) 92%, transparent)}.lc-heat-on{box-shadow:0 0 0 1.5px var(--color-blue-500)}.lc-ov-list-head{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:10px;display:flex}.lc-ov-list-title{white-space:nowrap;font-weight:600}.lc-ov-list-count{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;background:var(--dsw-alias-bg-layer-2);border-radius:8px;padding:1px 7px;font-size:11px}.lc-ov-day-chip{border:1px solid color-mix(in srgb, var(--color-blue-500) 45%, transparent);color:var(--color-blue-500);background:color-mix(in srgb, var(--color-blue-500) 10%, transparent);cursor:pointer;white-space:nowrap;border-radius:999px;padding:1px 10px;font-family:inherit;font-size:11px;line-height:1.4}.lc-ov-search{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:110px;font:inherit;color:var(--dsw-alias-label-primary);transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;outline:none;flex:150px;padding:4px 10px;font-size:12px}.lc-ov-search:focus{border-color:var(--dsw-alias-label-secondary)}.lc-ov-search::placeholder{color:var(--dsw-alias-label-secondary)}.lc-ov-groups{flex-wrap:wrap;gap:6px;margin:-2px 0 10px;display:flex}.lc-ov-chip{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;white-space:nowrap;text-overflow:ellipsis;max-width:220px;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), background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border-radius:999px;align-items:baseline;gap:5px;padding:2px 10px;font-size:11px;line-height:1.5;display:inline-flex;overflow:hidden}.lc-ov-chip:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary)}.lc-ov-chip-on,.lc-ov-chip-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.lc-ov-chip-n{font-variant-numeric:tabular-nums;opacity:.65;font-size:10px}.lc-ov-chip-on .lc-ov-chip-n{opacity:.85}.lc-ov-grid{grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;display:grid}.lc-ov-session{text-align:left;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;font:inherit;color:inherit;transition:border-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);border-radius:10px;flex-direction:column;gap:6px;padding:10px 12px;display:flex}.lc-ov-session:hover{border-color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}.lc-ov-session-current{border-color:color-mix(in srgb, var(--color-blue-500) 65%, transparent)}.lc-ov-session-head{align-items:flex-start;gap:6px;min-width:0;display:flex}.lc-ov-running{background:var(--color-green-500);border-radius:50%;flex:none;width:7px;height:7px}.lc-ov-session-title{-webkit-line-clamp:2;overflow-wrap:anywhere;-webkit-box-orient:vertical;flex:auto;min-width:0;font-weight:600;display:-webkit-box;overflow:hidden}.lc-ov-current{color:var(--color-blue-500);border:1px solid color-mix(in srgb, var(--color-blue-500) 45%, transparent);border-radius:4px;flex:none;padding:0 4px;font-size:10px;line-height:1.5}.lc-ov-session-times{flex-direction:column;flex:none;align-items:flex-end;gap:1px;display:flex}.lc-ov-session-time{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:11px}.lc-ov-session-created{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-variant-numeric:tabular-nums;font-size:10px;line-height:1.4}.lc-ov-session-crumb{min-width:0;color:var(--dsw-alias-label-secondary);white-space:nowrap;align-items:center;gap:5px;font-size:11px;display:flex;overflow:hidden}.lc-ov-crumb-group{text-overflow:ellipsis;flex:none;max-width:55%;overflow:hidden}.lc-ov-crumb-sep{opacity:.55;flex:none}.lc-ov-crumb-project{text-overflow:ellipsis;min-width:0;font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-label-primary);background:color-mix(in srgb, var(--dsw-alias-label-secondary) 16%, transparent);border-radius:999px;flex:0 auto;padding:0 8px;font-size:10px;line-height:1.6;overflow:hidden}.lc-ov-session-body{align-items:center;gap:14px;min-width:0;display:flex}.lc-ov-mini-stats{flex-direction:column;flex:auto;gap:3px;min-width:0;display:flex}.lc-ov-mini-stat{justify-content:space-between;align-items:baseline;gap:10px;font-size:11px;display:flex}.lc-ov-mini-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none}.lc-ov-mini-value{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.lc-ov-session-empty{color:var(--dsw-alias-label-secondary);padding:6px 0;font-size:11px;line-height:1.5}.lc-ov-session-preview{border-top:1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 60%, transparent);align-items:baseline;gap:6px;min-width:0;margin-top:auto;padding-top:6px;display:flex}.lc-ov-session-preview-label{color:var(--dsw-alias-label-secondary);flex:none;font-size:10px}.lc-ov-session-preview-text{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);flex:0 auto;font-size:11px;line-height:1.5;overflow:hidden}.lc-ov-pager{justify-content:center;align-items:center;gap:10px;margin-top:12px;display:flex}.lc-ov-pager-btn{border:1px solid var(--dsw-alias-border-l2);width:26px;height:22px;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);background:0 0;border-radius:6px;flex:none;justify-content:center;align-items:center;line-height:1;display:flex}.lc-ov-pager-btn:hover:enabled{border-color:var(--dsw-alias-label-secondary)}.lc-ov-pager-btn:disabled{opacity:.4;cursor:default}.lc-ov-pager-n{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;font-size:11px}";
11352
11576
  const tagId = "dsh-context/overview.css";
11353
11577
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
11354
11578
  const tag = document.createElement("style");
@@ -11407,7 +11631,7 @@ window.__ModuleLoader__.load({
11407
11631
  }),
11408
11632
  sidebar: () => watchSidebarContextTab(ctx, ContextView, t, NS)
11409
11633
  }), "dsh-context: placement");
11410
- const ContextJump = makeContextJumpButton(kit);
11634
+ const ContextJump = makeContextJumpButton(ctx, kit);
11411
11635
  ctx.slots.inject("conversation.chat.assistant-actions", () => {
11412
11636
  return ctx.slots.register({
11413
11637
  name: "conversation.chat.assistant-actions",
package/lib/index.d.ts CHANGED
@@ -796,10 +796,33 @@ interface HeaderRecord {
796
796
  interface ContextHeaders {
797
797
  headers: HeaderRecord[];
798
798
  }
799
+ /** One currency's DeepSeek open-platform balance figures. */
800
+ interface PlatformBalanceEntry {
801
+ /** The ISO code the platform reported (`CNY` / `USD`). */
802
+ currency: string;
803
+ /** Total available funds: the granted and topped-up balances summed. */
804
+ total: number;
805
+ /** The not-expired granted (gift) balance. */
806
+ granted: number;
807
+ /** The topped-up balance. */
808
+ toppedUp: number;
809
+ }
810
+ /**
811
+ * The DeepSeek open-platform balance, served by the plugin's
812
+ * `/api/dsh-context/balance` fetch route (host/balance.ts). `null` on the
813
+ * wire — and nothing rendered client-side — whenever the platform is not
814
+ * configured or the read fails: the capsule only ever shows a live figure.
815
+ */
816
+ interface PlatformBalance {
817
+ /** Whether the platform reports the balance sufficient for API calls. */
818
+ isAvailable: boolean;
819
+ /** One entry per currency the account holds; at least one. */
820
+ balances: PlatformBalanceEntry[];
821
+ }
799
822
  //#endregion
800
823
  //#region src/host/index.d.ts
801
824
  export declare const name = "dsh-context";
802
825
  export declare const inject: string[];
803
826
  export declare function apply(ctx: Context, config: Config): void;
804
827
  //#endregion
805
- export type { ActivityDay, ActivityState, Category, ContextActivity, ContextEventRecord, ContextHeaders, ContextTimeline, ContextTimelineDetail, HeaderRecord, HeaderTool, HeadersState, RequestRecord, Snapshot, SurfaceNode, TimelineCounts, TimelineLast, TimelineState };
828
+ export type { ActivityDay, ActivityState, Category, ContextActivity, ContextEventRecord, ContextHeaders, ContextTimeline, ContextTimelineDetail, HeaderRecord, HeaderTool, HeadersState, PlatformBalance, PlatformBalanceEntry, RequestRecord, Snapshot, SurfaceNode, TimelineCounts, TimelineLast, TimelineState };
package/lib/index.js CHANGED
@@ -2057,6 +2057,163 @@ function createToolAttribution(ctx) {
2057
2057
  return { ownerOf: (name) => mcpSourceOf(name) ?? live.get(name) ?? pinnedSourceOf(name) ?? (boot.has(name) ? "<unknown-plugin>" : void 0) };
2058
2058
  }
2059
2059
  //#endregion
2060
+ //#region src/host/balance.ts
2061
+ /** The plugin's balance route, under the authenticated `/api` fence. */
2062
+ const BALANCE_ROUTE = "/api/dsh-context/balance";
2063
+ /** The settings namespace llm-deepseek installs its connection section under. */
2064
+ const DEEPSEEK_SETTINGS_NS = "llm-deepseek";
2065
+ /** llm-deepseek's default credential ref (the env-var name its section resolves). */
2066
+ const DEFAULT_API_KEY_ENV = "DEEPSEEK_API_KEY";
2067
+ /** The platform's public API root (llm-deepseek's PUBLIC_BASE_URL default). */
2068
+ const PUBLIC_BASE_URL = "https://api.deepseek.com";
2069
+ /** A settled read serves identical reads for this long; failures decay faster. */
2070
+ const CACHE_TTL_MS = 3e5;
2071
+ const CACHE_FAIL_TTL_MS = 6e4;
2072
+ /** One platform read's whole budget — never worth blocking the route longer. */
2073
+ const FETCH_TIMEOUT_MS = 1e4;
2074
+ /** Narrow an unknown value to a string-keyed record, or null. */
2075
+ function asRecord$1(value) {
2076
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2077
+ }
2078
+ /** One platform amount ('110.00', or an already-numeric producer variant), or null. */
2079
+ function amountOf(value) {
2080
+ const n = typeof value === "string" ? Number(value) : value;
2081
+ return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : null;
2082
+ }
2083
+ /**
2084
+ * Resolve the DeepSeek connection facts exactly as llm-deepseek serves its
2085
+ * requests: its settings section for the credential ref and endpoint, the
2086
+ * credentials service for the key. `null` whenever any fact is missing —
2087
+ * the platform is simply not configured for this deployment.
2088
+ */
2089
+ async function resolveFacts(ctx) {
2090
+ const settings = ctx.get("settings");
2091
+ const section = typeof settings?.get === "function" ? asRecord$1(settings.get(DEEPSEEK_SETTINGS_NS)) : null;
2092
+ if (section === null) return null;
2093
+ const apiKeyEnv = typeof section.apiKeyEnv === "string" && section.apiKeyEnv !== "" ? section.apiKeyEnv : DEFAULT_API_KEY_ENV;
2094
+ const baseUrl = typeof section.baseURL === "string" && section.baseURL !== "" ? section.baseURL : PUBLIC_BASE_URL;
2095
+ const credentials = ctx.get("credentials");
2096
+ const resolve = typeof credentials?.resolve === "function" ? credentials.resolve.bind(credentials) : void 0;
2097
+ if (resolve === void 0) return null;
2098
+ let hit;
2099
+ try {
2100
+ hit = await resolve(apiKeyEnv);
2101
+ } catch {
2102
+ return null;
2103
+ }
2104
+ const apiKey = typeof hit?.value === "string" ? hit.value : "";
2105
+ return apiKey === "" ? null : {
2106
+ baseUrl,
2107
+ apiKey
2108
+ };
2109
+ }
2110
+ /**
2111
+ * Narrow the platform's payload to the wire value (the boundary rigor every
2112
+ * parser owes untrusted input): each entry's currency and three amounts are
2113
+ * re-proved, an entry failing the shape drops whole, and a payload with no
2114
+ * valid entry is no balance at all.
2115
+ */
2116
+ function balanceOfPayload(value) {
2117
+ const data = asRecord$1(value);
2118
+ if (data === null) return null;
2119
+ const infos = Array.isArray(data.balance_infos) ? data.balance_infos : [];
2120
+ const balances = [];
2121
+ for (const info of infos) try {
2122
+ const entry = asRecord$1(info);
2123
+ if (entry === null) continue;
2124
+ const currency = entry.currency;
2125
+ const total = amountOf(entry.total_balance);
2126
+ const granted = amountOf(entry.granted_balance);
2127
+ const toppedUp = amountOf(entry.topped_up_balance);
2128
+ if (typeof currency !== "string" || currency === "" || total === null || granted === null || toppedUp === null) continue;
2129
+ balances.push({
2130
+ currency,
2131
+ total,
2132
+ granted,
2133
+ toppedUp
2134
+ });
2135
+ } catch {
2136
+ continue;
2137
+ }
2138
+ return balances.length > 0 ? {
2139
+ isAvailable: data.is_available === true,
2140
+ balances
2141
+ } : null;
2142
+ }
2143
+ /** The outbound platform fetch through the real global fetch. */
2144
+ const defaultFetcher = (url, init) => fetch(url, init);
2145
+ let fetcher = defaultFetcher;
2146
+ let cache = null;
2147
+ let inFlight = null;
2148
+ /**
2149
+ * One platform read for these facts, shared across identical concurrent
2150
+ * reads and remembered briefly: successes for CACHE_TTL_MS, failures for
2151
+ * CACHE_FAIL_TTL_MS (a blip must not pin the capsule away for the full
2152
+ * window). Every failure path — transport, timeout, non-ok status, malformed
2153
+ * payload — resolves `null`; the route never throws into the transport.
2154
+ */
2155
+ async function readBalance(facts) {
2156
+ const key = facts.baseUrl + "\0" + facts.apiKey;
2157
+ if (cache !== null && cache.key === key && Date.now() - cache.at < (cache.value === null ? CACHE_FAIL_TTL_MS : CACHE_TTL_MS)) return cache.value;
2158
+ if (inFlight !== null) return inFlight;
2159
+ inFlight = (async () => {
2160
+ let value = null;
2161
+ try {
2162
+ const response = await fetcher(facts.baseUrl.replace(/\/+$/, "") + "/user/balance", {
2163
+ headers: {
2164
+ accept: "application/json",
2165
+ authorization: "Bearer " + facts.apiKey
2166
+ },
2167
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
2168
+ });
2169
+ if (response.ok) value = balanceOfPayload(await response.json());
2170
+ } catch {}
2171
+ cache = {
2172
+ key,
2173
+ at: Date.now(),
2174
+ value
2175
+ };
2176
+ return value;
2177
+ })();
2178
+ const settled = inFlight;
2179
+ try {
2180
+ return await settled;
2181
+ } finally {
2182
+ inFlight = null;
2183
+ }
2184
+ }
2185
+ /**
2186
+ * Serve the balance route whenever the connection service composes (see
2187
+ * host/detail.ts for the load-order contract). The registration rides the
2188
+ * injected fiber: the service unloading withdraws the route. A rejecting
2189
+ * registry only closes the capsule — never the plugin.
2190
+ */
2191
+ function watchBalanceChannel(ctx) {
2192
+ ctx.inject(["connection"], (c) => {
2193
+ const connection = c.get("connection");
2194
+ const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
2195
+ if (register === void 0) return;
2196
+ const handler = async () => {
2197
+ const facts = await resolveFacts(ctx);
2198
+ const value = facts !== null ? await readBalance(facts) : null;
2199
+ return Response.json({
2200
+ ok: true,
2201
+ value
2202
+ }, { headers: { "cache-control": "no-store" } });
2203
+ };
2204
+ try {
2205
+ c.effect(() => register({
2206
+ path: BALANCE_ROUTE,
2207
+ methods: ["POST"],
2208
+ requestBody: "buffered",
2209
+ fetch: handler
2210
+ }), "dsh-context: balance route");
2211
+ } catch {
2212
+ return;
2213
+ }
2214
+ });
2215
+ }
2216
+ //#endregion
2060
2217
  //#region src/host/backfill.ts
2061
2218
  /** The plugin's warm-up trigger route, under the authenticated `/api` fence. */
2062
2219
  const BACKFILL_ROUTE = "/api/dsh-context/backfill";
@@ -2067,8 +2224,15 @@ const BACKFILL_ROUTE = "/api/dsh-context/backfill";
2067
2224
  * workspace sidebar rides it), so the pass breathes between sessions.
2068
2225
  */
2069
2226
  const YIELD_MS = 100;
2070
- /** The `name` dsh raises for a durable log its migration surface refuses. */
2071
- const UNSUPPORTED_MIGRATION = "SessionFormatUnsupportedMigrationError";
2227
+ /**
2228
+ * The `name` family dsh raises for a durable log it cannot interpret. The
2229
+ * format edge throws `SessionFormatUnsupportedMigrationError`, but the
2230
+ * persistence seam translates it — together with every other "intact yet not
2231
+ * interpretable" refusal — into `SessionFormatUnsupportedError` before the
2232
+ * error escapes (issue #75): classify by the family prefix, since either
2233
+ * exact name may be the one that arrives.
2234
+ */
2235
+ const UNSUPPORTED_FORMAT = "SessionFormatUnsupported";
2072
2236
  function asRecord(value) {
2073
2237
  return value !== null && typeof value === "object" ? value : null;
2074
2238
  }
@@ -2123,15 +2287,15 @@ function isLive(sessions, id) {
2123
2287
  }
2124
2288
  }
2125
2289
  /**
2126
- * Whether the cold read failed because the running harness refuses to
2127
- * migrate this log — a permanent, source-side refusal (the artifact is left
2128
- * unchanged, so retrying cannot succeed). Matched by the error's documented
2129
- * `name`, so no dsh symbol needs importing and every supported baseline
2130
- * classifies alike: an unknown error shape just falls to the warn path.
2290
+ * Whether the cold read failed because the running harness refuses this log's
2291
+ * format — a permanent, source-side refusal (the artifact is left unchanged,
2292
+ * so retrying cannot succeed). Matched by the error's documented `name`, so no
2293
+ * dsh symbol needs importing and every supported baseline classifies alike: an
2294
+ * unknown error shape just falls to the warn path.
2131
2295
  */
2132
- function isUnsupportedMigration(error) {
2296
+ function isUnsupportedFormat(error) {
2133
2297
  try {
2134
- return asRecord(error)?.name === UNSUPPORTED_MIGRATION;
2298
+ return String(asRecord(error)?.name).startsWith(UNSUPPORTED_FORMAT);
2135
2299
  } catch {
2136
2300
  return false;
2137
2301
  }
@@ -2210,7 +2374,7 @@ function watchActivityBackfill(ctx) {
2210
2374
  cache.coldSnapshot(log.header, log.inheritedEventCount, log.events);
2211
2375
  folded++;
2212
2376
  } catch (error) {
2213
- if (isUnsupportedMigration(error)) {
2377
+ if (isUnsupportedFormat(error)) {
2214
2378
  refused++;
2215
2379
  ctx.logger.debug(`dsh-context: projection backfill skipped "${header.id}" (${messageOf(error)})`);
2216
2380
  } else ctx.logger.warn(`dsh-context: projection backfill skipped "${header.id}" (${messageOf(error)})`);
@@ -3281,6 +3445,7 @@ function detectHarnessVersion(ctx, resolveUrl = resolveRunningModule, pluginRoot
3281
3445
  const name = "dsh-context";
3282
3446
  const inject = ["sessionProjections"];
3283
3447
  function apply(ctx, config) {
3448
+ watchBalanceChannel(ctx);
3284
3449
  const harnessVersion = detectHarnessVersion(ctx);
3285
3450
  if (harnessVersion !== void 0 && !meetsBaseline(harnessVersion)) {
3286
3451
  ctx.sessionProjections.register(createFallbackTimelineDefinition(harnessVersion));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.53.3",
3
+ "version": "0.54.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": {