dsh-connect-trae 2.1.0 → 2.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.0 (2026-09-24)
4
+
5
+ ### Bug Fixes
6
+
7
+ - **支持 DSH 0.1.7 线,且同一个构建同时服务 0.1.5 与 0.1.7**(修复 [issue #13](https://github.com/dingminhua/dsh-connect-trae/issues/13))。这不是「多支持一个版本」的可选增强——**在 0.1.7 上插件的浏览器半侧此前根本不激活**:
8
+
9
+ ```
10
+ web boot: 1 entry did not activate
11
+ dsh-connect-trae: pending (waiting for service: settingsScope)
12
+ ```
13
+
14
+ - **根因:`settingsScope` 被整个移除,而 Cordis 的依赖闸门是硬闸。** DSH **0.1.7-alpha.1** 起客户端设置服务换成 `configForms`,`settingsScope` **不再存在**(不是弃用、没有兼容层;0.1.6-alpha.2 还有,0.1.7-alpha.1 起包内已无此符号)。插件 `inject` 里仍声明它,于是 fiber 永远停在 `PENDING`——**不是某个功能坏掉,是整个客户端插件不激活**,因此「重新登录」「重装插件」一概无效。修法与 workbuddy 一致:`inject` 收窄为两条线都有的 `['slots', 'locale']`,设置面改由 `ctx.get()` **按能力软探测**(属性访问会抛 `cannot get property X without inject`,`?.` 挡不住;`ctx.get` 对缺失服务返回 `undefined`)。
15
+ - **0.1.7 上另有三处断裂,issue 只报了第一处。** 逐项对照 workbuddy 的实测(该仓库已用同一套适配跑通 0.1.7):
16
+
17
+ | # | 位置 | 0.1.5 | 0.1.7 |
18
+ |---|---|---|---|
19
+ | 1 | 客户端设置服务 | `settingsScope` | `configForms` |
20
+ | 2 | 客户端槽位名 | `settings.plugin.item` | `plugins.bundle.config` / `plugins.row.config` |
21
+ | 3 | 宿主端注册 | `installSection()` | `configure({auto}, owner)` |
22
+ | 4 | schema 可写声明 | 无需 | 必须标 `volatile()` |
23
+ | 5 | primitives 图标名 | `…Outline14` | `…OutlineRegular` |
24
+
25
+ - **第 2 条(槽位名)最容易被漏掉**:两条线声明的槽位集合**互不相交**(0.1.5 只有 `settings.plugin.item`;0.1.7 只有 `plugins.*`)。就算设置服务改对了,槽位名不改卡片依然渲染不出来。现在三个槽位**各自独立 try/catch** 逐个注册——一条线上不存在的槽位不能把另一条线的注册一起带走。
26
+ - **第 3 条(宿主端)在 0.1.7 上是静默失败**:`installSection` 被 `SettingsForms.configure()` 取代。由于该调用位于嵌套的 `ctx.inject` 回调内,抛出的 `installSection is not a function` 落在**那个子 fiber** 上——外层插件照常加载、两个 provider 照常注册,**但 `trae` 设置命名空间从未注册**,表现为卡片设置区无声消失。现在按能力探测:有 `configure` 走 `configure`,否则回落 `installSection`。
27
+ - **第 4 条(易失声明)**:0.1.7 的 settings 写入门要求插件 schema 把可写字段标为 volatile,否则**写入被直接拒绝**(`Plugin entry "trae" has no volatile fields`),而 `scope.set()` 却正常 resolve——用户会看到开关翻过去又静默复原。新增 `asVolatile()`:运行时探测 `schema.volatile()`,**有则调用、无则退化为 identity no-op**(`volatile()` 自 schemastery 3.18.3 起才有,0.1.5 线锁在 3.18.2)。**故意不手写 `meta.volatile = true`**——那会绕过 schemastery 自身的 `validateVolatileSchema` 校验,产出一个它自己都不认的 schema。
28
+ - **0.1.7 以「活引用」交付配置值。** 该线把 volatile 字段以 `{get(): T}` 形式交给消费方;不解包则 `config.regions`、`value.accounts[region]`、`config.authFile` 静默变成对象或 `undefined`,表现为「设置明明写了却读不到」。更隐蔽的是**展开一个活引用得到的是 `{get: <function>}` 而不是值**——卡片「写一个区域、保留另一个」的合并会因此**丢掉兄弟区域并把函数泄进设置文档**。新增 `unwrapVolatile()` / `unwrapVolatileDeep()` 并在所有读取与合并路径上解包,同时监听 `loader/volatile-update` 在每次写入后重读选择。
29
+ - **第 5 条(图标)会让整个客户端包挂掉**:两条线 primitives 的图标名不重叠(`IconChevronDownOutline14` vs `IconChevronDownOutlineRegular`),**没有任何一个静态 import 能同时服务两边**——静态导入 0.1.5 的名字会让 0.1.7 上的客户端包解析失败,整个卡片随之消失。折叠箭头改为**纯 CSS caret**(`border` + `rotate`),与版本无关。
30
+ - **设置写入改为「写入后回读校验」**:`set()` 的 promise resolve **不代表值已落盘**(0.1.7 上被拒绝的写入会重新加载 Host 状态然后正常返回,0.1.5 上则可能被文件占用丢弃)。四处写入(账号选择、账号重扫、区域开关、模型目录)统一走 `writeSettingsField()`:写入后回读,未落盘即抛错并在卡片上明确显示,而不是「点了没反应」。0.1.7 的 `set()` 还会返回显式布尔值,`false` 同样按拒绝处理。
31
+ - **卡片的 `view` 属性**:0.1.7 的插件管理器通过 owner props 向每个配置条目索取两种视图(`summary` 一句话简介 / `page` 带保存控件的完整表单)。卡片接受该属性并在 `page` 下默认展开——否则在 0.1.7 的插件页里卡片会是折叠的一行。
32
+ - **开发环境的 schemastery 从 3.18.2 提到 3.18.4**(`pnpm-workspace.yaml` 的 override,与 `devDependencies` 对齐)。此前 dev 树锁在 3.18.2,那里没有 `volatile()`,`asVolatile()` 退化为 no-op——**于是 volatile 真正生效的那条路径(每个 0.1.7 用户都会走的那条)在本地从未被执行过**,这类缺陷可以同时躲过评审与 CI。这与 workbuddy 记录的教训同源。
33
+
34
+ ### Tests
35
+
36
+ 测试总数 282 → 310。
37
+
38
+ - **新增 `tests/client-activation.spec.tsx`(3 例,本次最关键的一组)**:用真实的 cordis `Context` **启动真实的客户端入口模块**,分别对 0.1.7 形态(有 `configForms`、**无** `settingsScope`)、0.1.5 形态(有 `settingsScope`、无 `configForms`)、以及**两者都没有**的形态断言 fiber 到达 `ACTIVE`、槽位注册齐全、scope 取自正确的服务。之所以能直接导入真实入口,是因为它运行期只依赖 React 与本地文件,所有 DSH 包都是**类型导入**、构建时即被擦除。
39
+ - 此前 `client-fallback.spec.ts` 抓不到这个缺陷:它**手工镜像**入口函数体,因此验证的是「思路」而**从不执行真实的 `inject` 数组**——而恰恰是那个数组让 fiber 卡在 PENDING。
40
+ - **变异验证**:把 `inject` 改回 `['slots','locale','settingsScope']`,0.1.7 与「无设置面」两例立即变红(fiber 停在 0/1,永远到不了 `ACTIVE`),0.1.5 那例仍绿——精确复现了 issue 报的现象。
41
+ - **新增 `tests/dsh-017-compat.spec.ts`(13 例)**:活引用的单层/深层解包、数组与嵌套、**不就地改写调用方对象**;`regionEnabledOf` / `regionStateOf` / `regionEnabled` 穿透活引用读值;合并时**保留兄弟区域且不泄露函数**;`asVolatile()` 的两条分支按运行时能力确定性断言;以及**宿主端 `configure()` 优先于 `installSection()`**(用一个只有 `configure` 的真 Service 装配,断言走的是 `configure({auto:true})` 且没有 `installSection` 报错)。
42
+ - **变异验证**:把 `configure` 探测改回无保护裸调 `installSection` → 1 条立即变红;把 `nextRegionSlots` 的深解包去掉 → 1 条立即变红。
43
+ - `client-fallback.spec.ts` 的镜像同步更新,并补 2 例:**两条线都提供时 `configForms` 优先**(0.1.7 的写入闸门只认它),以及 **`configForms` 缺席时回落 `settingsScope.bind()`**。
44
+ - **`card-region-switch.spec.tsx` 新增 8 例**(渲染真组件、点真按钮):穿透活引用读区域开关;合并时**保留兄弟区域且不泄露函数**;Host 明确拒绝(`set()` 返回 `false`)与**「接受了但没落盘」**(`set()` 返回 `true` 而文档未变)两种失败都必须显式报错;0.1.5 的 `void` 返回视为成功;`view: 'page'` 时默认展开、无 `view` 时默认折叠;无设置面时渲染为只读。
45
+ - **变异验证**:去掉 `regionsMapOf` 的解包 → 1 条失败;去掉 `view` 判断 → 1 条失败;去掉 `false` 判断 → 0 条失败(回读校验兜住了,说明两层防护确实互补);去掉回读校验 → 1 条失败;**两层都去掉 → 1 条失败**。
46
+ - 顺带移除了该文件对 `dsh-client-ui-primitives` 的整包 `vi.mock`:图标已改纯 CSS,不再需要这个桩。
47
+
48
+ **双版本验证**:同一份代码在 schemastery **3.18.4**(volatile 生效)与 **3.18.2**(no-op 分支)下**均为 310 通过**。
49
+
3
50
  ## 2.1.0 (2026-09-24)
4
51
 
5
52
  ### Features
package/README.en.md CHANGED
@@ -94,6 +94,25 @@ npm install dsh-connect-trae
94
94
 
95
95
  Restart the DSH process after install/update/uninstall.
96
96
 
97
+ ## DSH version compatibility
98
+
99
+ **One build serves both the DSH 0.1.5 and 0.1.7 lines** — no per-version package.
100
+
101
+ The two lines differ in their settings machinery, and the plugin probes for capability at runtime instead of assuming either:
102
+
103
+ | | 0.1.5 line | 0.1.7 line |
104
+ |---|---|---|
105
+ | Client settings service | `settingsScope` | `configForms` |
106
+ | Config card slots | `settings.plugin.item` | `plugins.bundle.config` / `plugins.row.config` |
107
+ | Host registration | `installSection()` | `configure({auto}, owner)` |
108
+ | Schema writable marker | not needed | `volatile()` required |
109
+ | Primitives icon names | `…Outline14` | `…OutlineRegular` |
110
+
111
+ Two decisions matter most:
112
+
113
+ - **`inject` declares only the services BOTH lines provide** (`slots` / `locale`); the settings surface is probed with `ctx.get()`. Cordis' dependency gate is hard — any `inject` entry the running line does not provide keeps `apply()` from ever running (0.1.7 removed `settingsScope`, which is why 2.1.0 and earlier sat at `pending (waiting for service: settingsScope)` on that line).
114
+ - **The collapse caret is pure CSS**, with no static icon import: the two lines' icon names do not overlap, so any static import fails to resolve on one of them.
115
+
97
116
  ## Windows notes
98
117
 
99
118
  - **Account data directory**: the plugin reads `%APPDATA%\Trae CN` / `%APPDATA%\TRAE SOLO CN` → `User\globalStorage\storage.json` (unrelated to the install directory). The folder names match macOS, so no extra config is needed; if the name does not match, point the `authFile` + `edition` plugin options at the exact path.
package/README.md CHANGED
@@ -96,6 +96,25 @@ npm install dsh-connect-trae
96
96
 
97
97
  安装、更新或卸载 bundle 后,需要重启对应的 DSH 进程。
98
98
 
99
+ ## DSH 版本兼容
100
+
101
+ **同一个构建同时服务 DSH 0.1.5 与 0.1.7 两条线**,无需按版本安装不同的包。
102
+
103
+ 两条线的设置体系不同,插件在运行期按能力探测,而不是假定某一条:
104
+
105
+ | | 0.1.5 线 | 0.1.7 线 |
106
+ |---|---|---|
107
+ | 客户端设置服务 | `settingsScope` | `configForms` |
108
+ | 配置卡片槽位 | `settings.plugin.item` | `plugins.bundle.config` / `plugins.row.config` |
109
+ | 宿主端注册 | `installSection()` | `configure({auto}, owner)` |
110
+ | schema 可写声明 | 无需 | 必须标 `volatile()` |
111
+ | primitives 图标名 | `…Outline14` | `…OutlineRegular` |
112
+
113
+ 两处关键做法:
114
+
115
+ - **`inject` 只声明两条线都有的服务**(`slots` / `locale`),设置面用 `ctx.get()` 软探测。Cordis 的依赖闸门是硬闸——`inject` 里任一服务在运行线上不存在,`apply()` 就永远不会执行(0.1.7 上 `settingsScope` 已被移除,这正是 2.1.0 及更早版本在 0.1.7 上卡在 `pending (waiting for service: settingsScope)` 的原因)。
116
+ - **折叠箭头用纯 CSS**,不静态导入任何图标:两条线的图标名不重叠,静态导入必然在其中一条线上解析失败。
117
+
99
118
  ## Windows 说明
100
119
 
101
120
  - **账号数据目录**:插件读取 `%APPDATA%\Trae CN` / `%APPDATA%\TRAE SOLO CN` 下的 `User\globalStorage\storage.json`(与安装目录无关)。目录名与 macOS 一致,无需额外配置;若目录名对不上,可用插件配置项 `authFile` + `edition` 直接指定完整路径。
package/lib/client.js CHANGED
@@ -5,7 +5,6 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
8
  let react_jsx_runtime = require("react/jsx-runtime");
10
9
  //#region src/status-paths.ts
11
10
  /** Plugin-owned usage endpoint consumed by its browser half. */
@@ -31,6 +30,42 @@ window.__ModuleLoader__.load({
31
30
  return `${path}?${TRAE_REGION_PARAM}=${region}`;
32
31
  }
33
32
  /**
33
+ * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
34
+ * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
35
+ */
36
+ function unwrapVolatile(value) {
37
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
38
+ return value;
39
+ }
40
+ /**
41
+ * Deep copy of a value with every `{get(): T}` live reference replaced by the
42
+ * value it resolves to.
43
+ *
44
+ * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
45
+ * reference is still `typeof === 'object'` — so it passes a naive object check
46
+ * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
47
+ * does NOT read the field: it produces `{ get: <function> }`. Any merge that
48
+ * spreads a resolved field to preserve its siblings — the card's "write one
49
+ * region, keep the other" write, or the account map's spread — would otherwise
50
+ * DROP every sibling and leak a function into the document.
51
+ *
52
+ * Both halves therefore unwrap before touching a resolved field. This lives in
53
+ * the node-free host↔client bridge because the browser half needs it too.
54
+ *
55
+ * Non-reference values are recursed into so a nested volatile field is caught
56
+ * as well; arrays and objects are rebuilt rather than mutated, so the caller's
57
+ * value is never touched.
58
+ */
59
+ function unwrapVolatileDeep(value) {
60
+ if (value === null || typeof value !== "object") return value;
61
+ if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
62
+ if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
63
+ const source = value;
64
+ const out = {};
65
+ for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
66
+ return out;
67
+ }
68
+ /**
34
69
  * Build the next `regions` settings value for the card's save. The write
35
70
  * targets ONLY the signed-in account's region slot; every other region's slot
36
71
  * is carried over untouched, so switching accounts never clobbers the other
@@ -38,8 +73,9 @@ window.__ModuleLoader__.load({
38
73
  * from an empty document.
39
74
  */
40
75
  function nextRegionSlots(regions, region, slot) {
76
+ const unwrapped = unwrapVolatileDeep(regions);
41
77
  return {
42
- ...typeof regions === "object" && regions !== null && !Array.isArray(regions) ? regions : {},
78
+ ...typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped) ? unwrapped : {},
43
79
  [region]: slot
44
80
  };
45
81
  }
@@ -50,17 +86,23 @@ window.__ModuleLoader__.load({
50
86
  * where the map was expected was a real shipped bug — the lookup then read
51
87
  * `section['cn']` (absent), so the card's checkbox reported `true` forever and
52
88
  * clicking it appeared to do nothing even though the write succeeded.
89
+ *
90
+ * Unwraps live references first: on DSH 0.1.7 the `regions` field arrives as a
91
+ * `{get(): T}` reference, which passes the `typeof === 'object'` check below
92
+ * and would otherwise be returned as if it were the map — making every region
93
+ * read back as absent (and the enabled flag read as "on" forever).
53
94
  */
54
95
  function regionsMapOf(value) {
55
- if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
56
- const record = value;
96
+ const unwrapped = unwrapVolatileDeep(value);
97
+ if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
98
+ const record = unwrapped;
57
99
  const nested = record["regions"];
58
100
  if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
59
101
  return record;
60
102
  }
61
103
  /** One region's stored slot as a plain object; any other shape reads as empty. */
62
104
  function regionSlotOf(value, region) {
63
- const slot = regionsMapOf(value)[region];
105
+ const slot = unwrapVolatile(regionsMapOf(value)[region]);
64
106
  return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
65
107
  }
66
108
  /**
@@ -123,7 +165,13 @@ window.__ModuleLoader__.load({
123
165
  .dsm-plugin-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
124
166
  .dsm-plugin-card-title{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;font-weight:600;line-height:1.4}
125
167
  .dsm-plugin-card-description{color:var(--dsw-alias-label-tertiary,#999);font-size:13px;line-height:1.5}
126
- .dsm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary,#999);flex:none;display:inline-flex;transition:transform .16s}
168
+ /* Pure-CSS caret: the host primitives' chevron icon names differ per DSH line
169
+ (0.1.5 IconChevronDownOutline14 vs 0.1.7 IconChevronDownOutlineRegular),
170
+ so no static import can serve both — importing the 0.1.5 name makes the
171
+ client bundle fail to resolve on 0.1.7, taking the whole card down. A border
172
+ caret in the plugin's own CSS is version-proof. */
173
+ .dsm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary,#999);flex:none;width:16px;height:16px;position:relative;transition:transform .16s}
174
+ .dsm-plugin-card-chevron::before{content:"";display:block;position:absolute;left:4px;top:5px;width:7px;height:7px;border-right:1.6px solid currentColor;border-bottom:1.6px solid currentColor;transform:rotate(45deg)}
127
175
  .dsm-plugin-card-chevron-open{transform:rotate(180deg)}
128
176
  .dsm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2,#36373b);margin:0 16px;padding:0 0 8px}
129
177
  .dsm-plugin-card-icon{width:32px;height:32px;flex:none;border-radius:7px}
@@ -230,11 +278,49 @@ window.__ModuleLoader__.load({
230
278
  */
231
279
  const POLL_INTERVAL_MS = 6e4;
232
280
  const TRAE_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-trae";
233
- /** Read the per-region account selections out of the settings snapshot. */
281
+ /**
282
+ * Read the per-region account selections out of the settings snapshot.
283
+ *
284
+ * Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
285
+ * live reference, which passes the `typeof === 'object'` check below and would
286
+ * be returned as if it were the map. Every lookup on it is then `undefined`,
287
+ * and the card's merge (`{ ...configured, [region]: id }`) would spread a
288
+ * reference into `{get: <function>}` — dropping the other region's selection
289
+ * and leaking a function into the settings document.
290
+ */
234
291
  function configuredAccountsOf(configured) {
235
- const accounts = configured?.accounts;
292
+ const accounts = unwrapVolatileDeep(configured)?.accounts;
236
293
  return typeof accounts === "object" && accounts !== null ? accounts : {};
237
294
  }
295
+ /**
296
+ * Write one settings field and confirm the value actually landed.
297
+ *
298
+ * `settingsScope.set()` / `configForms.set()` resolving is NOT proof that
299
+ * anything was stored. On DSH 0.1.7 a write can be accepted by the transport
300
+ * and still not materialize — a rejected write reloads Host state and merely
301
+ * RETURNS, so the caller's `await` succeeds while the document is unchanged.
302
+ * The 0.1.7 form also answers with an explicit boolean for exactly this reason.
303
+ *
304
+ * Either way the caller must not report success on an unpersisted write: the
305
+ * user would see the control flip and then silently revert.
306
+ *
307
+ * @throws {TraeSettingsWriteError} when the write was refused or did not land.
308
+ */
309
+ var TraeSettingsWriteError = class extends Error {
310
+ constructor(field, detail) {
311
+ super(detail === void 0 || detail === "" ? `trae: settings field "${field}" was not persisted by the settings write` : `trae: settings field "${field}" was not persisted by the settings write (${detail})`);
312
+ this.name = "TraeSettingsWriteError";
313
+ }
314
+ };
315
+ async function writeSettingsField(scope, field, value, landed) {
316
+ if (await scope.set(field, value) === false) throw new TraeSettingsWriteError(field, "the Host refused the write");
317
+ if (!landed(unwrapVolatileDeep(scope.getSnapshot().value))) throw new TraeSettingsWriteError(field);
318
+ }
319
+ /** Read one region's stored `regions` slot out of an unwrapped settings value. */
320
+ function regionSlotsOf(value) {
321
+ const regions = value?.regions;
322
+ return typeof regions === "object" && regions !== null ? regions : {};
323
+ }
238
324
  /** Inject the shared card CSS once. */
239
325
  if (typeof document !== "undefined") {
240
326
  const cssId = "dsh-connect-trae/client.css";
@@ -271,9 +357,9 @@ window.__ModuleLoader__.load({
271
357
  return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
272
358
  }
273
359
  /** Render Trae sign-in state and the total usage summary as one expandable card. */
274
- function TraeUsageCard({ t, settingsScope }) {
360
+ function TraeUsageCard({ t, settingsScope, view }) {
275
361
  if (t === void 0) throw new Error("Trae usage card requires its translation function");
276
- const [open, setOpen] = (0, react.useState)(false);
362
+ const [open, setOpen] = (0, react.useState)(view === "page");
277
363
  /** The region whose tab is on screen; each tab is its own provider stack. */
278
364
  const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
279
365
  /** Last-known usage per region, so tab dots survive tab switches. */
@@ -304,6 +390,12 @@ window.__ModuleLoader__.load({
304
390
  const [claiming, setClaiming] = (0, react.useState)(false);
305
391
  /** Last claim refusal, shown until the next successful refresh. */
306
392
  const [claimError, setClaimError] = (0, react.useState)(void 0);
393
+ /**
394
+ * A refused or unpersisted settings write, surfaced instead of silently
395
+ * reverting. On 0.1.7 a rejected write resolves normally, so without this
396
+ * the control would flip and then quietly snap back on the next snapshot.
397
+ */
398
+ const [writeError, setWriteError] = (0, react.useState)(void 0);
307
399
  const mounted = (0, react.useRef)(true);
308
400
  (0, react.useEffect)(() => {
309
401
  mounted.current = true;
@@ -399,6 +491,7 @@ window.__ModuleLoader__.load({
399
491
  ]);
400
492
  const rescanAccounts = async () => {
401
493
  setBusy(true);
494
+ setWriteError(void 0);
402
495
  try {
403
496
  const response = await fetch(withTraeRegion(TRAE_ACCOUNTS_REFRESH_PATH, activeRegion), {
404
497
  method: "POST",
@@ -409,11 +502,13 @@ window.__ModuleLoader__.load({
409
502
  if (!response.ok || !Array.isArray(body.accounts)) throw new Error(`HTTP ${response.status}`);
410
503
  const selected = body.accounts.find((account) => account.selected)?.id;
411
504
  const configured = configuredAccountsOf(settingsScope?.getSnapshot().value);
412
- if (selected !== void 0 && selected !== configured[activeRegion] && settingsScope?.getSnapshot().writable === true) await settingsScope.set("accounts", {
505
+ if (selected !== void 0 && selected !== configured[activeRegion] && settingsScope !== void 0 && settingsScope.getSnapshot().writable === true) await writeSettingsField(settingsScope, "accounts", {
413
506
  ...configured,
414
507
  [activeRegion]: selected
415
- });
508
+ }, (readBack) => configuredAccountsOf(readBack)[activeRegion] === selected);
416
509
  await refreshUsage(activeRegion);
510
+ } catch (error) {
511
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
417
512
  } finally {
418
513
  if (mounted.current) setBusy(false);
419
514
  }
@@ -421,13 +516,15 @@ window.__ModuleLoader__.load({
421
516
  const switchAccount = async (accountId) => {
422
517
  if (settingsScope === void 0) return;
423
518
  setSwitchingAccount(true);
519
+ setWriteError(void 0);
424
520
  try {
425
- const configured = configuredAccountsOf(settingsScope.getSnapshot().value);
426
- await settingsScope.set("accounts", {
427
- ...configured,
521
+ await writeSettingsField(settingsScope, "accounts", {
522
+ ...configuredAccountsOf(settingsScope.getSnapshot().value),
428
523
  [activeRegion]: accountId
429
- });
524
+ }, (readBack) => configuredAccountsOf(readBack)[activeRegion] === accountId);
430
525
  await refreshUsage(activeRegion);
526
+ } catch (error) {
527
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
431
528
  } finally {
432
529
  if (mounted.current) setSwitchingAccount(false);
433
530
  }
@@ -474,8 +571,11 @@ window.__ModuleLoader__.load({
474
571
  const toggleRegion = async (item, enabled) => {
475
572
  if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
476
573
  setTogglingRegion(item);
574
+ setWriteError(void 0);
477
575
  try {
478
- await settingsScope.set("regions", nextRegionEnabled(settingsScope.getSnapshot().value, item, enabled));
576
+ await writeSettingsField(settingsScope, "regions", nextRegionEnabled(settingsScope.getSnapshot().value, item, enabled), (readBack) => regionSlotsOf(readBack)[item] !== void 0 && regionSlotsOf(readBack)[item].enabled === enabled);
577
+ } catch (error) {
578
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
479
579
  } finally {
480
580
  if (mounted.current) setTogglingRegion(void 0);
481
581
  }
@@ -523,7 +623,7 @@ window.__ModuleLoader__.load({
523
623
  if (mounted.current) setBusy(false);
524
624
  }
525
625
  };
526
- const settingsValue = settingsScope?.getSnapshot().value;
626
+ const settingsValue = unwrapVolatileDeep(settingsScope?.getSnapshot().value);
527
627
  const region = activeRegion;
528
628
  const configuredRegions = typeof settingsValue === "object" && settingsValue !== null && typeof settingsValue.regions === "object" && settingsValue.regions !== null ? settingsValue.regions : {};
529
629
  const regionSlot = configuredRegions[region];
@@ -594,9 +694,10 @@ window.__ModuleLoader__.load({
594
694
  const saveModels = async () => {
595
695
  if (settingsScope === void 0) return;
596
696
  setSaving(true);
697
+ setWriteError(void 0);
597
698
  try {
598
699
  if (status.status !== "signed-in") return;
599
- await settingsScope.set("regions", nextRegionSlots(configuredRegions, activeRegion, {
700
+ const nextSlot = {
600
701
  enabled: regionOn(activeRegion),
601
702
  lastCatalog: visibleModels.map((model) => ({
602
703
  ...model,
@@ -605,9 +706,15 @@ window.__ModuleLoader__.load({
605
706
  enabledModelIds: [...activeEnabledIds],
606
707
  imageModelIds: [...activeImageIds].filter((id) => activeEnabledIds.has(id)),
607
708
  contextBudgets: activeContextBudgets
608
- }));
709
+ };
710
+ await writeSettingsField(settingsScope, "regions", nextRegionSlots(configuredRegions, activeRegion, nextSlot), (readBack) => {
711
+ const ids = regionSlotsOf(readBack)[activeRegion]?.enabledModelIds;
712
+ return Array.isArray(ids) && ids.length === nextSlot.enabledModelIds.length && nextSlot.enabledModelIds.every((id) => ids.includes(id));
713
+ });
609
714
  discardModels();
610
715
  await refreshUsage(activeRegion);
716
+ } catch (error) {
717
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
611
718
  } finally {
612
719
  if (mounted.current) setSaving(false);
613
720
  }
@@ -642,8 +749,7 @@ window.__ModuleLoader__.load({
642
749
  }),
643
750
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
644
751
  "aria-hidden": "true",
645
- className: `dsm-plugin-card-chevron${open ? " dsm-plugin-card-chevron-open" : ""}`,
646
- children: (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
752
+ className: `dsm-plugin-card-chevron${open ? " dsm-plugin-card-chevron-open" : ""}`
647
753
  })
648
754
  ]
649
755
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -695,6 +801,11 @@ window.__ModuleLoader__.load({
695
801
  className: "dsm-trae-tab-hint",
696
802
  children: t("row.tabHint")
697
803
  }),
804
+ writeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
805
+ className: "dsm-trae-usage-error",
806
+ role: "alert",
807
+ children: t("row.settingsWriteFailed", { message: writeError })
808
+ }),
698
809
  regionOn(activeRegion) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
699
810
  className: "dsm-trae-tab-off-notice",
700
811
  role: "status",
@@ -1069,6 +1180,7 @@ window.__ModuleLoader__.load({
1069
1180
  "row.tabSwitchHint": "Uncheck to switch this provider off. Its models disappear from the DSH model picker; its account, usage, and model settings are kept and return when you check it again.",
1070
1181
  "row.tabSwitchAria": "Offer the {region} provider to DSH",
1071
1182
  "row.tabOffNotice": "This provider is switched off: none of its models are offered to DSH. Your account, usage, and model settings are kept — check the box above the tab to bring it back.",
1183
+ "row.settingsWriteFailed": "The setting was not saved: {message}",
1072
1184
  "row.signedOut": "Not signed in",
1073
1185
  "row.signedOutHint": "Sign in once in the Trae desktop app; this plugin follows that sign-in automatically.",
1074
1186
  "row.searchedTitle": "Paths checked",
@@ -1146,6 +1258,7 @@ window.__ModuleLoader__.load({
1146
1258
  "row.tabSwitchHint": "取消勾选即关闭该供应商:它的模型不再出现在 DSH 模型选择器里;账号、用量与模型设置都会保留,重新勾选即恢复。",
1147
1259
  "row.tabSwitchAria": "是否向 DSH 提供{region}供应商",
1148
1260
  "row.tabOffNotice": "该供应商已关闭:它的模型不会提供给 DSH。账号、用量与模型设置均已保留——勾选上方复选框即可恢复。",
1261
+ "row.settingsWriteFailed": "设置未能保存:{message}",
1149
1262
  "row.signedOut": "未登录",
1150
1263
  "row.signedOutHint": "在 Trae 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
1151
1264
  "row.searchedTitle": "已检查的路径",
@@ -1216,12 +1329,17 @@ window.__ModuleLoader__.load({
1216
1329
  //#region src/client/index.tsx
1217
1330
  /** Stable browser-plugin name. */
1218
1331
  const name = "dsh-connect-trae-client";
1219
- /** Client services required by the Plugin configuration contribution. */
1220
- const inject = [
1221
- "slots",
1222
- "locale",
1223
- "settingsScope"
1224
- ];
1332
+ /**
1333
+ * Client services required by the Plugin configuration contribution.
1334
+ *
1335
+ * Deliberately only the two services that exist on BOTH host lines. Cordis'
1336
+ * dependency gate is hard: any `inject` entry the running line does not provide
1337
+ * keeps `apply` from ever running at all — which is exactly how this plugin
1338
+ * ended up `pending (waiting for service: settingsScope)` on 0.1.7. Probing the
1339
+ * settings surface through `ctx.get()` (which returns undefined, never throws,
1340
+ * for an absent service) is what lets one build serve both lines.
1341
+ */
1342
+ const inject = ["slots", "locale"];
1225
1343
  /**
1226
1344
  * Register card copy and the Trae card under Plugin configuration.
1227
1345
  *
@@ -1234,6 +1352,8 @@ window.__ModuleLoader__.load({
1234
1352
  * NOTE: the try/catch boundary of this function is mirrored (duplicated) in
1235
1353
  * `tests/client-fallback.spec.ts`, because the real client entry imports
1236
1354
  * browser-only DSH packages that cannot load in the Node test environment.
1355
+ * If you change the guarded body, the soft probe, or the per-slot
1356
+ * registration here, update that mirror too.
1237
1357
  */
1238
1358
  function apply(ctx) {
1239
1359
  try {
@@ -1243,16 +1363,36 @@ window.__ModuleLoader__.load({
1243
1363
  en
1244
1364
  }), "dsh-connect-trae: settings copy");
1245
1365
  const t = ctx.locale.bind(namespace);
1246
- const settingsScope = ctx.settingsScope.bind({ namespace: "trae" });
1247
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1248
- name: "settings.plugin.item",
1249
- key: "trae",
1250
- priority: 30,
1251
- inject: () => ({
1252
- t,
1253
- settingsScope
1254
- })
1255
- }, TraeUsageCard));
1366
+ const softGet = (name) => ctx.get(name);
1367
+ let settingsScope;
1368
+ const forms = softGet("configForms");
1369
+ const legacy = softGet("settingsScope");
1370
+ if (forms !== void 0) {
1371
+ let ns = "trae";
1372
+ try {
1373
+ const served = (forms.describe().getSnapshot().view?.namespaces ?? []).find((entry) => entry.ns === "trae" || /trae/i.test(entry.ns));
1374
+ if (served !== void 0) ns = served.ns;
1375
+ } catch {}
1376
+ settingsScope = forms.get(ns);
1377
+ } else if (legacy !== void 0) settingsScope = legacy.bind({ namespace: "trae" });
1378
+ const registerCard = (slotName, key) => {
1379
+ try {
1380
+ ctx.slots.inject(slotName, () => ctx.slots.register({
1381
+ name: slotName,
1382
+ key,
1383
+ priority: 30,
1384
+ inject: () => settingsScope === void 0 ? { t } : {
1385
+ t,
1386
+ settingsScope
1387
+ }
1388
+ }, TraeUsageCard));
1389
+ } catch (error) {
1390
+ console.error(`[dsh-connect-trae] card slot "${slotName}" failed to register (host provider unaffected):`, error);
1391
+ }
1392
+ };
1393
+ registerCard("plugins.bundle.config", "dsh-connect-trae");
1394
+ registerCard("plugins.row.config", "dsh-connect-trae#dsh-connect-trae");
1395
+ registerCard("settings.plugin.item", "trae");
1256
1396
  } catch (error) {
1257
1397
  console.error("[dsh-connect-trae] client card failed to load (host provider unaffected):", error);
1258
1398
  }
package/lib/index.d.ts CHANGED
@@ -1435,6 +1435,31 @@ interface TraeWebSearchPath {
1435
1435
  reason: 'missing' | 'unreadable' | 'invalid';
1436
1436
  message?: string;
1437
1437
  }
1438
+ /**
1439
+ * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
1440
+ * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
1441
+ */
1442
+ declare function unwrapVolatile<T>(value: T): T;
1443
+ /**
1444
+ * Deep copy of a value with every `{get(): T}` live reference replaced by the
1445
+ * value it resolves to.
1446
+ *
1447
+ * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
1448
+ * reference is still `typeof === 'object'` — so it passes a naive object check
1449
+ * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
1450
+ * does NOT read the field: it produces `{ get: <function> }`. Any merge that
1451
+ * spreads a resolved field to preserve its siblings — the card's "write one
1452
+ * region, keep the other" write, or the account map's spread — would otherwise
1453
+ * DROP every sibling and leak a function into the document.
1454
+ *
1455
+ * Both halves therefore unwrap before touching a resolved field. This lives in
1456
+ * the node-free host↔client bridge because the browser half needs it too.
1457
+ *
1458
+ * Non-reference values are recursed into so a nested volatile field is caught
1459
+ * as well; arrays and objects are rebuilt rather than mutated, so the caller's
1460
+ * value is never touched.
1461
+ */
1462
+ declare function unwrapVolatileDeep<T>(value: T): T;
1438
1463
  /**
1439
1464
  * Build the next `regions` settings value for the card's save. The write
1440
1465
  * targets ONLY the signed-in account's region slot; every other region's slot
@@ -1655,7 +1680,27 @@ declare function regionStateOf(config: Config, region: TraeRegion): TraeRegionSt
1655
1680
  * disagree about a region's state.
1656
1681
  */
1657
1682
  declare function regionEnabled(config: Config, region: TraeRegion): boolean;
1683
+ /**
1684
+ * Mark a schema's field as volatile on the DSH lines that support it.
1685
+ *
1686
+ * `volatile()` exists from schemastery 3.18.3 (the DSH 0.1.7 line, which is the
1687
+ * only line whose settings write gate reads the marker). Older pinning (3.18.2,
1688
+ * the 0.1.5 line) has no such method, and the schema must stay byte-identical
1689
+ * to the unmarked original there: hand-writing `meta.volatile = true` would
1690
+ * bypass schemastery's own `validateVolatileSchema` checks and produce a schema
1691
+ * no 0.1.5 consumer understands — so on that line this degrades to an identity
1692
+ * no-op.
1693
+ *
1694
+ * Without the marker, 0.1.7's settings write gate REJECTS every write with
1695
+ * `Plugin entry "trae" has no volatile fields` while the scope's `set()` still
1696
+ * resolves — so the card would look like it saved and silently revert.
1697
+ *
1698
+ * Exported so BOTH arms are testable on any machine: the capability is decided
1699
+ * by whichever schemastery the dependency tree resolves, so without a seam the
1700
+ * no-op arm would silently stop being exercised the moment the pin moved up.
1701
+ */
1702
+ declare function asVolatile<S>(schema: S): S;
1658
1703
  declare const Config: z<Config>;
1659
1704
  declare function apply(ctx: Context, config: Config): void;
1660
1705
  //#endregion
1661
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeObservedModelConfig, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, withTraeRegion };
1706
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeObservedModelConfig, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
package/lib/index.js CHANGED
@@ -3269,6 +3269,42 @@ function regionOfTraeStatusUrl(url) {
3269
3269
  return TRAE_REGIONS.includes(value) ? value : void 0;
3270
3270
  }
3271
3271
  /**
3272
+ * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
3273
+ * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
3274
+ */
3275
+ function unwrapVolatile(value) {
3276
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
3277
+ return value;
3278
+ }
3279
+ /**
3280
+ * Deep copy of a value with every `{get(): T}` live reference replaced by the
3281
+ * value it resolves to.
3282
+ *
3283
+ * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
3284
+ * reference is still `typeof === 'object'` — so it passes a naive object check
3285
+ * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
3286
+ * does NOT read the field: it produces `{ get: <function> }`. Any merge that
3287
+ * spreads a resolved field to preserve its siblings — the card's "write one
3288
+ * region, keep the other" write, or the account map's spread — would otherwise
3289
+ * DROP every sibling and leak a function into the document.
3290
+ *
3291
+ * Both halves therefore unwrap before touching a resolved field. This lives in
3292
+ * the node-free host↔client bridge because the browser half needs it too.
3293
+ *
3294
+ * Non-reference values are recursed into so a nested volatile field is caught
3295
+ * as well; arrays and objects are rebuilt rather than mutated, so the caller's
3296
+ * value is never touched.
3297
+ */
3298
+ function unwrapVolatileDeep(value) {
3299
+ if (value === null || typeof value !== "object") return value;
3300
+ if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
3301
+ if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
3302
+ const source = value;
3303
+ const out = {};
3304
+ for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
3305
+ return out;
3306
+ }
3307
+ /**
3272
3308
  * Build the next `regions` settings value for the card's save. The write
3273
3309
  * targets ONLY the signed-in account's region slot; every other region's slot
3274
3310
  * is carried over untouched, so switching accounts never clobbers the other
@@ -3276,8 +3312,9 @@ function regionOfTraeStatusUrl(url) {
3276
3312
  * from an empty document.
3277
3313
  */
3278
3314
  function nextRegionSlots(regions, region, slot) {
3315
+ const unwrapped = unwrapVolatileDeep(regions);
3279
3316
  return {
3280
- ...typeof regions === "object" && regions !== null && !Array.isArray(regions) ? regions : {},
3317
+ ...typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped) ? unwrapped : {},
3281
3318
  [region]: slot
3282
3319
  };
3283
3320
  }
@@ -3288,17 +3325,23 @@ function nextRegionSlots(regions, region, slot) {
3288
3325
  * where the map was expected was a real shipped bug — the lookup then read
3289
3326
  * `section['cn']` (absent), so the card's checkbox reported `true` forever and
3290
3327
  * clicking it appeared to do nothing even though the write succeeded.
3328
+ *
3329
+ * Unwraps live references first: on DSH 0.1.7 the `regions` field arrives as a
3330
+ * `{get(): T}` reference, which passes the `typeof === 'object'` check below
3331
+ * and would otherwise be returned as if it were the map — making every region
3332
+ * read back as absent (and the enabled flag read as "on" forever).
3291
3333
  */
3292
3334
  function regionsMapOf(value) {
3293
- if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
3294
- const record = value;
3335
+ const unwrapped = unwrapVolatileDeep(value);
3336
+ if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
3337
+ const record = unwrapped;
3295
3338
  const nested = record["regions"];
3296
3339
  if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
3297
3340
  return record;
3298
3341
  }
3299
3342
  /** One region's stored slot as a plain object; any other shape reads as empty. */
3300
3343
  function regionSlotOf(value, region) {
3301
- const slot = regionsMapOf(value)[region];
3344
+ const slot = unwrapVolatile(regionsMapOf(value)[region]);
3302
3345
  return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
3303
3346
  }
3304
3347
  /**
@@ -3735,7 +3778,7 @@ const TRAE_SETTINGS_NS = "trae";
3735
3778
  * picks (the same failure workbuddy fixed with its region split).
3736
3779
  */
3737
3780
  function regionStateOf(config, region) {
3738
- const stored = config.regions?.[region];
3781
+ const stored = unwrapVolatile(unwrapVolatile(config.regions)?.[region]);
3739
3782
  if (stored !== void 0) return stored;
3740
3783
  if (region !== "cn") return {};
3741
3784
  return {
@@ -3756,6 +3799,29 @@ function regionStateOf(config, region) {
3756
3799
  function regionEnabled(config, region) {
3757
3800
  return regionStateOf(config, region).enabled !== false;
3758
3801
  }
3802
+ /**
3803
+ * Mark a schema's field as volatile on the DSH lines that support it.
3804
+ *
3805
+ * `volatile()` exists from schemastery 3.18.3 (the DSH 0.1.7 line, which is the
3806
+ * only line whose settings write gate reads the marker). Older pinning (3.18.2,
3807
+ * the 0.1.5 line) has no such method, and the schema must stay byte-identical
3808
+ * to the unmarked original there: hand-writing `meta.volatile = true` would
3809
+ * bypass schemastery's own `validateVolatileSchema` checks and produce a schema
3810
+ * no 0.1.5 consumer understands — so on that line this degrades to an identity
3811
+ * no-op.
3812
+ *
3813
+ * Without the marker, 0.1.7's settings write gate REJECTS every write with
3814
+ * `Plugin entry "trae" has no volatile fields` while the scope's `set()` still
3815
+ * resolves — so the card would look like it saved and silently revert.
3816
+ *
3817
+ * Exported so BOTH arms are testable on any machine: the capability is decided
3818
+ * by whichever schemastery the dependency tree resolves, so without a seam the
3819
+ * no-op arm would silently stop being exercised the moment the pin moved up.
3820
+ */
3821
+ function asVolatile(schema) {
3822
+ if (typeof schema.volatile === "function") return schema.volatile();
3823
+ return schema;
3824
+ }
3759
3825
  const modelConfig = z.object({
3760
3826
  id: z.string().required(),
3761
3827
  name: z.string().required(),
@@ -3777,17 +3843,17 @@ const accountSelectionConfig = z.object({
3777
3843
  ai: z.string().description("Selected international account id (never a token)")
3778
3844
  });
3779
3845
  const Config = z.object({
3780
- authFile: z.string().description("Optional Trae storage.json path override"),
3781
- edition: z.union([
3846
+ authFile: asVolatile(z.string().description("Optional Trae storage.json path override")),
3847
+ edition: asVolatile(z.union([
3782
3848
  "auto",
3783
3849
  "cn",
3784
3850
  "sg",
3785
3851
  "solo",
3786
3852
  "solo-sg"
3787
- ]).default("auto").description("Trae edition hint"),
3853
+ ]).default("auto").description("Trae edition hint")),
3788
3854
  accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
3789
- accounts: accountSelectionConfig.description("Per-region account selections, keyed cn | ai"),
3790
- regions: z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai"),
3855
+ accounts: asVolatile(accountSelectionConfig.description("Per-region account selections, keyed cn | ai")),
3856
+ regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai")),
3791
3857
  lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
3792
3858
  enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
3793
3859
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
@@ -3846,7 +3912,7 @@ function apply(ctx, config) {
3846
3912
  */
3847
3913
  let legacyAccountRegion;
3848
3914
  const effectiveAccountFor = (region, value) => {
3849
- const explicit = value.accounts?.[region];
3915
+ const explicit = unwrapVolatile(value.accounts)?.[region];
3850
3916
  if (explicit !== void 0) return explicit;
3851
3917
  return legacyAccountRegion === region ? value.accountId : void 0;
3852
3918
  };
@@ -3887,10 +3953,12 @@ function apply(ctx, config) {
3887
3953
  try {
3888
3954
  preferred = (await store.current())?.edition;
3889
3955
  } catch {}
3890
- const hint = (config.edition !== void 0 && config.edition !== "auto" ? config.edition : void 0) ?? preferred;
3891
- return resolveTraeIdentity(config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (hint !== void 0 ? item.edition === hint : regionOfEdition(item.edition) === region)) : [{
3956
+ const authFile = unwrapVolatile(config.authFile);
3957
+ const edition = unwrapVolatile(config.edition);
3958
+ const hint = (edition !== void 0 && edition !== "auto" ? edition : void 0) ?? preferred;
3959
+ return resolveTraeIdentity(authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (hint !== void 0 ? item.edition === hint : regionOfEdition(item.edition) === region)) : [{
3892
3960
  edition: hint ?? (region === "ai" ? "sg" : "solo"),
3893
- path: config.authFile,
3961
+ path: authFile,
3894
3962
  source: "desktop"
3895
3963
  }], hint ?? (region === "ai" ? "solo-sg" : "cn"));
3896
3964
  };
@@ -3912,8 +3980,8 @@ function apply(ctx, config) {
3912
3980
  };
3913
3981
  const store = new TraeCredentialStore({
3914
3982
  region,
3915
- ...config.authFile === void 0 ? {} : { storagePath: config.authFile },
3916
- edition: config.edition ?? "auto",
3983
+ ...unwrapVolatile(config.authFile) === void 0 ? {} : { storagePath: unwrapVolatile(config.authFile) },
3984
+ edition: unwrapVolatile(config.edition) ?? "auto",
3917
3985
  refresh: async (credential) => refreshTraeCredential(credential, void 0, await refreshDeviceSafe())
3918
3986
  });
3919
3987
  const solo = new TraeSoloUpstreamClient({
@@ -4041,7 +4109,7 @@ function apply(ctx, config) {
4041
4109
  const applySelection = (value) => {
4042
4110
  for (const region of REGION_KEYS) {
4043
4111
  const stack = stacks[region];
4044
- stack.store.setSource(value.authFile, value.edition ?? "auto", effectiveAccountFor(region, value));
4112
+ stack.store.setSource(unwrapVolatile(value.authFile), unwrapVolatile(value.edition) ?? "auto", effectiveAccountFor(region, value));
4045
4113
  stack.catalog.set(configuredModels(value, region));
4046
4114
  stack.invalidateAdapter();
4047
4115
  }
@@ -4070,7 +4138,18 @@ function apply(ctx, config) {
4070
4138
  const legacyInstallSettingsSection = dshSettings.installSettingsSection;
4071
4139
  if (typeof legacyInstallSettingsSection === "function") legacyInstallSettingsSection(ctx, TRAE_SETTINGS_NS, Config, config, sectionHooks);
4072
4140
  else ctx.inject(["settings"], (settingsCtx) => {
4073
- settingsCtx.settings.installSection(ctx, TRAE_SETTINGS_NS, Config, config, sectionHooks);
4141
+ const settings = settingsCtx.settings;
4142
+ if (typeof settings.configure === "function") {
4143
+ settings.configure({ auto: true }, ctx.fiber);
4144
+ return;
4145
+ }
4146
+ if (typeof settings.installSection === "function") {
4147
+ const entry = unwrapVolatileDeep(config);
4148
+ settings.installSection(ctx, TRAE_SETTINGS_NS, Config, entry, sectionHooks);
4149
+ }
4150
+ });
4151
+ ctx.on("loader/volatile-update", () => {
4152
+ applySelection(current());
4074
4153
  });
4075
4154
  let stopped = false;
4076
4155
  ctx.effect(() => () => {
@@ -4153,4 +4232,4 @@ function apply(ctx, config) {
4153
4232
  });
4154
4233
  }
4155
4234
  //#endregion
4156
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, withTraeRegion };
4235
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
4
  "description": "Connect locally signed-in Trae models to DeepSeek Harness, with a usage/credits summary and daily check-in claiming.",
5
- "version": "2.1.0",
5
+ "version": "2.2.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -100,7 +100,7 @@
100
100
  "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
101
101
  "@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.2",
102
102
  "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
103
- "@deepseek-ai/schemastery": "3.18.2",
103
+ "@deepseek-ai/schemastery": "3.18.4",
104
104
  "@earendil-works/pi-ai": "^0.85.1",
105
105
  "@testing-library/dom": "^10.4.2",
106
106
  "@testing-library/react": "^16.3.3",