@mars-sea/dsh-commandcode-provider 0.11.4 → 0.11.5

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/CHANGELOG.md CHANGED
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.11.5] - 2026-09-18
10
+
11
+ ### Fixed
12
+ - **The composer session-cost figure survives dsh 0.1.6-alpha.2.** That release deleted the `data-composer-stats` marker from the shipped stats row (`StatsPills`) while leaving the row's markup otherwise identical, and the readout's pill lookup *required* that attribute — so there the lookup matched nothing on every sync and the amount silently stopped appearing beside the token pill (after three misses it logged a single console line), leaving only the usage dialog still carrying per-row prices. `STATS_ROOT` is now a PREFERENCE: when the attribute is absent the lookup falls back to the dock OUTLET (`[data-slot="conversation.composer.dock"]`), the `display:contents` div holding exactly that slot's entries — the shipped `stats` cell and ours — whose LAST dialog trigger is therefore still the token pill. The fallback is deliberately the outlet and never the outlet's PARENT: the same release moved the composer's context-usage ring out of the card into a new footer `div` beside the dock, and that ring's trigger is itself a `button[aria-haspopup="dialog"]` rendered AFTER the dock, so the parent-scoped lookup this previously depended on would have appended the cost to the context meter instead. `tests/session-cost-display.test.ts` now builds both the unmarked 0.1.6-alpha.2 row and a footer-sibling meter, and fails if the cost stops landing on the token pill or the meter becomes the host; the earlier suite constructed its own marked row and accepted a missing anchor as a no-op, which is exactly why the deletion could go dark with every check green.
13
+ - **A second composer's usage dialog can no longer be priced with this session's figures.** dsh 0.1.6-alpha.2 mounts an embedded Conversation in the sidebar, so two composers — each with its own session, dock and dialog — can be live and open at once. The dialog is portaled onto `body` and carries no id or `aria-controls` tying it back to its trigger, so the document-level lookup was exact only while a single composer was guaranteed: the per-row shape confirmation compares against THIS entry's counts, so a dialog belonging to the other session that happened to match would have been decorated with the wrong figures. `resolveDialog()` now decorates only while the document holds EXACTLY ONE `[data-session-stats-usage]` and otherwise declines — the same rule the shape check already follows one level down. Pinned by two tests: with two dialogs open neither is priced, and the survivor is priced once the other composer's dialog unmounts.
14
+
15
+ ### Added
16
+ - **dsh 0.1.6-alpha.2 is a declared-compatible engine.** The engine's other changes need no adapter work, and each was verified against the installed release rather than inferred: the Typert strict result codec's member swap (`schema` → the lazy `create()` factory) was already covered by 0.11.4's two-member codec — both the Host and Client contributions register on that release's real registry, which is worth restating because 0.1.6-alpha.2 is the first PUBLISHED engine to require `create()`; `npm run test:engine` passes against it (the bundle links and evaluates there, and the request-image policy is the durable one); every slot this plugin registers into or re-states (`conversation.composer.dock`, `main`, `sidebar.footer.action`, `settings.section`, `settings.models.provider-card`) has an unchanged declaration and the dock still supplies `sessionId`/`useProjection`/`useChat`; the platform seed table, the host packages this plugin imports and the sidebar/footer class stems are unchanged; and the release's new plugin-management page asks nothing of a plugin whose registrations are already cordis effects — which is also what its runtime unload needs. The one genuinely new surface it exposes, a sidebar-mounted embedded Conversation, is what the second fix above defends against.
17
+
9
18
  ## [0.11.4] - 2026-09-17
10
19
 
11
20
  ### Fixed
package/lib/client.js CHANGED
@@ -2157,7 +2157,7 @@ window.__ModuleLoader__.load({
2157
2157
  }
2158
2158
  //#endregion
2159
2159
  //#region package.json
2160
- var version = "0.11.4";
2160
+ var version = "0.11.5";
2161
2161
  var repository = {
2162
2162
  "type": "git",
2163
2163
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -5205,12 +5205,25 @@ window.__ModuleLoader__.load({
5205
5205
  }
5206
5206
  //#endregion
5207
5207
  //#region src/client/session-cost-display.ts
5208
- /** The shipped composer stats row (dsh-client-ui-chat `StatsPills`). */
5208
+ /**
5209
+ * The shipped composer stats row (dsh-client-ui-chat `StatsPills`).
5210
+ *
5211
+ * A PREFERENCE, not a requirement: 0.1.6-alpha.2 dropped this attribute from the
5212
+ * row's root while leaving the rest of the markup identical, so a lookup that
5213
+ * insisted on it would silently stop finding the row on that engine. When it is
5214
+ * absent the search falls back to the dock OUTLET — deliberately the outlet and
5215
+ * never the outlet's parent, because from 0.1.6-alpha.2 the parent is the new
5216
+ * composer footer, which holds the `ContextMeter` as well, and that meter's
5217
+ * trigger is itself a `button[aria-haspopup="dialog"]` rendered AFTER the dock.
5218
+ * A parent-scoped "last trigger wins" would therefore append the cost to the
5219
+ * context ring instead of the token pill.
5220
+ */
5209
5221
  const STATS_ROOT = "[data-composer-stats]";
5210
5222
  /**
5211
- * The shipped token-usage dialog's `<dl>`. Unique in the whole of
5212
- * `dsh-client-ui-chat`: the per-message turn-usage panel has its own dialog with
5213
- * different markup, so the composer's is unambiguous.
5223
+ * The shipped token-usage dialog's `<dl>`. Unique WITHIN one `StatsPills`
5224
+ * render: the per-message turn-usage panel has its own dialog with different
5225
+ * markup, so a single composer's usage dialog is unambiguous. It is not unique
5226
+ * across the document once two composers are live — see {@link resolveDialog}.
5214
5227
  */
5215
5228
  const USAGE_DIALOG = "[data-session-stats-usage]";
5216
5229
  /**
@@ -5311,7 +5324,7 @@ window.__ModuleLoader__.load({
5311
5324
  const button = this.resolvePillButton();
5312
5325
  if (button === null) {
5313
5326
  this.pillMisses += 1;
5314
- if (this.pillMisses === MISSES_BEFORE_WARNING) console.warn(`[dsh-commandcode-provider] no ${STATS_ROOT} row to append the session cost to; the figure stays in the usage dialog only`);
5327
+ if (this.pillMisses === MISSES_BEFORE_WARNING) console.warn("[dsh-commandcode-provider] no shipped token pill in this composer's dock outlet to append the session cost to; the figure stays in the usage dialog only");
5315
5328
  return;
5316
5329
  }
5317
5330
  this.pillMisses = 0;
@@ -5335,11 +5348,24 @@ window.__ModuleLoader__.load({
5335
5348
  if (this.pillA11y.textContent !== described) this.pillA11y.textContent = described;
5336
5349
  }
5337
5350
  }
5338
- /** The shipped token pill, scoped to this entry's own composer. */
5351
+ /**
5352
+ * The shipped token pill, scoped to this entry's own composer.
5353
+ *
5354
+ * The scope is the dock OUTLET (`display: contents`, one per slot), which holds
5355
+ * exactly that slot's entries — the shipped `stats` cell and ours, in their
5356
+ * registration order. {@link STATS_ROOT} narrows that to the stats row on the
5357
+ * engines that still mark it; on the engines that do not, the outlet is
5358
+ * already the narrowest correct container, because the composer footer's
5359
+ * `ContextMeter` sits BESIDE the outlet rather than inside it.
5360
+ *
5361
+ * A missing scope is "no pill", never a document-wide search: the outlet is
5362
+ * what makes this lookup per-composer, and a second composer can be live at
5363
+ * once (0.1.6-alpha.2 mounts an embedded Conversation in the sidebar).
5364
+ */
5339
5365
  resolvePillButton() {
5340
- const root = (this.scope() ?? this.doc).querySelector(STATS_ROOT);
5341
- if (root === null) return null;
5342
- const triggers = root.querySelectorAll(DIALOG_TRIGGER);
5366
+ const scope = this.scope();
5367
+ if (scope === null) return null;
5368
+ const triggers = (scope.querySelector(STATS_ROOT) ?? scope).querySelectorAll(DIALOG_TRIGGER);
5343
5369
  return triggers.length === 0 ? null : triggers[triggers.length - 1] ?? null;
5344
5370
  }
5345
5371
  /**
@@ -5462,18 +5488,30 @@ window.__ModuleLoader__.load({
5462
5488
  this.dialogPrices.delete(dd);
5463
5489
  }
5464
5490
  /**
5465
- * The shipped usage dialog, or null while it is closed.
5491
+ * The shipped usage dialog, or null while it is closed (or ambiguous).
5466
5492
  *
5467
5493
  * The dialog is portaled onto `body`, so unlike the pill it cannot be scoped
5468
- * from this entry; DSH renders one composer, and the attribute is unique in
5469
- * the chat client, so a document-level lookup is exact. Should a future build
5470
- * mount two composers at once, both dialogs would describe whichever session
5471
- * this entry belongs to — noted rather than defended against.
5494
+ * from this entry, and the attribute is unique per DIALOG rather than per
5495
+ * document. While one composer was guaranteed, a document-level lookup was
5496
+ * exact. 0.1.6-alpha.2 ends that guarantee: it mounts an embedded Conversation
5497
+ * in the sidebar, so a second composer — with its own session, its own dock
5498
+ * and its own dialog — can be live and open at the same time.
5499
+ *
5500
+ * There is no DOM link from a dialog back to its trigger (the portaled panel
5501
+ * carries no id and no `aria-controls`), so with two dialogs open document
5502
+ * order says nothing about ownership. The shape confirmation in
5503
+ * {@link dialogShapeMatches} would usually reject the stranger, but it compares
5504
+ * against THIS entry's counts, so a coincidentally matching dialog would be
5505
+ * priced with another session's figures. Ambiguity is therefore answered by
5506
+ * declining to decorate at all — the same rule the shape check already
5507
+ * follows, applied one level up: a dialog we cannot prove is ours is left
5508
+ * alone.
5472
5509
  */
5473
5510
  resolveDialog() {
5474
5511
  if (this.dialogHost?.isConnected === true) return this.dialogHost;
5475
5512
  this.clearDialog();
5476
- return this.doc.querySelector(USAGE_DIALOG);
5513
+ const dialogs = this.doc.querySelectorAll(USAGE_DIALOG);
5514
+ return dialogs.length === 1 ? dialogs[0] ?? null : null;
5477
5515
  }
5478
5516
  /** Give the dialog back: every price removed, every hidden row restored. */
5479
5517
  clearDialog() {
@@ -5544,7 +5582,17 @@ window.__ModuleLoader__.load({
5544
5582
  }
5545
5583
  //#endregion
5546
5584
  //#region src/client/session-cost-view.tsx
5547
- /** The dock outlet this entry renders inside, i.e. what it can scope itself from. */
5585
+ /**
5586
+ * The dock outlet this entry renders inside, i.e. what it scopes itself from.
5587
+ *
5588
+ * The OUTLET itself, not its parent. The outlet is one `display:contents` div
5589
+ * holding exactly this slot's entries (the shipped `stats` cell and ours), so it
5590
+ * is the narrowest container that is still per-composer. Its parent is NOT:
5591
+ * from 0.1.6-alpha.2 the composer stacks the dock inside a footer that also
5592
+ * holds the `ContextMeter`, whose trigger is another
5593
+ * `button[aria-haspopup="dialog"]` rendered after the dock — scoping to the
5594
+ * parent would let the cost land on the context ring.
5595
+ */
5548
5596
  const DOCK_ANCHOR = "[data-slot=\"conversation.composer.dock\"]";
5549
5597
  /** Module-level constant so the marker's `style` prop never diffs. */
5550
5598
  const HIDDEN_STYLE = { display: "none" };
@@ -5582,7 +5630,7 @@ window.__ModuleLoader__.load({
5582
5630
  if (typeof document === "undefined") return;
5583
5631
  const display = new SessionCostDisplay({
5584
5632
  doc: document,
5585
- scope: () => markerRef.current?.closest(DOCK_ANCHOR)?.parentElement ?? null
5633
+ scope: () => markerRef.current?.closest(DOCK_ANCHOR) ?? null
5586
5634
  });
5587
5635
  displayRef.current = display;
5588
5636
  display.start();