dsh-context 0.53.1 → 0.53.3

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.
Files changed (3) hide show
  1. package/lib/client.js +343 -343
  2. package/lib/index.js +81 -12
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -3400,11 +3400,12 @@ window.__ModuleLoader__.load({
3400
3400
  }
3401
3401
  //#endregion
3402
3402
  //#region src/client/format.ts
3403
- /** `fmt`: the k/M suffix style shared by bars/details/stats; `fmtTime`: local HH:MM:SS. */
3403
+ /** `fmt`: the k/M/B suffix style shared by bars/details/stats; `fmtTime`: local HH:MM:SS. */
3404
3404
  function fmt(n) {
3405
3405
  if (n === void 0 || n === null || isNaN(n)) return "—";
3406
3406
  const sign = n < 0 ? "-" : "";
3407
3407
  const a = Math.abs(n);
3408
+ if (a >= 1e9) return sign + (a / 1e9).toFixed(1) + "B";
3408
3409
  if (a >= 1e6) return sign + (a / 1e6).toFixed(1) + "M";
3409
3410
  if (a >= 1e3) return sign + (a / 1e3).toFixed(1) + "k";
3410
3411
  return sign + String(Math.round(a));
@@ -5293,77 +5294,67 @@ window.__ModuleLoader__.load({
5293
5294
  };
5294
5295
  }
5295
5296
  //#endregion
5296
- //#region node_modules/.pnpm/@deepseek-ai+dsh-util-workspace-path@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-workspace-path/lib/index.js
5297
- /**
5298
- * The `dsh-resource://file/…` address grammar: how a file is named across the
5299
- * Sidebar and the resource model, built and parsed without touching a
5300
- * filesystem.
5301
- * @module
5302
- */
5303
- /** The scheme and type every file address opens with. */
5304
- const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
5305
- /** Component-encode one id or path segment, keeping `:` literal for drive letters. */
5306
- function encodeSegment(segment) {
5307
- return encodeURIComponent(segment).replace(/%3A/gi, ":");
5308
- }
5309
- /** Encode a `/`-separated path segment by segment. */
5310
- function encodePath(path) {
5311
- return path.split("/").map(encodeSegment).join("/");
5312
- }
5313
- /**
5314
- * Build the address of a file read through one Session.
5315
- * @param sessionId - the Session whose Host workspace resolves the path.
5316
- * @param path - absolute or workspace-relative path; backslashes are normalized to `/`, and leading `./` prefixes are dropped.
5317
- * @returns the `dsh-resource://file/session/<sessionId>/<path>` address.
5318
- */
5319
- function sessionFileAddress(sessionId, path) {
5320
- const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
5321
- return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
5322
- }
5323
- /**
5324
- * Browser-safe Workspace path and display helpers.
5325
- * @module @deepseek-ai/dsh-util-workspace-path
5326
- */
5327
- /** Whether a path uses a Windows drive or UNC prefix. */
5328
- function isWindowsStylePath(value) {
5329
- return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
5330
- }
5331
- /**
5332
- * Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`) or Windows drive or UNC.
5333
- * @param path - the path to classify.
5334
- * @returns `true` for an absolute path; `false` for a Workspace-relative one.
5335
- */
5336
- function isAbsoluteWorkspacePath(path) {
5337
- return path.startsWith("/") || isWindowsStylePath(path);
5338
- }
5339
- /**
5340
- * Read the final non-empty segment of a Workspace path for display.
5341
- * Workspace-label surfaces use this helper instead of deriving another basename.
5342
- * @param path - Workspace directory path using POSIX or Windows separators.
5343
- * @returns the final segment, or an empty string for a separator-only path.
5344
- */
5345
- function workspaceTitleOf(path) {
5346
- const trimmed = path.replace(/[/\\]+$/, "");
5347
- const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
5348
- return trimmed.slice(separator + 1);
5297
+ //#region src/client/overviewStore.ts
5298
+ function createStore() {
5299
+ let open = false;
5300
+ const listeners = /* @__PURE__ */ new Set();
5301
+ return {
5302
+ subscribe: (listener) => {
5303
+ listeners.add(listener);
5304
+ return () => {
5305
+ listeners.delete(listener);
5306
+ };
5307
+ },
5308
+ getSnapshot: () => open,
5309
+ set: (next) => {
5310
+ if (next === open) return;
5311
+ open = next;
5312
+ for (const listener of listeners) listener();
5313
+ }
5314
+ };
5349
5315
  }
5316
+ const overviewStore = createStore();
5317
+ //#endregion
5318
+ //#region src/client/components/overviewButton.tsx
5350
5319
  /**
5351
- * The address for a path as a caller holds it: a relative path, or an absolute
5352
- * path inside the Session's workspace, becomes a `session`-scoped address; an
5353
- * absolute path outside it, or one whose workspace root is unknown, keeps its
5354
- * absolute path in that Session's address.
5355
- * @param sessionId - the Session the path is read in.
5356
- * @param cwd - that Session's workspace root, when known.
5357
- * @param path - absolute or workspace-relative path, in either separator spelling.
5358
- * @returns the `dsh-resource://file/…` address.
5320
+ * The Context Dashboard's sidebar entry: a footer action stacked directly
5321
+ * above Settings (the harness's own foot layout: footer actions, then the
5322
+ * settings row), mirroring the Settings trigger's geometry in both column
5323
+ * widths. The button carries the plugin emblem and, on the wide
5324
+ * column, its label. Clicking opens the overview overlay through the shared
5325
+ * module store (overviewStore.ts).
5326
+ *
5327
+ * The per-user `insightsEntry` preference takes the entry down (renders
5328
+ * null) without unregistering the seat. The subscription fails open: an
5329
+ * absent settings store, an unserved namespace, or a value the plugin
5330
+ * cannot understand all leave the entry visible.
5359
5331
  */
5360
- function fileAddressFor(sessionId, cwd, path) {
5361
- const normalized = path.replace(/\\/g, "/");
5362
- if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized);
5363
- const root = cwd === void 0 ? "" : cwd.replace(/\\/g, "/").replace(/\/+$/, "");
5364
- if (root !== "" && normalized === root) return sessionFileAddress(sessionId, "");
5365
- if (root !== "" && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1));
5366
- return sessionFileAddress(sessionId, normalized);
5332
+ /** Stable subscription faces for the settings-less degrade (no re-subscribes). */
5333
+ const subscribeNever = () => () => {};
5334
+ const entryShown = () => "show";
5335
+ function makeOverviewButton(kit, settings) {
5336
+ const { t } = kit;
5337
+ const subscribeEntry = settings === void 0 ? subscribeNever : (listener) => settings.store.subscribe(listener);
5338
+ const getEntry = settings === void 0 ? entryShown : () => settings.insightsEntry();
5339
+ return function OverviewButton(props) {
5340
+ if ((0, react.useSyncExternalStore)(subscribeEntry, getEntry) === "hide") return null;
5341
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5342
+ type: "button",
5343
+ className: props.wide === true ? "lc-ov-entry" : "lc-ov-entry lc-ov-entry-rail",
5344
+ title: t("ov.entry"),
5345
+ "aria-label": t("ov.entry"),
5346
+ onClick: () => {
5347
+ overviewStore.set(true);
5348
+ },
5349
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextIcon, {
5350
+ size: props.wide === true ? 16 : 18,
5351
+ className: "lc-ov-entry-icon"
5352
+ }), props.wide === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5353
+ className: "lc-ov-entry-label",
5354
+ children: t("ov.entry")
5355
+ })]
5356
+ });
5357
+ };
5367
5358
  }
5368
5359
  //#endregion
5369
5360
  //#region src/shared/providers.ts
@@ -5543,6 +5534,258 @@ window.__ModuleLoader__.load({
5543
5534
  return formatCost(amount, currency).replace(/0+$/, "").replace(/\.$/, "");
5544
5535
  }
5545
5536
  //#endregion
5537
+ //#region node_modules/.pnpm/@opencode-ai+models@0.0.72/node_modules/@opencode-ai/models/dist/error.js
5538
+ /**
5539
+ * The only error thrown by the models.dev client.
5540
+ *
5541
+ * - `Transport` — the fetch itself failed (network, DNS, abort). `cause` is the underlying error.
5542
+ * - `UnexpectedStatus` — non-2xx response. `cause` is `{ status: number }`.
5543
+ * - `MalformedResponse` — the body was empty or not valid JSON. `cause` is the parse error, if any.
5544
+ */
5545
+ var ModelsDevError = class extends Error {
5546
+ reason;
5547
+ name = "ModelsDevError";
5548
+ constructor(reason, options) {
5549
+ super(reason, options);
5550
+ this.reason = reason;
5551
+ }
5552
+ };
5553
+ //#endregion
5554
+ //#region node_modules/.pnpm/@opencode-ai+models@0.0.72/node_modules/@opencode-ai/models/dist/client.js
5555
+ /**
5556
+ * Creates a stateless models.dev client. Every method performs exactly one
5557
+ * `GET` and nothing is ever cached — callers who want caching should wrap
5558
+ * calls with their own policy. For a no-network alternative, see the
5559
+ * `@opencode-ai/models/snapshot` entrypoint.
5560
+ */
5561
+ function make(options = {}) {
5562
+ const baseUrl = options.baseUrl ?? "https://models.dev";
5563
+ const base = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
5564
+ const request = async (path, requestOptions) => {
5565
+ const fetch = options.fetch ?? globalThis.fetch;
5566
+ const headers = new Headers();
5567
+ for (const [key, value] of new Headers(options.headers)) headers.set(key, value);
5568
+ for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value);
5569
+ let response;
5570
+ try {
5571
+ response = await fetch(new URL(path, base), {
5572
+ method: "GET",
5573
+ headers,
5574
+ signal: requestOptions?.signal
5575
+ });
5576
+ } catch (cause) {
5577
+ throw new ModelsDevError("Transport", { cause });
5578
+ }
5579
+ if (!response.ok) {
5580
+ try {
5581
+ await response.body?.cancel();
5582
+ } catch {}
5583
+ throw new ModelsDevError("UnexpectedStatus", { cause: { status: response.status } });
5584
+ }
5585
+ let text;
5586
+ try {
5587
+ text = await response.text();
5588
+ } catch (cause) {
5589
+ throw new ModelsDevError("Transport", { cause });
5590
+ }
5591
+ if (text === "") throw new ModelsDevError("MalformedResponse");
5592
+ try {
5593
+ return JSON.parse(text);
5594
+ } catch (cause) {
5595
+ throw new ModelsDevError("MalformedResponse", { cause });
5596
+ }
5597
+ };
5598
+ return {
5599
+ /** All providers with their models, pricing, and limits (`/api.json`). */
5600
+ providers: (requestOptions) => request("api.json", requestOptions),
5601
+ /** Provider-agnostic model metadata (`/models.json`). */
5602
+ models: (requestOptions) => request("models.json", requestOptions),
5603
+ /** Providers and model metadata in a single request (`/catalog.json`). */
5604
+ catalog: (requestOptions) => request("catalog.json", requestOptions)
5605
+ };
5606
+ }
5607
+ //#endregion
5608
+ //#region src/client/modelPrices.ts
5609
+ /**
5610
+ * The client's model-price book (client/cost.ts prices from it): the
5611
+ * models.dev registry, fetched through the official @opencode-ai/models
5612
+ * SDK. The registry payload is untrusted wire input, so `pricesBookOf`
5613
+ * re-proves every field at the boundary — a non-conforming provider/model/
5614
+ * cost entry drops whole. The book keeps EVERY provider that prices (keyed
5615
+ * by the registry's own provider id), so a dsh provider id outside the
5616
+ * rename table still prices by direct passthrough. One fetch per page load,
5617
+ * kicked on first subscribe; a failure degrades to a visible state the cost
5618
+ * cell notes, with a backed-off automatic retry — never a spinner, and
5619
+ * never an unhandled rejection.
5620
+ */
5621
+ /** One finite non-negative registry figure, or null. */
5622
+ function rateOf(value) {
5623
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
5624
+ }
5625
+ /**
5626
+ * Extract the whole registry's per-model USD rates from a delivered
5627
+ * providers payload (`/api.json`), keyed by the registry's provider ids.
5628
+ * Any shape failure skips just that entry; a payload that is not a record
5629
+ * at all returns null (the store treats it as a failed fetch and retries).
5630
+ */
5631
+ function pricesBookOf(value) {
5632
+ const data = asRecord(value);
5633
+ if (data === null || Array.isArray(data)) return null;
5634
+ const book = {};
5635
+ for (const providerId of Object.keys(data)) {
5636
+ const provider = asRecord(data[providerId]);
5637
+ const modelList = provider !== null ? asRecord(provider.models) : null;
5638
+ if (modelList === null) continue;
5639
+ const models = {};
5640
+ for (const id of Object.keys(modelList)) {
5641
+ const model = asRecord(modelList[id]);
5642
+ const cost = model !== null ? asRecord(model.cost) : null;
5643
+ if (cost === null) continue;
5644
+ const miss = rateOf(cost.input);
5645
+ const out = rateOf(cost.output);
5646
+ if (miss === null || out === null) continue;
5647
+ models[id] = {
5648
+ hit: rateOf(cost.cache_read) ?? miss,
5649
+ miss,
5650
+ write: rateOf(cost.cache_write) ?? miss,
5651
+ out
5652
+ };
5653
+ }
5654
+ if (Object.keys(models).length > 0) book[providerId] = models;
5655
+ }
5656
+ return book;
5657
+ }
5658
+ const defaultLoader = () => make().providers();
5659
+ /** The retry backoff base; each consecutive failure doubles the wait, capped at 3 doublings. */
5660
+ const RETRY_BASE_MS = 3e4;
5661
+ let loader = defaultLoader;
5662
+ let snap = {
5663
+ prices: null,
5664
+ failed: false
5665
+ };
5666
+ let inFlight = false;
5667
+ let failures = 0;
5668
+ let timer = null;
5669
+ const listeners = /* @__PURE__ */ new Set();
5670
+ function fail() {
5671
+ snap = {
5672
+ ...snap,
5673
+ failed: true
5674
+ };
5675
+ failures++;
5676
+ timer = setTimeout(() => {
5677
+ timer = null;
5678
+ fire();
5679
+ }, RETRY_BASE_MS * 2 ** Math.min(failures - 1, 3));
5680
+ }
5681
+ async function fire() {
5682
+ inFlight = true;
5683
+ try {
5684
+ const book = pricesBookOf(await loader());
5685
+ if (book !== null) {
5686
+ snap = {
5687
+ prices: book,
5688
+ failed: false
5689
+ };
5690
+ failures = 0;
5691
+ } else fail();
5692
+ } catch {
5693
+ fail();
5694
+ }
5695
+ inFlight = false;
5696
+ for (const fn of listeners) fn();
5697
+ }
5698
+ function kick() {
5699
+ if (snap.prices !== null || inFlight || timer !== null) return;
5700
+ fire();
5701
+ }
5702
+ /** The useSyncExternalStore seam: subscribing also kicks the first fetch. */
5703
+ const subscribeModelPrices = (fn) => {
5704
+ listeners.add(fn);
5705
+ kick();
5706
+ return () => {
5707
+ listeners.delete(fn);
5708
+ };
5709
+ };
5710
+ const getModelPricesSnap = () => snap;
5711
+ /** The stats board's read of the price book (null until the fetch lands). */
5712
+ function useModelPrices() {
5713
+ return (0, react.useSyncExternalStore)(subscribeModelPrices, getModelPricesSnap);
5714
+ }
5715
+ //#endregion
5716
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-workspace-path@0.1.5-rc.1_@deepseek-ai+cordis@4.0.2/node_modules/@deepseek-ai/dsh-util-workspace-path/lib/index.js
5717
+ /**
5718
+ * The `dsh-resource://file/…` address grammar: how a file is named across the
5719
+ * Sidebar and the resource model, built and parsed without touching a
5720
+ * filesystem.
5721
+ * @module
5722
+ */
5723
+ /** The scheme and type every file address opens with. */
5724
+ const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
5725
+ /** Component-encode one id or path segment, keeping `:` literal for drive letters. */
5726
+ function encodeSegment(segment) {
5727
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
5728
+ }
5729
+ /** Encode a `/`-separated path segment by segment. */
5730
+ function encodePath(path) {
5731
+ return path.split("/").map(encodeSegment).join("/");
5732
+ }
5733
+ /**
5734
+ * Build the address of a file read through one Session.
5735
+ * @param sessionId - the Session whose Host workspace resolves the path.
5736
+ * @param path - absolute or workspace-relative path; backslashes are normalized to `/`, and leading `./` prefixes are dropped.
5737
+ * @returns the `dsh-resource://file/session/<sessionId>/<path>` address.
5738
+ */
5739
+ function sessionFileAddress(sessionId, path) {
5740
+ const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
5741
+ return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
5742
+ }
5743
+ /**
5744
+ * Browser-safe Workspace path and display helpers.
5745
+ * @module @deepseek-ai/dsh-util-workspace-path
5746
+ */
5747
+ /** Whether a path uses a Windows drive or UNC prefix. */
5748
+ function isWindowsStylePath(value) {
5749
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
5750
+ }
5751
+ /**
5752
+ * Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`) or Windows drive or UNC.
5753
+ * @param path - the path to classify.
5754
+ * @returns `true` for an absolute path; `false` for a Workspace-relative one.
5755
+ */
5756
+ function isAbsoluteWorkspacePath(path) {
5757
+ return path.startsWith("/") || isWindowsStylePath(path);
5758
+ }
5759
+ /**
5760
+ * Read the final non-empty segment of a Workspace path for display.
5761
+ * Workspace-label surfaces use this helper instead of deriving another basename.
5762
+ * @param path - Workspace directory path using POSIX or Windows separators.
5763
+ * @returns the final segment, or an empty string for a separator-only path.
5764
+ */
5765
+ function workspaceTitleOf(path) {
5766
+ const trimmed = path.replace(/[/\\]+$/, "");
5767
+ const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
5768
+ return trimmed.slice(separator + 1);
5769
+ }
5770
+ /**
5771
+ * The address for a path as a caller holds it: a relative path, or an absolute
5772
+ * path inside the Session's workspace, becomes a `session`-scoped address; an
5773
+ * absolute path outside it, or one whose workspace root is unknown, keeps its
5774
+ * absolute path in that Session's address.
5775
+ * @param sessionId - the Session the path is read in.
5776
+ * @param cwd - that Session's workspace root, when known.
5777
+ * @param path - absolute or workspace-relative path, in either separator spelling.
5778
+ * @returns the `dsh-resource://file/…` address.
5779
+ */
5780
+ function fileAddressFor(sessionId, cwd, path) {
5781
+ const normalized = path.replace(/\\/g, "/");
5782
+ if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized);
5783
+ const root = cwd === void 0 ? "" : cwd.replace(/\\/g, "/").replace(/\/+$/, "");
5784
+ if (root !== "" && normalized === root) return sessionFileAddress(sessionId, "");
5785
+ if (root !== "" && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1));
5786
+ return sessionFileAddress(sessionId, normalized);
5787
+ }
5788
+ //#endregion
5546
5789
  //#region src/client/overview.ts
5547
5790
  /**
5548
5791
  * The Context Dashboard's data layer: everything the panel renders is derived
@@ -5576,21 +5819,6 @@ window.__ModuleLoader__.load({
5576
5819
  return null;
5577
5820
  }
5578
5821
  }
5579
- /** The running-session tally for the footer entry's badge (0 on any shape failure). */
5580
- function runningCountOf(snapshot) {
5581
- const state = asRecord(snapshot);
5582
- if (state === null) return 0;
5583
- const byId = asRecord(state.byId);
5584
- if (byId === null) return 0;
5585
- let count = 0;
5586
- try {
5587
- for (const id of Object.keys(byId)) {
5588
- const row = asRecord(byId[id]);
5589
- if (row !== null && row.running === true) count++;
5590
- }
5591
- } catch {}
5592
- return count;
5593
- }
5594
5822
  /**
5595
5823
  * The `useWorkspaces` standard prop read — same guarded-hook contract as
5596
5824
  * {@link sessionsSnapshotOf}. The overview joins its session → workspace
@@ -5857,7 +6085,12 @@ window.__ModuleLoader__.load({
5857
6085
  let toolsMs = 0;
5858
6086
  let calls = 0;
5859
6087
  let wallMs = 0;
6088
+ let costSessions = 0;
6089
+ let usageSessions = 0;
5860
6090
  for (const row of rows) {
6091
+ const cost = row.timeline?.cost;
6092
+ if (estimateSessionCost(cost, prices, currency) !== null) costSessions++;
6093
+ if (usageTotalsOf(cost) !== null) usageSessions++;
5861
6094
  turns += turnsOf(row.timeline);
5862
6095
  const timing = row.timeline?.timing;
5863
6096
  toolCalls += timing?.toolCalls ?? 0;
@@ -5871,7 +6104,9 @@ window.__ModuleLoader__.load({
5871
6104
  tokens: totals?.total ?? 0,
5872
6105
  turns,
5873
6106
  cost: estimateSessionCost(usage, prices, currency),
6107
+ costSessions,
5874
6108
  cacheHit: totals === null ? null : cacheHitPercent(totals.cacheRead, totals.input + totals.cacheRead + totals.cacheWrite),
6109
+ usageSessions,
5875
6110
  toolCalls,
5876
6111
  toolsMs,
5877
6112
  calls,
@@ -5933,6 +6168,20 @@ window.__ModuleLoader__.load({
5933
6168
  Promise.resolve(sessions.refresh()).catch(() => {});
5934
6169
  } catch {}
5935
6170
  }
6171
+ const BACKFILL_ROUTE = "/api/dsh-context/backfill";
6172
+ /**
6173
+ * Summon the host's projection warm-up (host/backfill.ts): the dashboard is
6174
+ * the rows' only reader, so the host defers the corpus-wide cold reads until
6175
+ * this surface first opens (one pass per host process — later opens no-op
6176
+ * server-side, and the host answers at once). Fire-and-forget: an older host
6177
+ * without the route, or a transport hiccup, just leaves the panel on the
6178
+ * rows it already has.
6179
+ */
6180
+ function requestActivityBackfill() {
6181
+ try {
6182
+ fetch(BACKFILL_ROUTE, { method: "POST" }).catch(() => {});
6183
+ } catch {}
6184
+ }
5936
6185
  /**
5937
6186
  * The row's relative-activity label ("3m ago"), unit-stepped: under a
5938
6187
  * minute reads "just now", then minutes, hours, days. A future or invalid
@@ -5948,258 +6197,6 @@ window.__ModuleLoader__.load({
5948
6197
  return t("ov.time.d", { n: Math.floor(hours / 24) });
5949
6198
  }
5950
6199
  //#endregion
5951
- //#region src/client/overviewStore.ts
5952
- function createStore() {
5953
- let open = false;
5954
- const listeners = /* @__PURE__ */ new Set();
5955
- return {
5956
- subscribe: (listener) => {
5957
- listeners.add(listener);
5958
- return () => {
5959
- listeners.delete(listener);
5960
- };
5961
- },
5962
- getSnapshot: () => open,
5963
- set: (next) => {
5964
- if (next === open) return;
5965
- open = next;
5966
- for (const listener of listeners) listener();
5967
- }
5968
- };
5969
- }
5970
- const overviewStore = createStore();
5971
- //#endregion
5972
- //#region src/client/components/overviewButton.tsx
5973
- /**
5974
- * The Context Dashboard's sidebar entry: a footer action stacked directly
5975
- * above Settings (the harness's own foot layout: footer actions, then the
5976
- * settings row), mirroring the Settings trigger's geometry in both column
5977
- * widths. The button carries the plugin emblem and, on the wide
5978
- * column, its label; a badge counts the sessions currently running (the one
5979
- * glanceable live fact a footer can carry). Clicking opens the overview
5980
- * overlay through the shared module store (overviewStore.ts).
5981
- *
5982
- * The per-user `insightsEntry` preference takes the entry down (renders
5983
- * null) without unregistering the seat. The subscription fails open: an
5984
- * absent settings store, an unserved namespace, or a value the plugin
5985
- * cannot understand all leave the entry visible.
5986
- */
5987
- /** Stable subscription faces for the settings-less degrade (no re-subscribes). */
5988
- const subscribeNever = () => () => {};
5989
- const entryShown = () => "show";
5990
- function makeOverviewButton(kit, settings) {
5991
- const { t } = kit;
5992
- const subscribeEntry = settings === void 0 ? subscribeNever : (listener) => settings.store.subscribe(listener);
5993
- const getEntry = settings === void 0 ? entryShown : () => settings.insightsEntry();
5994
- return function OverviewButton(props) {
5995
- const running = runningCountOf(sessionsSnapshotOf(props));
5996
- if ((0, react.useSyncExternalStore)(subscribeEntry, getEntry) === "hide") return null;
5997
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5998
- type: "button",
5999
- className: props.wide === true ? "lc-ov-entry" : "lc-ov-entry lc-ov-entry-rail",
6000
- title: t("ov.entry"),
6001
- "aria-label": t("ov.entry"),
6002
- onClick: () => {
6003
- overviewStore.set(true);
6004
- },
6005
- children: [
6006
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextIcon, {
6007
- size: props.wide === true ? 16 : 18,
6008
- className: "lc-ov-entry-icon"
6009
- }),
6010
- props.wide === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6011
- className: "lc-ov-entry-label",
6012
- children: t("ov.entry")
6013
- }),
6014
- running > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6015
- className: "lc-ov-badge",
6016
- "aria-hidden": "true",
6017
- children: running
6018
- })
6019
- ]
6020
- });
6021
- };
6022
- }
6023
- //#endregion
6024
- //#region node_modules/.pnpm/@opencode-ai+models@0.0.72/node_modules/@opencode-ai/models/dist/error.js
6025
- /**
6026
- * The only error thrown by the models.dev client.
6027
- *
6028
- * - `Transport` — the fetch itself failed (network, DNS, abort). `cause` is the underlying error.
6029
- * - `UnexpectedStatus` — non-2xx response. `cause` is `{ status: number }`.
6030
- * - `MalformedResponse` — the body was empty or not valid JSON. `cause` is the parse error, if any.
6031
- */
6032
- var ModelsDevError = class extends Error {
6033
- reason;
6034
- name = "ModelsDevError";
6035
- constructor(reason, options) {
6036
- super(reason, options);
6037
- this.reason = reason;
6038
- }
6039
- };
6040
- //#endregion
6041
- //#region node_modules/.pnpm/@opencode-ai+models@0.0.72/node_modules/@opencode-ai/models/dist/client.js
6042
- /**
6043
- * Creates a stateless models.dev client. Every method performs exactly one
6044
- * `GET` and nothing is ever cached — callers who want caching should wrap
6045
- * calls with their own policy. For a no-network alternative, see the
6046
- * `@opencode-ai/models/snapshot` entrypoint.
6047
- */
6048
- function make(options = {}) {
6049
- const baseUrl = options.baseUrl ?? "https://models.dev";
6050
- const base = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
6051
- const request = async (path, requestOptions) => {
6052
- const fetch = options.fetch ?? globalThis.fetch;
6053
- const headers = new Headers();
6054
- for (const [key, value] of new Headers(options.headers)) headers.set(key, value);
6055
- for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value);
6056
- let response;
6057
- try {
6058
- response = await fetch(new URL(path, base), {
6059
- method: "GET",
6060
- headers,
6061
- signal: requestOptions?.signal
6062
- });
6063
- } catch (cause) {
6064
- throw new ModelsDevError("Transport", { cause });
6065
- }
6066
- if (!response.ok) {
6067
- try {
6068
- await response.body?.cancel();
6069
- } catch {}
6070
- throw new ModelsDevError("UnexpectedStatus", { cause: { status: response.status } });
6071
- }
6072
- let text;
6073
- try {
6074
- text = await response.text();
6075
- } catch (cause) {
6076
- throw new ModelsDevError("Transport", { cause });
6077
- }
6078
- if (text === "") throw new ModelsDevError("MalformedResponse");
6079
- try {
6080
- return JSON.parse(text);
6081
- } catch (cause) {
6082
- throw new ModelsDevError("MalformedResponse", { cause });
6083
- }
6084
- };
6085
- return {
6086
- /** All providers with their models, pricing, and limits (`/api.json`). */
6087
- providers: (requestOptions) => request("api.json", requestOptions),
6088
- /** Provider-agnostic model metadata (`/models.json`). */
6089
- models: (requestOptions) => request("models.json", requestOptions),
6090
- /** Providers and model metadata in a single request (`/catalog.json`). */
6091
- catalog: (requestOptions) => request("catalog.json", requestOptions)
6092
- };
6093
- }
6094
- //#endregion
6095
- //#region src/client/modelPrices.ts
6096
- /**
6097
- * The client's model-price book (client/cost.ts prices from it): the
6098
- * models.dev registry, fetched through the official @opencode-ai/models
6099
- * SDK. The registry payload is untrusted wire input, so `pricesBookOf`
6100
- * re-proves every field at the boundary — a non-conforming provider/model/
6101
- * cost entry drops whole. The book keeps EVERY provider that prices (keyed
6102
- * by the registry's own provider id), so a dsh provider id outside the
6103
- * rename table still prices by direct passthrough. One fetch per page load,
6104
- * kicked on first subscribe; a failure degrades to a visible state the cost
6105
- * cell notes, with a backed-off automatic retry — never a spinner, and
6106
- * never an unhandled rejection.
6107
- */
6108
- /** One finite non-negative registry figure, or null. */
6109
- function rateOf(value) {
6110
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
6111
- }
6112
- /**
6113
- * Extract the whole registry's per-model USD rates from a delivered
6114
- * providers payload (`/api.json`), keyed by the registry's provider ids.
6115
- * Any shape failure skips just that entry; a payload that is not a record
6116
- * at all returns null (the store treats it as a failed fetch and retries).
6117
- */
6118
- function pricesBookOf(value) {
6119
- const data = asRecord(value);
6120
- if (data === null || Array.isArray(data)) return null;
6121
- const book = {};
6122
- for (const providerId of Object.keys(data)) {
6123
- const provider = asRecord(data[providerId]);
6124
- const modelList = provider !== null ? asRecord(provider.models) : null;
6125
- if (modelList === null) continue;
6126
- const models = {};
6127
- for (const id of Object.keys(modelList)) {
6128
- const model = asRecord(modelList[id]);
6129
- const cost = model !== null ? asRecord(model.cost) : null;
6130
- if (cost === null) continue;
6131
- const miss = rateOf(cost.input);
6132
- const out = rateOf(cost.output);
6133
- if (miss === null || out === null) continue;
6134
- models[id] = {
6135
- hit: rateOf(cost.cache_read) ?? miss,
6136
- miss,
6137
- write: rateOf(cost.cache_write) ?? miss,
6138
- out
6139
- };
6140
- }
6141
- if (Object.keys(models).length > 0) book[providerId] = models;
6142
- }
6143
- return book;
6144
- }
6145
- const defaultLoader = () => make().providers();
6146
- /** The retry backoff base; each consecutive failure doubles the wait, capped at 3 doublings. */
6147
- const RETRY_BASE_MS = 3e4;
6148
- let loader = defaultLoader;
6149
- let snap = {
6150
- prices: null,
6151
- failed: false
6152
- };
6153
- let inFlight = false;
6154
- let failures = 0;
6155
- let timer = null;
6156
- const listeners = /* @__PURE__ */ new Set();
6157
- function fail() {
6158
- snap = {
6159
- ...snap,
6160
- failed: true
6161
- };
6162
- failures++;
6163
- timer = setTimeout(() => {
6164
- timer = null;
6165
- fire();
6166
- }, RETRY_BASE_MS * 2 ** Math.min(failures - 1, 3));
6167
- }
6168
- async function fire() {
6169
- inFlight = true;
6170
- try {
6171
- const book = pricesBookOf(await loader());
6172
- if (book !== null) {
6173
- snap = {
6174
- prices: book,
6175
- failed: false
6176
- };
6177
- failures = 0;
6178
- } else fail();
6179
- } catch {
6180
- fail();
6181
- }
6182
- inFlight = false;
6183
- for (const fn of listeners) fn();
6184
- }
6185
- function kick() {
6186
- if (snap.prices !== null || inFlight || timer !== null) return;
6187
- fire();
6188
- }
6189
- /** The useSyncExternalStore seam: subscribing also kicks the first fetch. */
6190
- const subscribeModelPrices = (fn) => {
6191
- listeners.add(fn);
6192
- kick();
6193
- return () => {
6194
- listeners.delete(fn);
6195
- };
6196
- };
6197
- const getModelPricesSnap = () => snap;
6198
- /** The stats board's read of the price book (null until the fetch lands). */
6199
- function useModelPrices() {
6200
- return (0, react.useSyncExternalStore)(subscribeModelPrices, getModelPricesSnap);
6201
- }
6202
- //#endregion
6203
6200
  //#region src/shared/days.ts
6204
6201
  /**
6205
6202
  * The local-day key (`YYYY-MM-DD`) shared by the activity fold (host) and the
@@ -6685,7 +6682,10 @@ window.__ModuleLoader__.load({
6685
6682
  const rows = (0, react.useMemo)(() => rowsOfSnapshot(snapshot, wsSnapshot), [snapshot, wsSnapshot]);
6686
6683
  const groups = (0, react.useMemo)(() => sessionGroupsOf(wsSnapshot), [wsSnapshot]);
6687
6684
  (0, react.useEffect)(() => {
6688
- if (open) refreshSessions(ctx);
6685
+ if (open) {
6686
+ requestActivityBackfill();
6687
+ refreshSessions(ctx);
6688
+ }
6689
6689
  }, [open]);
6690
6690
  (0, react.useEffect)(() => {
6691
6691
  setPage(0);
@@ -6824,7 +6824,7 @@ window.__ModuleLoader__.load({
6824
6824
  }),
6825
6825
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6826
6826
  className: "lc-stat-sub",
6827
- children: t("ov.kpi.sessionsSub", { n: kpi.sessions })
6827
+ children: t("ov.kpi.sessionsSub", { n: kpi.costSessions })
6828
6828
  })
6829
6829
  ]
6830
6830
  }),
@@ -6841,7 +6841,7 @@ window.__ModuleLoader__.load({
6841
6841
  }),
6842
6842
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6843
6843
  className: "lc-stat-sub",
6844
- children: t("ov.kpi.sessionsSub", { n: kpi.sessions })
6844
+ children: t("ov.kpi.sessionsSub", { n: kpi.usageSessions })
6845
6845
  })
6846
6846
  ]
6847
6847
  }),
@@ -9492,7 +9492,7 @@ window.__ModuleLoader__.load({
9492
9492
  return function PluginInfo() {
9493
9493
  const [latest, setLatest] = (0, react.useState)(null);
9494
9494
  (0, react.useEffect)(() => {
9495
- if ("0.53.1".includes("-dev")) return;
9495
+ if ("0.53.3".includes("-dev")) return;
9496
9496
  let on = true;
9497
9497
  fetchLatestVersion().then((v) => {
9498
9498
  if (on && v) setLatest(v);
@@ -9501,8 +9501,8 @@ window.__ModuleLoader__.load({
9501
9501
  on = false;
9502
9502
  };
9503
9503
  }, []);
9504
- const update = latest !== null && isNewerVersion(latest, "0.53.1") ? latest : null;
9505
- const nameText = "dsh-context (v0.53.1)";
9504
+ const update = latest !== null && isNewerVersion(latest, "0.53.3") ? latest : null;
9505
+ const nameText = "dsh-context (v0.53.3)";
9506
9506
  const nameValue = [nameText];
9507
9507
  if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9508
9508
  className: "lc-pi-update",
@@ -11348,7 +11348,7 @@ window.__ModuleLoader__.load({
11348
11348
  }
11349
11349
  //#endregion
11350
11350
  //#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;position:relative;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-badge{box-sizing:border-box;background:var(--color-green-500);color:#fff;pointer-events:none;border-radius:999px;justify-content:center;align-items:center;min-width:15px;height:15px;padding:0 4px;font-size:9px;font-weight:700;line-height:1;display:flex;position:absolute;top:1px;right:4px}.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}";
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}";
11352
11352
  const tagId = "dsh-context/overview.css";
11353
11353
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
11354
11354
  const tag = document.createElement("style");
package/lib/index.js CHANGED
@@ -870,7 +870,7 @@ function removeSurfaceSeqs(st, claimed, goneSeq) {
870
870
  * rather than through `deriveEventMessage`, whose 0.1.2-rc.1 generation knows
871
871
  * nothing of the V3 `system/message` variant. A malformed payload reads null.
872
872
  */
873
- function messageOf(data) {
873
+ function messageOf$1(data) {
874
874
  const message = data?.message;
875
875
  return message !== null && typeof message === "object" ? message : null;
876
876
  }
@@ -1230,7 +1230,7 @@ function applyTimeline(state, event, bounds) {
1230
1230
  pushSystem(s, {
1231
1231
  seq: event.seq,
1232
1232
  time: event.time,
1233
- tokens: estimateSystemContent(messageOf(data)?.content)
1233
+ tokens: estimateSystemContent(messageOf$1(data)?.content)
1234
1234
  });
1235
1235
  break;
1236
1236
  }
@@ -2058,8 +2058,17 @@ function createToolAttribution(ctx) {
2058
2058
  }
2059
2059
  //#endregion
2060
2060
  //#region src/host/backfill.ts
2061
- /** Inter-session pacing so a multi-hundred-session backfill never starve the host. */
2062
- const YIELD_MS = 10;
2061
+ /** The plugin's warm-up trigger route, under the authenticated `/api` fence. */
2062
+ const BACKFILL_ROUTE = "/api/dsh-context/backfill";
2063
+ /**
2064
+ * Inter-session pacing so a multi-hundred-session backfill never starve the
2065
+ * host: each cold read is a full durable-log decode, and a boot-time host
2066
+ * under sustained read pressure stalls its own client handshake (the
2067
+ * workspace sidebar rides it), so the pass breathes between sessions.
2068
+ */
2069
+ const YIELD_MS = 100;
2070
+ /** The `name` dsh raises for a durable log its migration surface refuses. */
2071
+ const UNSUPPORTED_MIGRATION = "SessionFormatUnsupportedMigrationError";
2063
2072
  function asRecord(value) {
2064
2073
  return value !== null && typeof value === "object" ? value : null;
2065
2074
  }
@@ -2114,11 +2123,63 @@ function isLive(sessions, id) {
2114
2123
  }
2115
2124
  }
2116
2125
  /**
2117
- * Arm the warm-up on whichever deployment composes the four cold-path
2118
- * services; returns the deferred inject's disposer (abort on unload).
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.
2131
+ */
2132
+ function isUnsupportedMigration(error) {
2133
+ try {
2134
+ return asRecord(error)?.name === UNSUPPORTED_MIGRATION;
2135
+ } catch {
2136
+ return false;
2137
+ }
2138
+ }
2139
+ /** The error's printable form — a hostile error may throw on its own toString. */
2140
+ function messageOf(error) {
2141
+ try {
2142
+ return String(error);
2143
+ } catch {
2144
+ return "unprintable error";
2145
+ }
2146
+ }
2147
+ /**
2148
+ * Arm the warm-up behind its trigger route. The route flips `requested`
2149
+ * when the dashboard first opens; the cold-path faces compose `launch`.
2150
+ * Either half may land first; the first request after both launches the one
2151
+ * pass, and every later request is a no-op. Returns the deferred injects'
2152
+ * disposer (abort on unload).
2119
2153
  */
2120
2154
  function watchActivityBackfill(ctx) {
2121
- const handle = ctx.inject([
2155
+ let requested = false;
2156
+ let started = false;
2157
+ let launch = null;
2158
+ const tryLaunch = () => {
2159
+ if (!requested || started || launch === null) return;
2160
+ started = true;
2161
+ launch();
2162
+ };
2163
+ const routeFiber = ctx.inject(["connection"], (c) => {
2164
+ const connection = c.get("connection");
2165
+ const register = typeof connection?.fetch?.register === "function" ? connection.fetch.register.bind(connection.fetch) : void 0;
2166
+ if (register === void 0) return;
2167
+ try {
2168
+ c.effect(() => register({
2169
+ path: BACKFILL_ROUTE,
2170
+ methods: ["POST"],
2171
+ requestBody: "buffered",
2172
+ fetch: () => {
2173
+ requested = true;
2174
+ tryLaunch();
2175
+ return Response.json({ ok: true }, { headers: { "cache-control": "no-store" } });
2176
+ }
2177
+ }), "dsh-context: backfill route");
2178
+ } catch {
2179
+ return;
2180
+ }
2181
+ });
2182
+ const runnerFiber = ctx.inject([
2122
2183
  "sessionQuery",
2123
2184
  "sessionProjectionCache",
2124
2185
  "sessionPersistence",
@@ -2137,6 +2198,7 @@ function watchActivityBackfill(ctx) {
2137
2198
  const listed = await query.listSessions(abort.signal);
2138
2199
  if (!Array.isArray(listed)) return;
2139
2200
  let folded = 0;
2201
+ let refused = 0;
2140
2202
  for (const record of listed) {
2141
2203
  if (abort.signal.aborted) return;
2142
2204
  const header = headerOf(record);
@@ -2148,21 +2210,28 @@ function watchActivityBackfill(ctx) {
2148
2210
  cache.coldSnapshot(log.header, log.inheritedEventCount, log.events);
2149
2211
  folded++;
2150
2212
  } catch (error) {
2151
- ctx.logger.warn(`dsh-context: projection backfill skipped "${header.id}" (${String(error)})`);
2213
+ if (isUnsupportedMigration(error)) {
2214
+ refused++;
2215
+ ctx.logger.debug(`dsh-context: projection backfill skipped "${header.id}" (${messageOf(error)})`);
2216
+ } else ctx.logger.warn(`dsh-context: projection backfill skipped "${header.id}" (${messageOf(error)})`);
2152
2217
  }
2153
2218
  await new Promise((resolve) => setTimeout(resolve, YIELD_MS));
2154
2219
  }
2155
2220
  if (folded > 0) ctx.logger.info(`dsh-context: projection rows backfilled for ${folded} session(s)`);
2221
+ if (refused > 0) ctx.logger.info(`dsh-context: projection backfill skipped ${refused} session(s) (legacy log format is not migratable; sources left unchanged)`);
2156
2222
  };
2157
- run().catch((error) => {
2158
- if (!abort.signal.aborted) ctx.logger.warn(`dsh-context: projection backfill stopped early (${String(error)})`);
2159
- });
2223
+ launch = () => {
2224
+ run().catch((error) => {
2225
+ if (!abort.signal.aborted) ctx.logger.warn(`dsh-context: projection backfill stopped early (${messageOf(error)})`);
2226
+ });
2227
+ };
2228
+ tryLaunch();
2160
2229
  return () => {
2161
2230
  abort.abort();
2162
2231
  };
2163
2232
  });
2164
2233
  return () => {
2165
- handle?.dispose?.();
2234
+ for (const fiber of [routeFiber, runnerFiber]) fiber?.dispose?.();
2166
2235
  };
2167
2236
  }
2168
2237
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.53.1",
3
+ "version": "0.53.3",
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": {