dsh-usage-state 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -22,7 +22,8 @@ hover any segment: Source DeepSeek · Mode API balance · Granted 0 ·
22
22
 
23
23
  ## Install
24
24
 
25
- **Requirements**: DSH `0.1.5-rc.2` through `0.2.x`, Node ≥ 20, installed into the `web` profile. The package **ships the prebuilt `lib/`**, so installation has no build step.
25
+ **Requirements**: DSH `0.2.0-rc.2` or a later 0.2 release, Node ≥ 20, installed into the `web` profile. The package **ships the prebuilt `lib/`**, so installation has no build step.
26
+ **On the 0.1 line install `0.3.2` instead**: DSH 0.2 replaced the whole settings API (`settingsScope` / `settings.register` → `configForms` / the plugin's own `Config`), and the two share no surface, so `0.4.0` and up support the 0.2 line only (see revision 17 of [`docs/design-consensus.md`](docs/design-consensus.md)).
26
27
 
27
28
  ```bash
28
29
  # 1) install (npm package)
@@ -98,9 +99,11 @@ Expanding **Advanced** lets you override the source and endpoint, name a credent
98
99
 
99
100
  ## Compatibility
100
101
 
101
- - Verified against DSH `0.1.5-rc.2` and `0.2.0-rc.2`, Node ≥ 20. The DSH requirement is declared as `>=0.1.5-rc.1 <0.3.0-0` through `engines.dsh` — the whole 0.1 line from 0.1.5 up, plus the 0.2 line (this is what dsh-market's compatibility badge and install gate read).
102
+ - **0.2 line only**: `engines.dsh` = `>=0.2.0-rc.2 <0.3.0-0` (this is what dsh-market's badge and install gate read). On an earlier host, install `0.3.2`.
103
+ - `peerDependencies` carry `@deepseek-ai/dsh-settings` (`^0.2.0-rc.2`) and `@deepseek-ai/schemastery` (`^3.18.2`). The first one is deliberate: the runtime install gate only reads `@deepseek-ai/dsh*` peers, so declaring it makes a 0.1.x host **refuse the install** instead of accepting it and then failing to boot. Both are platform-provided; pnpm may print a `missing peer` warning for them, which is expected.
104
+ - Version `0.4.0` migrates to the 0.2 settings model (`Config` + `configForms`, with cross-entry config read through `configEditor`). Verification and results: [`docs/implementation.md`](docs/implementation.md) §10; rationale: [`docs/design-consensus.md`](docs/design-consensus.md) revision 17.
105
+ - Version `0.3.2` (last of the 0.1 line) only widened `engines.dsh`; **its claimed 0.2 compatibility was wrong** — on `0.2.0-rc.2` it fails the entire web boot because `settingsScope` does not exist there.
102
106
  - Published to npm as [`dsh-usage-state`](https://www.npmjs.com/package/dsh-usage-state); GitHub installs work too.
103
- - Version `0.3.2`: 0.2-line compatibility review (no code change — only `engines.dsh` was widened); DeepSeek, z.ai and OpenCode Zen Go are verified against live accounts; see the limitations below.
104
107
 
105
108
  ## Limitations
106
109
 
package/README.md CHANGED
@@ -24,7 +24,8 @@
24
24
 
25
25
  ## 安装
26
26
 
27
- **前置条件**:DSH `0.1.5-rc.2` ~ `0.2.x`、Node ≥ 20,装进 `web` profile。包内**自带预构建的 `lib/`**,安装时没有构建步骤。
27
+ **前置条件**:DSH `0.2.0-rc.2` 或更新的 0.2 线、Node ≥ 20,装进 `web` profile。包内**自带预构建的 `lib/`**,安装时没有构建步骤。
28
+ **用 0.1 线请装 `0.3.2`**:DSH 0.2 换掉了整套设置 API(`settingsScope` / `settings.register` → `configForms` / 插件自带 `Config`),两套 API 没有重叠,所以 `0.4.0` 起只支持 0.2 线(原因见 [`docs/design-consensus.md`](docs/design-consensus.md) 修订 17)。
28
29
 
29
30
  ```bash
30
31
  # 1) 安装(npm 包)
@@ -122,9 +123,11 @@ dsh plugin --profile web add /path/to/dsh-usage-state
122
123
 
123
124
  ## 兼容性
124
125
 
125
- - 实测环境:DSH `0.1.5-rc.2` 与 `0.2.0-rc.2`,Node ≥ 20。DSH 版本要求通过 `engines.dsh` 声明为 `>=0.1.5-rc.1 <0.3.0-0`,即 **0.1.5 起的整条 0.1 线与 0.2 线**(dsh-market 的兼容徽标与"可安装"判定读的就是它)。
126
+ - **只支持 DSH 0.2 线**:`engines.dsh` = `>=0.2.0-rc.2 <0.3.0-0`(市场读它做徽标与"可安装"判定)。0.2 之前的宿主请装 `0.3.2`。
127
+ - `peerDependencies` 里有 `@deepseek-ai/dsh-settings`(`^0.2.0-rc.2`)与 `@deepseek-ai/schemastery`(`^3.18.2`):前者是**故意的**——平台的运行时安装闸门只读 `@deepseek-ai/dsh*` 的 peer,声明它可以让 0.1.x 宿主在**安装时**就拒绝,而不是装上之后把启动搞崩;两者都由平台提供,pnpm 可能为此打一行 `missing peer` 警告,属预期。
128
+ - 版本 `0.4.0`:迁到 0.2 原生设置模型(`Config` + `configForms`,宿主侧 `configEditor` 读跨条目配置)。验证方式与结论见 [`docs/implementation.md`](docs/implementation.md) §10、决策见 [`docs/design-consensus.md`](docs/design-consensus.md) 修订 17。
129
+ - 版本 `0.3.2`(0.1 线末版):仅放宽 `engines.dsh`;**它声称的 0.2 兼容是错的**(0.3.2 在 0.2.0-rc.2 上会因 `settingsScope` 不存在而让整个 web 启动失败)。
126
130
  - 同名发布在 npm([`dsh-usage-state`](https://www.npmjs.com/package/dsh-usage-state)),也可从 GitHub 直接安装。
127
- - 版本 `0.3.2`:DSH 0.2 发布线的兼容复核(无代码改动,只放宽 `engines.dsh`);DeepSeek、z.ai 与 OpenCode Zen Go 已在真机验证,其余见下。
128
131
 
129
132
  ## 已知限制
130
133
 
package/cordis.patch.yml CHANGED
@@ -1,7 +1,18 @@
1
1
  # Mount layer contributed by this plugin.
2
2
  #
3
3
  # `dsh plugin add` appends this package to the profile's `dsh.profile.bundles`,
4
- # which applies this patch on boot; the row id only has to be unique.
4
+ # which applies this patch on boot.
5
+ #
6
+ # The row id **is the settings namespace** on DSH 0.2 (`configForms.get(id)` on the
7
+ # client, `entry.options.id` on the host), so it has to keep matching
8
+ # `USAGE_STATE_NS` in `src/host/settings.ts`.
9
+ #
10
+ # `config: {}` gives every install a section to resolve: without it the entry has
11
+ # no config at all, the settings projection answers `undefined`, and the status
12
+ # line stays silent until the user saves something. A user's own
13
+ # `- id: usage-state, config: { … }` row still replaces this whole value, because
14
+ # the profile patch layer applies after every bundle layer.
5
15
  - insert:
6
16
  - id: usage-state
7
17
  name: dsh-usage-state
18
+ config: {}
package/lib/client.js CHANGED
@@ -1524,6 +1524,54 @@ window.__ModuleLoader__.load({
1524
1524
  }
1525
1525
  }
1526
1526
  //#endregion
1527
+ //#region src/client/settings-form.ts
1528
+ /**
1529
+ * Present one `configForms` entry as the settings scope the components consume.
1530
+ *
1531
+ * Two things the platform form does not do for us:
1532
+ *
1533
+ * - **Decoding.** The host hands the stored section through unchanged, because this
1534
+ * plugin's schema declares no fields on purpose (see `host/settings.ts`). So
1535
+ * `normalizeConfig` runs here, keeping the single lenient decoder the plugin has
1536
+ * always had. A namespace the platform has not answered yet keeps `value`
1537
+ * undefined, which is what makes the status line stay silent instead of drawing
1538
+ * defaults the host never accepted.
1539
+ * - **Snapshot identity.** `useSettingsValue` stores whatever `getSnapshot()`
1540
+ * returns, so a fresh object per call would re-render forever. The decoded
1541
+ * snapshot is cached against the platform snapshot it came from.
1542
+ */
1543
+ function usageStateSettings(form) {
1544
+ let source;
1545
+ let decoded;
1546
+ const snapshot = () => {
1547
+ const next = form.getSnapshot();
1548
+ if (decoded === void 0 || next !== source) {
1549
+ source = next;
1550
+ decoded = {
1551
+ status: next.status,
1552
+ value: next.value === void 0 ? void 0 : normalizeConfig(next.value),
1553
+ revision: next.revision,
1554
+ writable: next.writable,
1555
+ mode: next.mode
1556
+ };
1557
+ }
1558
+ return decoded;
1559
+ };
1560
+ return {
1561
+ getSnapshot: snapshot,
1562
+ subscribe: (listener) => form.subscribe(listener),
1563
+ set: async (field, value) => {
1564
+ await form.set(field, value);
1565
+ },
1566
+ unset: async (field) => {
1567
+ await form.unset(field);
1568
+ },
1569
+ mutate: async (ops, expectedRevision) => {
1570
+ await form.mutate(ops, expectedRevision);
1571
+ }
1572
+ };
1573
+ }
1574
+ //#endregion
1527
1575
  //#region src/client/slots.ts
1528
1576
  const STATUS_LINE_SLOTS = [{
1529
1577
  name: "conversation.composer.dock",
@@ -1680,7 +1728,7 @@ window.__ModuleLoader__.load({
1680
1728
  const inject = [
1681
1729
  "slots",
1682
1730
  "locale",
1683
- "settingsScope",
1731
+ "configForms",
1684
1732
  "remote",
1685
1733
  "remote.session",
1686
1734
  "remote.credentials"
@@ -1727,10 +1775,7 @@ window.__ModuleLoader__.load({
1727
1775
  en
1728
1776
  }), "dsh-usage-state: dictionaries");
1729
1777
  const t = ctx.locale.bind(LOCALE_NS);
1730
- const settings = ctx.settingsScope.bind({
1731
- namespace: USAGE_STATE_NS,
1732
- decode: (section) => normalizeConfig(section)
1733
- });
1778
+ const settings = usageStateSettings(ctx.configForms.get(USAGE_STATE_NS));
1734
1779
  const usageStateRemote = () => remoteService(ctx, "remote.usageState");
1735
1780
  const store = new UsageStateClientStore({
1736
1781
  getState: async (force) => {
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import z from "@deepseek-ai/schemastery";
4
5
  //#region src/shared/config.ts
5
6
  const DEFAULT_CONFIG = {
6
7
  models: [],
@@ -1410,29 +1411,42 @@ var UsageStateService = class {
1410
1411
  };
1411
1412
  //#endregion
1412
1413
  //#region src/host/settings.ts
1413
- /** Settings namespace owned by this plugin; must match `/^[a-z][a-z0-9-]*$/`. */
1414
- const USAGE_STATE_NS = "usage-state";
1415
- const usageStateSchema = Object.assign((section) => normalizeConfig(section), { toJSON: () => ({
1416
- uid: 1,
1417
- refs: { 1: {
1418
- type: "object",
1419
- meta: {},
1420
- dict: {}
1421
- } }
1422
- }) });
1423
1414
  /**
1424
- * Register the namespace and keep a live in-process snapshot of it.
1415
+ * The plugin's `Config`, which the 0.2 loader resolves from this module export
1416
+ * (`entry.fiber.runtime.Config`), validates with `Config['~standard']`, and hands
1417
+ * to `apply` as its second argument.
1425
1418
  *
1426
- * `ctx.inject` is the graceful-degradation boundary: on a host without a settings
1427
- * provider the callback never runs and the plugin's own defaults stand. `applies`
1428
- * is metadata in this DSH version — what actually makes a change live is the
1429
- * `watch` subscription below.
1419
+ * Two deliberate choices, both about staying compatible with documents this
1420
+ * plugin does not fully control:
1421
+ *
1422
+ * - **A volatile root.** The loader hands a volatile schema's value over as a live
1423
+ * reference, commits edits into it in place, and announces them as
1424
+ * `loader/volatile-update`. A non-volatile schema would instead find the config
1425
+ * "changed" on every settings write and remount the plugin.
1426
+ * - **`any`, not a field-by-field object.** The settings service projects form
1427
+ * values *through* the schema, so a declared object schema silently drops every
1428
+ * field it does not declare — including keys a hand-edited document carries, and
1429
+ * including keys a future version adds. Validation stays where it has always
1430
+ * been: `normalizeConfig`, which is deliberately lenient, so a dirty document
1431
+ * still loads instead of failing the entry.
1432
+ */
1433
+ const Config = z.any().volatile();
1434
+ /**
1435
+ * Keep a live snapshot of the plugin's own config.
1436
+ *
1437
+ * `reference` is `apply`'s second argument. On a host that does not model volatile
1438
+ * config (or when the entry carries no schema) the loader passes nothing, and the
1439
+ * plugin keeps its own defaults — the same graceful degradation the 0.1 path got
1440
+ * from `ctx.inject(['settings'], …)`, which 0.2 no longer offers.
1430
1441
  */
1431
- function installUsageStateSettings(ctx, onConfig) {
1432
- ctx.inject(["settings"], (settingsCtx) => {
1433
- const scope = settingsCtx.settings.register(USAGE_STATE_NS, usageStateSchema, { applies: "live" });
1434
- onConfig(normalizeConfig(scope.get()));
1435
- scope.watch((next) => onConfig(normalizeConfig(next)));
1442
+ function installUsageStateSettings(ctx, reference, onConfig) {
1443
+ if (reference === void 0) return;
1444
+ const publish = () => {
1445
+ onConfig(normalizeConfig(reference.get()));
1446
+ };
1447
+ publish();
1448
+ ctx.on("loader/volatile-update", () => {
1449
+ publish();
1436
1450
  });
1437
1451
  }
1438
1452
  //#endregion
@@ -1598,11 +1612,38 @@ function resolveTargets(config, options = {}) {
1598
1612
  const name = "usage-state";
1599
1613
  /** `timer` is what provides `ctx.timeout` / `ctx.interval`. */
1600
1614
  const inject = ["timer"];
1615
+ /** The shared volatile-reference protocol (`cosmokit.volatile.write`). */
1616
+ const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
1617
+ /** Whether a loader config value is a live volatile reference rather than data. */
1618
+ function isVolatileRef(value) {
1619
+ return typeof value === "object" && value !== null && VOLATILE_WRITE in value && typeof value.get === "function";
1620
+ }
1621
+ /**
1622
+ * Detach a loader config: volatile fields are live references, and a snapshot is
1623
+ * only useful once they are read out. Mirrors the platform's own `plainConfig`,
1624
+ * including its refusal to guard against cycles (config cannot contain one).
1625
+ */
1626
+ function plainConfig(value) {
1627
+ if (isVolatileRef(value)) return plainConfig(value.get());
1628
+ if (Array.isArray(value)) return value.map((item) => plainConfig(item));
1629
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, plainConfig(item)]));
1630
+ return value;
1631
+ }
1632
+ /**
1633
+ * Another plugin's resolved config, by profile entry id.
1634
+ *
1635
+ * DSH 0.1 answered this from `settings.get(namespace)`; 0.2 derives every
1636
+ * namespace from the entry that owns it, so the resolved value lives on that
1637
+ * entry's fiber (the same place the platform's settings service reads it).
1638
+ * A host without a config editor degrades to `undefined`, which is what this
1639
+ * plugin's provider refinement has always done when a namespace was absent.
1640
+ */
1601
1641
  function readNamespace(ctx, namespace) {
1602
- const settings = ctx.get("settings");
1603
- if (settings?.get === void 0) return void 0;
1642
+ const editor = ctx.get("configEditor");
1643
+ if (typeof editor?.entries !== "function") return void 0;
1604
1644
  try {
1605
- return settings.get(namespace);
1645
+ const entry = editor.entries().find((candidate) => candidate.options?.id === namespace);
1646
+ return entry?.fiber?.config === void 0 ? void 0 : plainConfig(entry.fiber.config);
1606
1647
  } catch {
1607
1648
  return;
1608
1649
  }
@@ -1675,7 +1716,7 @@ function createUsageState(ctx, deps = {}) {
1675
1716
  const catalog = toSourceCatalog(ALL_SOURCES);
1676
1717
  const lookup = createCredentialLookup(ctx, deps.credentialFallback ?? {});
1677
1718
  let config = DEFAULT_CONFIG;
1678
- installUsageStateSettings(ctx, (next) => {
1719
+ installUsageStateSettings(ctx, deps.config, (next) => {
1679
1720
  config = next;
1680
1721
  });
1681
1722
  /**
@@ -1757,8 +1798,12 @@ function createUsageState(ctx, deps = {}) {
1757
1798
  store.start();
1758
1799
  return provideUsageState(ctx, service);
1759
1800
  }
1760
- function apply(ctx) {
1761
- createUsageState(ctx);
1801
+ /**
1802
+ * Cordis entry point. The second argument is the loader's resolved config for this
1803
+ * entry — a live reference, because the exported schema is a volatile root.
1804
+ */
1805
+ function apply(ctx, config) {
1806
+ createUsageState(ctx, { config });
1762
1807
  }
1763
1808
  //#endregion
1764
- export { apply, createCredentialLookup, createUsageState, inject, name, provideUsageState };
1809
+ export { Config, apply, createCredentialLookup, createUsageState, inject, isVolatileRef, name, plainConfig, provideUsageState };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "dsh-usage-state",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "DSH plugin: show account balance (API mode) or coding-plan quota (5h/7d/30d) for the model in use",
6
6
  "license": "MIT",
7
7
  "engines": {
8
8
  "node": ">=20",
9
- "dsh": ">=0.1.5-rc.1 <0.3.0-0"
9
+ "dsh": ">=0.2.0-rc.2 <0.3.0-0"
10
10
  },
11
11
  "scripts": {
12
12
  "test": "node --import ./tests/support/client-render-hook.mjs --test",
@@ -28,12 +28,13 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "devDependencies": {
31
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
32
- "@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
33
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
34
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
35
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
36
- "@deepseek-ai/dsh-typert-loader": "^0.1.5-rc.2",
31
+ "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
32
+ "@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
33
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.2",
34
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
35
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
36
+ "@deepseek-ai/dsh-typert-loader": "^0.2.0-rc.2",
37
+ "@deepseek-ai/schemastery": "^3.18.2",
37
38
  "@types/node": "^24.0.0",
38
39
  "@types/react": "~18.3.1",
39
40
  "@types/react-dom": "~18.3.1",
@@ -43,6 +44,10 @@
43
44
  "typescript": "^5.9.2",
44
45
  "unrun": "^0.2.39"
45
46
  },
47
+ "peerDependencies": {
48
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
49
+ "@deepseek-ai/schemastery": "^3.18.2"
50
+ },
46
51
  "dependencies": {
47
52
  "zod": "4.5.1"
48
53
  },