dsh-connect-trae 3.4.3 → 3.5.1

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,107 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.1 (2026-10-16)
4
+
5
+ > **DeepSeek-V4 四个模型行不再必然报 4001。** 本版修一处 wire 名单合并的方向错误:
6
+ > 同名的多行里,旧代码让**最后**一行取胜,于是一个元数据残缺的小写尾行抢走了展示名,
7
+ > 桥接层把 `config_name` 改写成了上游不认的小写拼写。改为**每张映射表先到先得**后,
8
+ > 规范行留在原位,不再发生任何改写。
9
+
10
+ ### 修复
11
+
12
+ - **DeepSeek-V4 四行(`DeepSeek-V4-Flash` / `-Flash-Official` / `-Pro` / `-Pro-Official`)选中发送必报 4001**(issue #38):
13
+ - **上游名单的形状**:CN 区 wire(`get_detail_param`)把同一族列了**三行**,共享同一个展示名,其中尾行是
14
+ 元数据残缺的小写行 —— `DeepSeek-V4-Flash` 展示名同时对应 `DeepSeek-V4-Flash-Official`、
15
+ `DeepSeek-V4-Flash`、`deepseek-v4-flash` 三个 id;`DeepSeek-V4-Pro` 完全同构。
16
+ - **改坏它的那一步**:`mergeTraeModelSources` 用 `wireByName.set(...)` / `wireById.set(...)`
17
+ **无条件写入**,所以后到的那一行会覆盖先到的同名键。尾行的 `name` 正好是小写 id,
18
+ 于是 `displayKey("DeepSeek-V4-Flash")` 被它抢占,桥接层据此把 `config_name` 改写成
19
+ `deepseek-v4-flash`,上游回 `4001 the param is invalid`。
20
+ - **为什么 `-Official` 行也一起废**:`TraeSoloBridge` 在 catalog 取不到 `wireConfigName` 时
21
+ 会退回 `wireResolver(entry?.name)`,而 `entry.name` 同样是 `DeepSeek-V4-Flash`,
22
+ 于是又落到那一行上 —— **两行一起废**。
23
+ - **修法**:两张映射表都改成**先到先得**(`if (!map.has(key)) map.set(...)`)。
24
+ `displayKey("DeepSeek-V4-Flash")` 于是命中 id 完全相同的规范行,该行的 `config_name`
25
+ 本就是 `DeepSeek-V4-Flash`(与展示名一致),**不再发生改写**。
26
+ - **本机 CN 实测**(插件真实请求形状 `{messages, model, config_name, function, stream}`):
27
+ - 修前:`DeepSeek-V4-Flash` → 发出 `deepseek-v4-flash` → 流内 `4001`;`DeepSeek-V4-Pro` 同;
28
+ - 修后:四行发出 `DeepSeek-V4-Flash` / `DeepSeek-V4-Pro`(`-Official` 两行发出自身 id)→ **HTTP 200 + 完整正文**;
29
+ - 对照探测:大写能通、小写必 `4001`,`glm-5.2` 作基线正常 —— 证实「号码牌本身是好的,是被改写坏的」。
30
+
31
+ > 注意:`4001` 是**流内** `event:error`(外层 HTTP 仍是 200),所以只看状态码会把它误判成成功。
32
+
33
+ ### 测试
34
+
35
+ - 新增 2 例回归:`mergeTraeModelSources` 在「同名 + 尾行小写」的 wire 上必须让规范行取胜;
36
+ 以及「按展示名 join」在改为先到先得后仍然生效(防止过度修正)。
37
+ 两例在旧代码上均失败、在新代码上通过。
38
+ - 全仓 **619** 例测试通过。
39
+
40
+ ## 3.5.0 (2026-10-14)
41
+
42
+ > **积分读数不再只有一个固定位置。** 本版向同族 `dsh-connect-workbuddy` 学来它的 issue #62
43
+ > 修法:把「读数显示在哪里」做成每个区域一个选项,默认值与升级前完全一致。
44
+
45
+ ### 新增
46
+
47
+ - **输入框读数的显示位置可选:输入框工具栏 / 输入框下方**(学自 `dsh-connect-workbuddy` issue #62):
48
+ - **为什么需要**:那个位置本身是脆的。`conversation.input.left`(输入框工具栏)是一个**共享的 list 插槽**,
49
+ 外壳自己的控件和**任何其他 bundle** 都注册在那里,而 list 插槽**无法保证每个占用者都可见** ——
50
+ 同族的报告者装了第三方插件(`dsh-auto-continue`)之后,积分读数就**整个消失了**。
51
+ 本插件其实已经因为这个原因丢过一次这行读数:它早年放在**侧边栏底部**,那是一个共享的**根级行**,
52
+ 与 `dsh-connect-workbuddy` 的两行并排时被挤成只剩一个「Tr」的残片(2.12.0 才迁到输入框工具栏)。
53
+ **同一个坑踩了两次,这次把位置交给用户。**
54
+ - **两个席位**:
55
+ - `composer`(**默认**)—— 输入框工具栏 `conversation.input.left`,与模型选择同一行,即现在的样子;
56
+ - `stats` —— 输入框下方 `conversation.composer.dock`,外壳自己渲染「轮次 / 步数 · tok/s · token ·
57
+ 缓存命中」那一行;`order: 10` 排在 shell 统计**之后**,所以官方那几颗统计仍在最左,读数是**追加**而不是顶替。
58
+ - **判定顺序:归属 → 区域开关 → 显示开关 → 席位。** 席位排在**最后**,所以一个被关掉的区域在
59
+ **两个席位都不渲染**,而不是先在非当前席位画一帧再被拒。
60
+
61
+ ### 修复
62
+
63
+ - **区域被关闭后,它的积分读数仍在输入框工具栏里渲染**(本次移植时由新守卫发现):
64
+ - 把某个区域的供应商关掉时,Host 会把该区域的 adapter 路由与可配置供应商条目**一并撤下**,
65
+ 模型选择器里也不再有它 —— 但 gate 只检查了「显示积分」那个开关,**从未检查区域开关**。
66
+ 于是一个已被关闭的区域只要模型投影还指着它的 provider,读数就会继续画在那里,
67
+ 并且它的取数请求注定失败。
68
+ - 现按 Host 的同一套 opt-out 规则(`regionEnabledOf`:只有显式 `false` 才算关闭)在 gate 里补上这一判。
69
+ 这与卡片上那个区域开关**用的是同一个 reader**,所以卡片和 gate 不可能对「这个区域开没开」产生分歧。
70
+ - 同族的 gate 一直有这一判,是本插件漏了。
71
+
72
+ ### 设计取舍(两条,都写下来免得后人改回去)
73
+
74
+ - **两个席位都无条件注册,决定权在 gate 里一处**(席位不符就 `return null`)。
75
+ 若改成「按设置只注册当前席位」,则每次改设置都要**重新注册插槽** —— 而本仓库的文件头已经写明
76
+ 这种绑定是脆的(rc.6→rc.7 的 `id`/`order` → `key`/`priority` 改名就是同类问题),
77
+ 且用户是「一边看着结果一边改」的。现在改设置只是一次 settings 写入。
78
+ - **卡片控件读的是设置文档,不是 Host 的 usage 应答**(这里**故意与同族不同**)。
79
+ 同族那样做是因为它的 0.1.7 部署上浏览器设置镜像**实测是过期的那份**;本仓库没有复现到这种过期,
80
+ 而读镜像才能让控件**首帧就是对的** —— 只靠 usage 文档喂值的控件会先停在默认值上等一次网络往返,
81
+ 这正是「看起来没生效」的观感。这一点有守卫钉住(见下)。
82
+
83
+ ### 测试
84
+
85
+ - **+19**(全仓 **598 → 617**):
86
+ - `tests/credit-position.spec.tsx`(新增 13 例):默认 `composer`(含设置存在之前的 profile、
87
+ 手改的未知值 `'sidebar'`);**按区域**而非全局读取;整段设置与裸 `regions` 映射两种入参都接受
88
+ (传错形状在本卡片上真的出过货);写入只改该区域该字段(模型列表 / 预算 / enabled / 兄弟区域都不动);
89
+ 席位与两个开关相互独立;gate 在**指定席位渲染 / 另一席位渲染 0 个 / 两个席位都不渲染(隐藏或区域关闭)/
90
+ 别的 provider 两个席位都不渲染 / 按会话所指区域取席位(CN 与 AI 各在各自席位)**;
91
+ 以及 Host 侧 `creditPosition()` 与浏览器侧 `regionCreditPositionOf()` **两边答案一致**(含 schema 默认值)。
92
+ - `tests/card-composer-credits-switch.spec.tsx`(新增 6 例):控件默认值、**跟随标签页切换**
93
+ (按区域而非一个全局值)、写入是**整槽合并**(不丢模型选择、不动另一个区域)、
94
+ 区域关闭时**禁用而不是隐藏**(控件消失会读成功能没了)、与显示开关相互独立(隐藏后仍保留所选席位)、
95
+ 只读 scope 拒绝写入。
96
+ - `tests/client-activation.spec.tsx`(更新 2 处 + 新增 1 条断言):插槽清单由 3 个变 4 个,
97
+ 并**钉住两个席位的 slot id 互不相同** —— 两个注册共用一个 id 会让后一个静默替换前一个,
98
+ 于是设置能选到一个已不在册的席位、读数反而彻底消失(正是本特性要修的那个缺陷)。
99
+ 测试替身原先只认 `key` 字段,现在同时认 `id`/`key`(真实的 0.1.7 注册表两者都用),
100
+ 否则一条关于 slot 身份的新断言会静默读到 `undefined` 而假绿。
101
+ - **变异验证**(各自变红后恢复):删掉 gate 的席位判定 → 4 例红;把读取的默认值去掉(升级前的 profile
102
+ 读成 `undefined`)→ 4 例红;只注册一个席位 → 2 例红;控件不读所存席位 → 2 例红。
103
+ - 构建产物核对:`lib/client.js` 内两个插槽名与两个 slot id 均在。
104
+
3
105
  ## 3.4.3 (2026-10-13)
4
106
 
5
107
  > **保存模型列表时终于有了「正在进行」的反馈。** 本版对齐同族 `dsh-connect-workbuddy` 4.4.2
package/README.en.md CHANGED
@@ -51,6 +51,8 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
51
51
 
52
52
  <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="536" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
53
53
 
54
+ **Where that readout sits is a choice** (learned from the sibling `dsh-connect-workbuddy` issue #62). The default is the **composer toolbar** (the position described above); if another plugin occupies it and the readout does not appear, move it **below the input box** — the row carrying the round / step / token statistics, where the readout is APPENDED after the shipped figures rather than replacing them. The seat is set **per region** (CN and international are stored separately), in the card's settings block next to the "show credits in the composer row" checkbox. Why a second seat exists at all: the composer toolbar is a **shared list slot** that the shell's own controls and every other plugin register into, and a shared slot **cannot promise that every occupant is visible** — the report behind that issue is a readout that vanished entirely once a third-party plugin was installed (this plugin has been squeezed out of a shared row once before too, down to a clipped "Tr" at the sidebar foot).
55
+
54
56
  - **Secure loopback shim** — one random port + in-process random secret per region; the real Trae token is never handed to pi-ai.
55
57
 
56
58
  ## How it works
package/README.md CHANGED
@@ -52,6 +52,8 @@
52
52
 
53
53
  <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="536" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
54
54
 
55
+ **读数显示在哪里可以选**(学自同族 `dsh-connect-workbuddy` issue #62)。默认在**输入框工具栏**(即上面说的位置);如果那里被别的插件占了、读数不显示,改成**输入框下方**——那是「轮次 / 步数 / token」统计所在的那一行,读数是**追加**在官方统计之后,不会顶掉它们。位置**按区域分别设置**(国内版与国际版各存各的),在卡片「设置」区紧挨「在输入框工具栏显示积分读数」那个勾选框。之所以要有第二个位置:输入框工具栏是**共享的 list 插槽**,外壳自己的控件和其他插件都注册在那里,而共享插槽**无法保证每个占用者都可见**——同族的报告者装了第三方插件后读数就整个消失了(本插件早年放在侧边栏底部时也被挤成过一个「Tr」的残片)。
56
+
55
57
  - **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 Trae token 不交给 pi-ai。
56
58
 
57
59
  ## 工作原理
@@ -112,6 +114,12 @@ Trae does not serve this model under the SOLO function the request used
112
114
 
113
115
  即「上游不提供该模型」被如实说明,而不是静默失败或像网络故障。**官方把它们开放到 SOLO 通道后,插件会自动可用**(判据就是能不能拿到调用 id),无需升级插件。
114
116
 
117
+ > **同一条报错的另一种成因(3.5.1 已修,issue #38)**:`DeepSeek-V4` 四行曾报**同样的 4001**,但成因相反 ——
118
+ > 它们在上游名单里**本来可以调用**,只是 CN 区的 wire 把同一族列了三行且共享展示名,旧代码让**最后**一行
119
+ > (元数据残缺的小写行 `deepseek-v4-flash`)抢走了展示名键,桥接层据此把 `config_name` 改写成小写拼写,
120
+ > 上游拒收。**判别方法**:看这个模型在上游名单里有没有调用 id —— 有而报 4001,就是这一类。
121
+ > 3.5.1 起两张映射表都改为先到先得,不再发生改写,四行已实测恢复正常。
122
+
115
123
  > **为什么现在会显示(2026-10-02 起的策略)**:此前这类「目录里有、但拿不到调用 id」的模型会被**直接隐藏**,理由是「放出来只会报错」。但可调用名单是**按账号档位**下发的——在免费账号上测不到,不等于付费账号用不了(issue #19 的报告人就是 Pro:他的账号能调用本机免费账号被门禁挡住的模型)。**隐藏用户自己 Trae IDE 里有的模型,比让他看到一条明确的错误更糟**,所以改为:全部呈现 + 如实报错。
116
124
 
117
125
  > `deepseek-v4.1-flash` 曾在此列——`docs/DS41_CALLABILITY.md`(2026-09-15 取证)实测 8 个 SOLO function
package/lib/client.js CHANGED
@@ -203,6 +203,39 @@ window.__ModuleLoader__.load({
203
203
  });
204
204
  }
205
205
  /**
206
+ * Which seat one region's readout uses, defaulting to the composer row.
207
+ *
208
+ * Anything other than an explicit `'stats'` reads as `'composer'`, which covers
209
+ * the two cases that matter: a profile written BEFORE this setting existed (the
210
+ * composer row was the only seat, so an upgrade moves nothing on screen), and a
211
+ * hand-edited value the schema does not know. Falling back rather than
212
+ * rendering nowhere is deliberate — an unrecognized value must not make the
213
+ * readout disappear, which is the very defect this setting exists to fix.
214
+ *
215
+ * `value` may be the whole settings section or the `regions` map (see
216
+ * {@link regionsMapOf}).
217
+ */
218
+ function regionCreditPositionOf(value, region) {
219
+ return regionSlotOf(value, region)["creditPosition"] === "stats" ? "stats" : "composer";
220
+ }
221
+ /**
222
+ * Build the next `regions` settings value for one region's credit seat.
223
+ *
224
+ * Same merge rule as every sibling region write: only the target region's
225
+ * `creditPosition` changes, so a seat change cannot drop that slot's model
226
+ * directory, selection, budgets or enabled flag, nor touch the other region.
227
+ *
228
+ * `value` may be the whole settings section or the `regions` map; the RETURN
229
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
230
+ * 'regions', ...)` needs.
231
+ */
232
+ function nextRegionCreditPosition(value, region, position) {
233
+ return nextRegionSlots(regionsMapOf(value), region, {
234
+ ...regionSlotOf(value, region),
235
+ creditPosition: position
236
+ });
237
+ }
238
+ /**
206
239
  * Whether Trae's plan identity says this account is on a paying plan.
207
240
  *
208
241
  * `has_package` alone is NOT that question — it reports whether a package
@@ -299,6 +332,16 @@ window.__ModuleLoader__.load({
299
332
  .dsm-trae-pref b{font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6)}
300
333
  .dsm-trae-pref-hint{color:var(--dsw-alias-label-tertiary,#9aa0a8)}
301
334
  .dsm-trae-pref span{min-width:0}
335
+ /* The credit readout's seat picker (learned from workbuddy #62). Its OWN label
336
+ class, deliberately not the plain .dsm-trae-pref the two checkboxes use: a
337
+ query for "the composer-credits switch" must not be able to pick this control
338
+ up, because the two are different widgets and a test selecting by position
339
+ would otherwise start asserting about a <select>.
340
+ The select keeps its own class for the same reason, and takes the checkbox
341
+ column's width so it reads as a sibling of the box rather than a stray row. */
342
+ .dsm-trae-credit-position-pref{align-items:flex-start}
343
+ .dsm-trae-credit-position{flex:none;max-width:150px;margin-top:-1px;padding:2px 6px;font-size:12px}
344
+ .dsm-trae-credit-position:disabled{cursor:default}
302
345
  /* The card's checkboxes are the platform's own, styled only with
303
346
  accent-color 鈥?the SAME idiom .dsm-trae-mainui-switch-label above already
304
347
  uses for the composer-line switch, and the one the sibling
@@ -1123,6 +1166,49 @@ window.__ModuleLoader__.load({
1123
1166
  setSettingsRevision((value) => value + 1);
1124
1167
  }), [settingsScope]);
1125
1168
  /**
1169
+ * WHERE the active tab's credit readout sits (learned from workbuddy #62).
1170
+ *
1171
+ * Read from the committed SETTINGS document, through the same
1172
+ * `regionCreditPositionOf` helper the Host uses — so the control reads
1173
+ * instantly and can never disagree with the readout's own gate, which reads
1174
+ * the same scope. This mirrors how the show/hide box beside it works
1175
+ * (`committedSettings`), rather than how the sibling workbuddy card does it:
1176
+ * that card reads the Host's usage answer because a measured defect on its
1177
+ * 0.1.7 deployments left the browser settings mirror for its namespace STALE.
1178
+ * No such staleness is reproducible here, and reading the mirror eagerly is
1179
+ * what keeps this control correct on first paint — a picker fed only by the
1180
+ * usage document would sit on the default until a fetch landed, which is the
1181
+ * "looks like it did nothing" failure this control must not have.
1182
+ *
1183
+ * `settingsRevision` is read through the setter above, and the whole block
1184
+ * re-renders when the scope notifies, so a write made elsewhere (another
1185
+ * window, the DSH settings form) converges here.
1186
+ */
1187
+ const creditPositionOn = (item) => regionCreditPositionOf(settingsScope?.getSnapshot().value, item);
1188
+ /**
1189
+ * Move one region's credit readout between its two seats.
1190
+ *
1191
+ * Same shape as every other region write on this card: the whole target slot
1192
+ * goes in (so a seat change cannot drop that region's model picks, budgets or
1193
+ * enabled flag, nor touch the sibling region), the write is read-back
1194
+ * verified, and the Host is re-read afterwards. The re-read is load-bearing
1195
+ * here — the control renders the Host's answer, so without it the picker
1196
+ * keeps showing the seat it had and the change looks like it did nothing.
1197
+ */
1198
+ const changeCreditPosition = async (item, position) => {
1199
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
1200
+ setSavingSettings(true);
1201
+ setWriteError(void 0);
1202
+ try {
1203
+ await writeSettingsField(settingsScope, "regions", nextRegionCreditPosition(settingsScope.getSnapshot().value, item, position), (readBack) => regionCreditPositionOf(readBack, item) === position);
1204
+ await refreshUsage(item);
1205
+ } catch (error) {
1206
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
1207
+ } finally {
1208
+ if (mounted.current) setSavingSettings(false);
1209
+ }
1210
+ };
1211
+ /**
1126
1212
  * Whether one region's provider is switched on, read off the SAME committed
1127
1213
  * settings document the Host reads (`regionEnabledOf` mirrors the Host's
1128
1214
  * `regionStateOf` opt-out rule: only an explicit `false` disables). Reading
@@ -1913,57 +1999,87 @@ window.__ModuleLoader__.load({
1913
1999
  }),
1914
2000
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1915
2001
  className: "dsm-trae-prefs",
1916
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1917
- className: "dsm-trae-pref",
1918
- title: t("row.followAppLoginHint"),
1919
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1920
- type: "checkbox",
1921
- checked: settingsEdits.followAppLogin,
1922
- disabled: settingsScope?.getSnapshot().writable !== true || savingSettings,
1923
- "aria-describedby": followHintId,
1924
- onChange: (event) => {
1925
- writePreference("followAppLogin", event.currentTarget.checked);
1926
- }
1927
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1928
- id: followHintId,
1929
- children: [
1930
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
1931
- id: followLabelId,
1932
- children: t("row.followAppLogin")
1933
- }),
1934
- " ",
1935
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1936
- className: "dsm-trae-pref-hint",
1937
- children: t("row.followAppLoginHint")
1938
- })
1939
- ]
1940
- })]
1941
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1942
- className: "dsm-trae-pref",
1943
- title: t(activeRegion === "cn" ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi"),
1944
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1945
- type: "checkbox",
1946
- checked: settingsEdits.showPointsInMainUi,
1947
- disabled: settingsScope?.getSnapshot().writable !== true || savingSettings,
1948
- "aria-describedby": creditsHintId,
1949
- onChange: (event) => {
1950
- writePreference("showPointsInMainUi", event.currentTarget.checked);
1951
- }
1952
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1953
- id: creditsHintId,
1954
- children: [
1955
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
1956
- id: creditsLabelId,
1957
- children: t("row.showPointsInMainUi")
1958
- }),
2002
+ children: [
2003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2004
+ className: "dsm-trae-pref",
2005
+ title: t("row.followAppLoginHint"),
2006
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2007
+ type: "checkbox",
2008
+ checked: settingsEdits.followAppLogin,
2009
+ disabled: settingsScope?.getSnapshot().writable !== true || savingSettings,
2010
+ "aria-describedby": followHintId,
2011
+ onChange: (event) => {
2012
+ writePreference("followAppLogin", event.currentTarget.checked);
2013
+ }
2014
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2015
+ id: followHintId,
2016
+ children: [
2017
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
2018
+ id: followLabelId,
2019
+ children: t("row.followAppLogin")
2020
+ }),
2021
+ " ",
2022
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2023
+ className: "dsm-trae-pref-hint",
2024
+ children: t("row.followAppLoginHint")
2025
+ })
2026
+ ]
2027
+ })]
2028
+ }),
2029
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2030
+ className: "dsm-trae-pref",
2031
+ title: t(activeRegion === "cn" ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi"),
2032
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2033
+ type: "checkbox",
2034
+ checked: settingsEdits.showPointsInMainUi,
2035
+ disabled: settingsScope?.getSnapshot().writable !== true || savingSettings,
2036
+ "aria-describedby": creditsHintId,
2037
+ onChange: (event) => {
2038
+ writePreference("showPointsInMainUi", event.currentTarget.checked);
2039
+ }
2040
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2041
+ id: creditsHintId,
2042
+ children: [
2043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
2044
+ id: creditsLabelId,
2045
+ children: t("row.showPointsInMainUi")
2046
+ }),
2047
+ " ",
2048
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2049
+ className: "dsm-trae-pref-hint",
2050
+ children: t(activeRegion === "cn" ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi")
2051
+ })
2052
+ ]
2053
+ })]
2054
+ }),
2055
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2056
+ className: "dsm-trae-pref dsm-trae-credit-position-pref",
2057
+ title: t("row.creditPositionHint"),
2058
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2059
+ className: "dsm-trae-credit-position",
2060
+ value: activeRegionOn ? creditPositionOn(activeRegion) : "composer",
2061
+ disabled: !activeRegionOn || settingsScope?.getSnapshot().writable !== true || savingSettings,
2062
+ "aria-label": t("row.creditPositionAria", { region: activeRegion === "cn" ? t("row.tabCn") : t("row.tabAi") }),
2063
+ onChange: (event) => {
2064
+ changeCreditPosition(activeRegion, event.currentTarget.value === "stats" ? "stats" : "composer");
2065
+ },
2066
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2067
+ value: "composer",
2068
+ children: t("row.creditPositionComposer")
2069
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2070
+ value: "stats",
2071
+ children: t("row.creditPositionStats")
2072
+ })]
2073
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
2074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t("row.creditPositionLabel") }),
1959
2075
  " ",
1960
2076
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1961
2077
  className: "dsm-trae-pref-hint",
1962
- children: t(activeRegion === "cn" ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi")
2078
+ children: t("row.creditPositionHint")
1963
2079
  })
1964
- ]
1965
- })]
1966
- })]
2080
+ ] })]
2081
+ })
2082
+ ]
1967
2083
  }),
1968
2084
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1969
2085
  className: "dsm-trae-accounts",
@@ -2975,6 +3091,23 @@ window.__ModuleLoader__.load({
2975
3091
  * row is shared with the shell's own permission/agent/model controls, so the
2976
3092
  * readout must be asked for, not imposed — 2.13.0 rendered it unconditionally
2977
3093
  * and 2.13.3 restored the opt-in after the user went looking for the switch.
3094
+ *
3095
+ * WHY THERE IS A SECOND SEAT (learned from dsh-connect-workbuddy issue #62).
3096
+ * That row is a LIST seat, so it is shared not only with the shell's own
3097
+ * controls but with any other plugin that registers there — and one report is
3098
+ * exactly that: with a third-party plugin installed the readout stopped
3099
+ * appearing. A list seat cannot promise that every occupant is visible, so the
3100
+ * seat becomes the USER's choice: `composer` (default, unchanged for anyone who
3101
+ * does not choose) or `stats`, the ambient dock below the composer card where
3102
+ * the shell renders its own round/step/token pills. This plugin has already
3103
+ * lost this readout once to a shared seat — it used to live at the sidebar
3104
+ * foot, where it was clipped to "Tr" beside dsh-connect-workbuddy's rows.
3105
+ *
3106
+ * The gate is registered in BOTH seats and renders in the one the setting
3107
+ * names, which keeps the decision in ONE function instead of re-registering
3108
+ * slots when the setting changes — and this repository already documents that
3109
+ * kind of binding as fragile (the rc.6→rc.7 `id`/`order` → `key`/`priority`
3110
+ * rename). Registration is unconditional; only the render is conditional.
2978
3111
  */
2979
3112
  /**
2980
3113
  * The provider route the session is currently pointed at.
@@ -2987,6 +3120,17 @@ window.__ModuleLoader__.load({
2987
3120
  return typeof provider === "string" && provider !== "" ? provider : void 0;
2988
3121
  }
2989
3122
  /**
3123
+ * The settings tree this plugin writes, unwrapped to a plain object.
3124
+ *
3125
+ * `unwrapVolatileDeep` first: on DSH 0.1.7 the scope's value is a live
3126
+ * cosmokit reference, and reading a field off it without unwrapping yields a
3127
+ * proxy whose `===` comparison against a string is false — which would make the
3128
+ * seat test below reject EVERY seat and silently hide the readout.
3129
+ */
3130
+ function scopeValueOf(scope) {
3131
+ return scope === void 0 ? void 0 : unwrapVolatileDeep(scope.getSnapshot().value);
3132
+ }
3133
+ /**
2990
3134
  * Read the composer-readout switch out of a committed settings snapshot.
2991
3135
  *
2992
3136
  * Absent means OFF, matching the host default (`false`): a scope that has not
@@ -2994,11 +3138,10 @@ window.__ModuleLoader__.load({
2994
3138
  * the composer toolbar that the user never asked for.
2995
3139
  */
2996
3140
  function composerPointsEnabledOf(scope) {
2997
- if (scope === void 0) return false;
2998
- return unwrapVolatileDeep(scope.getSnapshot().value)?.showPointsInMainUi === true;
3141
+ return scopeValueOf(scope)?.showPointsInMainUi === true;
2999
3142
  }
3000
3143
  function ComposerPointsGate(props) {
3001
- const { t, settingsScope, useProjection } = props;
3144
+ const { t, position, settingsScope, useProjection } = props;
3002
3145
  const [enabled, setEnabled] = (0, react.useState)(() => composerPointsEnabledOf(settingsScope));
3003
3146
  (0, react.useEffect)(() => settingsScope?.subscribe(() => {
3004
3147
  setEnabled(composerPointsEnabledOf(settingsScope));
@@ -3008,6 +3151,8 @@ window.__ModuleLoader__.load({
3008
3151
  const provider = selectedProviderOf(projection);
3009
3152
  const region = provider === void 0 ? void 0 : TRAE_COMPOSER_PROVIDERS[provider];
3010
3153
  if (region === void 0) return null;
3154
+ if (!regionEnabledOf(scopeValueOf(settingsScope), region)) return null;
3155
+ if (regionCreditPositionOf(scopeValueOf(settingsScope), region) !== (position ?? "composer")) return null;
3011
3156
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerPoints, {
3012
3157
  t,
3013
3158
  provider,
@@ -3103,6 +3248,11 @@ window.__ModuleLoader__.load({
3103
3248
  "row.showPointsInMainUi": "Show credits in the composer row",
3104
3249
  "row.showPointsInMainUiHint": "A clickable readout in the composer tool row while a model from this plugin is selected: Trae CN · <balance> on the CN side, Trae Global · <plan> on the international one.",
3105
3250
  "row.showPointsInMainUiHintAi": "A clickable readout in the composer tool row while a model from this plugin is selected: Trae Global · <plan>.",
3251
+ "row.creditPositionLabel": "position",
3252
+ "row.creditPositionComposer": "composer toolbar",
3253
+ "row.creditPositionStats": "below the input box",
3254
+ "row.creditPositionAria": "Credit readout position for {region}",
3255
+ "row.creditPositionHint": "Where the credit readout sits. \"composer toolbar\" is the default (the row with the model picker). Choose \"below the input box\" — the row carrying the round / step / token statistics — if another plugin occupies the composer toolbar and the readout does not appear.",
3106
3256
  "composer.switchTo": "Switch to {account}",
3107
3257
  "composer.noAccounts": "No account in this region",
3108
3258
  "composer.panelTitle": "Trae credits",
@@ -3245,6 +3395,11 @@ window.__ModuleLoader__.load({
3245
3395
  "row.showPointsInMainUi": "在输入框工具栏显示积分读数",
3246
3396
  "row.showPointsInMainUiHint": "在输入框工具栏显示可点击读数(选中本插件模型时出现):国内版为「Trae CN · 余额」,国际版为「Trae 国际版 · 套餐」。",
3247
3397
  "row.showPointsInMainUiHintAi": "在输入框工具栏显示可点击读数(选中本插件模型时出现):国际版为「Trae 国际版 · 套餐」。",
3398
+ "row.creditPositionLabel": "显示位置",
3399
+ "row.creditPositionComposer": "输入框工具栏",
3400
+ "row.creditPositionStats": "输入框下方",
3401
+ "row.creditPositionAria": "{region} 的积分显示位置",
3402
+ "row.creditPositionHint": "积分显示在哪里。「输入框工具栏」是默认位置(与模型选择同一行)。如果装了别的插件后读数不显示,改成「输入框下方」—— 那是「轮次 / 步数 / token」统计所在的那一行。",
3248
3403
  "composer.switchTo": "切换到 {account}",
3249
3404
  "composer.noAccounts": "该区域暂无账号",
3250
3405
  "composer.panelTitle": "Trae 积分",
@@ -3444,24 +3599,37 @@ window.__ModuleLoader__.load({
3444
3599
  * Registration is unconditional; the gate reads the Host's modelSelection
3445
3600
  * projection and returns null for every other provider, so nothing is drawn
3446
3601
  * and no fetch loop starts while another provider is selected.
3602
+ *
3603
+ * THE SAME READOUT IS REGISTERED IN TWO SEATS (learned from workbuddy issue
3604
+ * #62), and BOTH registrations are unconditional too. Which one actually
3605
+ * draws is decided inside the gate by the per-region `creditPosition`
3606
+ * setting, so changing the setting is a settings write rather than a slot
3607
+ * re-registration — this file's own header records how fragile rebinding a
3608
+ * slot is across DSH lines (the rc.6→rc.7 `id`/`order` → `key`/`priority`
3609
+ * rename). The composer row is a LIST seat shared with every other bundle,
3610
+ * so a third party parking something there can crowd this readout out of
3611
+ * existence; the second seat is what keeps one plugin's presence from
3612
+ * deciding another's.
3447
3613
  */
3448
- const registerComposerPoints = () => {
3614
+ const registerPointsSlot = (slotName, id, order, position) => {
3449
3615
  try {
3450
- ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
3451
- name: "conversation.input.left",
3452
- id: "dsh-connect-trae-composer-points",
3453
- order: 4,
3616
+ ctx.slots.inject(slotName, () => ctx.slots.register({
3617
+ name: slotName,
3618
+ id,
3619
+ order,
3454
3620
  locale: namespace,
3455
3621
  inject: () => ({
3456
3622
  t,
3457
- settingsScope
3623
+ settingsScope,
3624
+ position
3458
3625
  })
3459
3626
  }, ComposerPointsGate));
3460
3627
  } catch (error) {
3461
- console.error("[dsh-connect-trae] composer points slot failed to register (host provider unaffected):", error);
3628
+ console.error(`[dsh-connect-trae] points slot "${slotName}" failed to register (host provider unaffected):`, error);
3462
3629
  }
3463
3630
  };
3464
- registerComposerPoints();
3631
+ registerPointsSlot("conversation.input.left", "dsh-connect-trae-composer-points", 4, "composer");
3632
+ registerPointsSlot("conversation.composer.dock", "dsh-connect-trae-stats-points", 10, "stats");
3465
3633
  } catch (error) {
3466
3634
  console.error("[dsh-connect-trae] client card failed to load (host provider unaffected):", error);
3467
3635
  }
package/lib/index.d.ts CHANGED
@@ -2134,6 +2134,51 @@ declare function regionEnabledOf(value: unknown, region: TraeRegion): boolean;
2134
2134
  * 'regions', ...)` needs.
2135
2135
  */
2136
2136
  declare function nextRegionEnabled(value: unknown, region: TraeRegion, enabled: boolean): Record<string, unknown>;
2137
+ /**
2138
+ * The two seats this region's credit readout can occupy.
2139
+ *
2140
+ * `composer` is the composer tool row (`conversation.input.left`); `stats` is
2141
+ * the ambient dock below the composer card (`conversation.composer.dock`),
2142
+ * beside the shell's own round/step/token pills.
2143
+ *
2144
+ * WHY THE SEAT IS A CHOICE AT ALL (learned from dsh-connect-workbuddy issue
2145
+ * #62): the composer tool row is a LIST seat, so it is shared not only with the
2146
+ * shell's own controls but with any other plugin that registers there — and a
2147
+ * list seat cannot promise that every occupant is visible. The report behind
2148
+ * that issue is exactly that: with a third-party plugin installed the readout
2149
+ * stopped appearing. A fixed seat therefore lets one plugin's presence decide
2150
+ * another's, so the seat belongs to the user.
2151
+ *
2152
+ * Named as a union rather than a boolean because a third seat is plausible and
2153
+ * a `showAtTop: boolean` would have to be MIGRATED rather than extended.
2154
+ */
2155
+ type TraeCreditPosition = 'composer' | 'stats';
2156
+ /**
2157
+ * Which seat one region's readout uses, defaulting to the composer row.
2158
+ *
2159
+ * Anything other than an explicit `'stats'` reads as `'composer'`, which covers
2160
+ * the two cases that matter: a profile written BEFORE this setting existed (the
2161
+ * composer row was the only seat, so an upgrade moves nothing on screen), and a
2162
+ * hand-edited value the schema does not know. Falling back rather than
2163
+ * rendering nowhere is deliberate — an unrecognized value must not make the
2164
+ * readout disappear, which is the very defect this setting exists to fix.
2165
+ *
2166
+ * `value` may be the whole settings section or the `regions` map (see
2167
+ * {@link regionsMapOf}).
2168
+ */
2169
+ declare function regionCreditPositionOf(value: unknown, region: TraeRegion): TraeCreditPosition;
2170
+ /**
2171
+ * Build the next `regions` settings value for one region's credit seat.
2172
+ *
2173
+ * Same merge rule as every sibling region write: only the target region's
2174
+ * `creditPosition` changes, so a seat change cannot drop that slot's model
2175
+ * directory, selection, budgets or enabled flag, nor touch the other region.
2176
+ *
2177
+ * `value` may be the whole settings section or the `regions` map; the RETURN
2178
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
2179
+ * 'regions', ...)` needs.
2180
+ */
2181
+ declare function nextRegionCreditPosition(value: unknown, region: TraeRegion, position: TraeCreditPosition): Record<string, unknown>;
2137
2182
  /** Subscription status of an international (ai) account, rendered instead of the CN credit packs. */
2138
2183
  interface TraeWebPayStatus {
2139
2184
  isDollarUsageBilling: boolean;
@@ -2399,6 +2444,29 @@ interface TraeRegionState {
2399
2444
  imageModelIds?: string[];
2400
2445
  /** Local DSH context budget per model in this region. */
2401
2446
  contextBudgets?: Record<string, number>;
2447
+ /**
2448
+ * WHERE this region's credit readout sits (learned from dsh-connect-workbuddy
2449
+ * issue #62).
2450
+ *
2451
+ * Two seats, because the first one is not always available:
2452
+ *
2453
+ * - `composer` — the composer tool row (`conversation.input.left`), shared
2454
+ * with the shell's own controls. It is the closer, more glanceable seat,
2455
+ * and it is the default so nothing moves for anyone who does not choose.
2456
+ * - `stats` — the ambient dock below the composer card
2457
+ * (`conversation.composer.dock`), where the shell renders its own
2458
+ * round/step/token pills. That row is a LIST seat whose shipped occupant
2459
+ * registers separately, so a third-party plugin that ALSO parks something
2460
+ * in the composer row cannot crowd this readout out of existence.
2461
+ *
2462
+ * The report behind this (workbuddy #62) was exactly that: with another
2463
+ * plugin installed, the readout vanished from the composer row. Making the
2464
+ * seat a choice is what keeps one plugin's presence from deciding another's.
2465
+ * Trae already lost this readout once to a shared seat — see the sibling
2466
+ * plugin's note about the sidebar foot, where this line was clipped to "Tr"
2467
+ * beside dsh-connect-workbuddy's rows — so the setting applies here too.
2468
+ */
2469
+ creditPosition?: TraeCreditPosition;
2402
2470
  }
2403
2471
  interface Config {
2404
2472
  authFile?: string;
@@ -2492,6 +2560,18 @@ declare function regionStateOf(config: Config, region: TraeRegion): TraeRegionSt
2492
2560
  * disagree about a region's state.
2493
2561
  */
2494
2562
  declare function regionEnabled(config: Config, region: TraeRegion): boolean;
2563
+ /**
2564
+ * WHERE one region's credit readout sits (learned from workbuddy #62).
2565
+ *
2566
+ * A local reader rather than a bare schema default, so a config written before
2567
+ * the setting existed reads as `composer` — the seat that was the only one, so
2568
+ * an upgrade moves nothing on screen — and so a hand-edited value the schema
2569
+ * does not know falls back to a seat that renders rather than to nowhere.
2570
+ *
2571
+ * Shared with the browser half by construction: the card renders the Host's
2572
+ * answer for this field, so the control cannot disagree with the readout.
2573
+ */
2574
+ declare function creditPosition(config: Config, region: TraeRegion): TraeCreditPosition;
2495
2575
  /**
2496
2576
  * Mark a schema's field as volatile.
2497
2577
  *
@@ -2510,4 +2590,4 @@ declare function asVolatile<S>(schema: S): S;
2510
2590
  declare const Config: z<Config>;
2511
2591
  declare function apply(ctx: Context$1, config: Config): void;
2512
2592
  //#endregion
2513
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, 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_STATE_DB_FILENAME, 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 TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, 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 TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
2593
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, 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_STATE_DB_FILENAME, 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 TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, 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 TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
package/lib/index.js CHANGED
@@ -222,10 +222,8 @@ function displayKey(name) {
222
222
  function mergeTraeModelSources(remote, wire) {
223
223
  const wireByName = /* @__PURE__ */ new Map();
224
224
  const wireById = /* @__PURE__ */ new Map();
225
- for (const model of wire) {
226
- wireByName.set(displayKey(model.name), model);
227
- wireById.set(displayKey(model.id), model);
228
- }
225
+ for (const model of wire) if (!wireById.has(displayKey(model.id))) wireById.set(displayKey(model.id), model);
226
+ for (const model of wire) if (!wireByName.has(displayKey(model.name))) wireByName.set(displayKey(model.name), model);
229
227
  const result = [];
230
228
  for (const model of remote) {
231
229
  const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
@@ -996,6 +994,39 @@ function nextRegionEnabled(value, region, enabled) {
996
994
  enabled
997
995
  });
998
996
  }
997
+ /**
998
+ * Which seat one region's readout uses, defaulting to the composer row.
999
+ *
1000
+ * Anything other than an explicit `'stats'` reads as `'composer'`, which covers
1001
+ * the two cases that matter: a profile written BEFORE this setting existed (the
1002
+ * composer row was the only seat, so an upgrade moves nothing on screen), and a
1003
+ * hand-edited value the schema does not know. Falling back rather than
1004
+ * rendering nowhere is deliberate — an unrecognized value must not make the
1005
+ * readout disappear, which is the very defect this setting exists to fix.
1006
+ *
1007
+ * `value` may be the whole settings section or the `regions` map (see
1008
+ * {@link regionsMapOf}).
1009
+ */
1010
+ function regionCreditPositionOf(value, region) {
1011
+ return regionSlotOf(value, region)["creditPosition"] === "stats" ? "stats" : "composer";
1012
+ }
1013
+ /**
1014
+ * Build the next `regions` settings value for one region's credit seat.
1015
+ *
1016
+ * Same merge rule as every sibling region write: only the target region's
1017
+ * `creditPosition` changes, so a seat change cannot drop that slot's model
1018
+ * directory, selection, budgets or enabled flag, nor touch the other region.
1019
+ *
1020
+ * `value` may be the whole settings section or the `regions` map; the RETURN
1021
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
1022
+ * 'regions', ...)` needs.
1023
+ */
1024
+ function nextRegionCreditPosition(value, region, position) {
1025
+ return nextRegionSlots(regionsMapOf(value), region, {
1026
+ ...regionSlotOf(value, region),
1027
+ creditPosition: position
1028
+ });
1029
+ }
999
1030
  //#endregion
1000
1031
  //#region src/decrypt.ts
1001
1032
  const TRAE_AUTH_STORAGE_KEY = "iCubeAuthInfo://icube.cloudide";
@@ -5619,6 +5650,20 @@ function regionEnabled(config, region) {
5619
5650
  return regionStateOf(config, region).enabled !== false;
5620
5651
  }
5621
5652
  /**
5653
+ * WHERE one region's credit readout sits (learned from workbuddy #62).
5654
+ *
5655
+ * A local reader rather than a bare schema default, so a config written before
5656
+ * the setting existed reads as `composer` — the seat that was the only one, so
5657
+ * an upgrade moves nothing on screen — and so a hand-edited value the schema
5658
+ * does not know falls back to a seat that renders rather than to nowhere.
5659
+ *
5660
+ * Shared with the browser half by construction: the card renders the Host's
5661
+ * answer for this field, so the control cannot disagree with the readout.
5662
+ */
5663
+ function creditPosition(config, region) {
5664
+ return regionStateOf(config, region).creditPosition === "stats" ? "stats" : "composer";
5665
+ }
5666
+ /**
5622
5667
  * Mark a schema's field as volatile.
5623
5668
  *
5624
5669
  * `volatile()` exists from schemastery 3.18.3; since 2.3.0 the peer range
@@ -5651,7 +5696,8 @@ const regionStateConfig = z.object({
5651
5696
  lastCatalog: z.array(modelConfig).default([]),
5652
5697
  enabledModelIds: z.array(z.string()).default([]),
5653
5698
  imageModelIds: z.array(z.string()).default([]),
5654
- contextBudgets: z.dict(z.number().step(1).min(1)).default({})
5699
+ contextBudgets: z.dict(z.number().step(1).min(1)).default({}),
5700
+ creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("积分读数显示位置:\"composer\" = 输入框工具栏(默认),\"stats\" = 输入框下方(与「轮次 / 步数 / token」统计同一行)。装了别的插件后读数不显示时改成 stats。")
5655
5701
  });
5656
5702
  const accountSelectionConfig = z.object({
5657
5703
  cn: z.string().description("Selected domestic (CN) account id (never a token)"),
@@ -6216,4 +6262,4 @@ function apply(ctx, config) {
6216
6262
  });
6217
6263
  }
6218
6264
  //#endregion
6219
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, 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_STATE_DB_FILENAME, 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, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
6265
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, 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_STATE_DB_FILENAME, 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, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
4
  "description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
5
- "version": "3.4.3",
5
+ "version": "3.5.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",