dsh-connect-trae 2.8.0 → 2.10.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,140 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.10.0 (2026-10-07)
4
+
5
+ > **新增:在 DSH 主界面侧边栏常驻一行「Trae通用积分」**([#26](https://github.com/dingminhua/dsh-connect-trae/issues/26),**实现由报告人 JiewiW 贡献**)。
6
+ >
7
+ > 看积分原先必须打开插件设置卡片、展开、找到额度区。现在侧边栏底部(工作区列表下方、设置上方)常驻一行「Trae通用积分:<数值>」与刷新按钮,**默认显示**,每 5 分钟自动更新,也可手动刷新。
8
+
9
+ > 这是 2.9.0 起五个版本合并后的首个公开版本:npm 上的 `latest` 此前仍是 2.8.0,因此**从 2.8.0 升级会一次拿到下面全部内容**。
10
+
11
+ ### Features
12
+
13
+ - **侧边栏积分行**:在 `sidebar.footer.action` 槽位渲染「Trae通用积分:<数值>」+ 「刷新」按钮。
14
+ - **默认显示**(opt-out):`showPointsInMainUi` 默认 `true`。
15
+ - **关闭即彻底消失**:行内的 `×` 或卡片里的复选框都能关闭,关闭后**侧边栏不留任何 DOM**,轮询一并停止。
16
+ - 开启后**先立即抓取一次,完成后再启动 5 分钟周期**的定时器,不叠加;关闭或卸载即清除定时器并中止在途请求。
17
+ - 抓取失败保留上一次数值并提示;刷新进行中忽略重复点击。
18
+ - 显示的是**通用积分**(`credits.generalAvailable`,SOLO 聊天实际消耗的那个池子),与卡片上的数字口径一致。
19
+ - 侧边栏收起(56px 导轨)时整行不渲染;国际版为订阅制、无可比数字,因此不显示。
20
+ - **开关在三处,写同一个字段**(`showPointsInMainUi`),永不冲突:侧边栏那一行的 `×`、卡片顶部、卡片「模型」区标题旁(在「从 Trae 刷新」「一键测试启用模型」左侧)。
21
+
22
+ ### 为什么不只用插件卡片里的复选框
23
+
24
+ 第一版把开关**只**放在插件卡片里,结果在装了 **dshmarket** 的宿主上等于不存在:市场插件接管了插件页,而它的 UI **完全不渲染插件自带的配置卡片**(其客户端 bundle 里 `plugins.row.config` / `plugins.bundle.config` **0 处引用**)。于是积分行被挡住、开关又在打不开的页面里。现在把开关同时放在**模型区**(用户管理模型时视线所在)与**积分行本身**,并默认显示——不再依赖任何一个可能不可达的页面。
25
+
26
+ ### 修复(本版合并的 2.9.1–2.9.4)
27
+
28
+ - **2.9.1**:积分行默认显示,关闭入口移到行内(原为默认关闭 + 开关藏在插件卡片)。
29
+ - **2.9.2**:标签由「通用积分」改为「**Trae通用积分**」,避免与侧边栏里其他插件的数字混淆。
30
+ - **2.9.3**:**补上复选框缺失的 CSS**——移植贡献者实现时只搬了侧边栏积分行的样式,漏掉了卡片里复选框的样式,导致它一直在渲染但**没有布局与配色、等同于不可见**;同时在「模型」区标题旁加了同一个复选框。
31
+ - **2.9.4**:关闭后**彻底不显示**,不再残留「积分」小按钮(回到开关的路已在卡片两处)。
32
+
33
+ ### Tests
34
+
35
+ - 全仓 423 → **435**:开关默认即显示;关闭后容器内**无任何 DOM**(`container.innerHTML === ''`)且轮询停止;行内 `×` 与卡片两处复选框**写同一字段、互相同步**;`preferStrongerRow` 等既有用例不变。
36
+ - **变异验证多次全部被抓**:关闭时残留元素 → 2 例失败;去掉模型区的复选框 → 2 例失败;「缺失即开启」改回「必须显式 true」→ 1 例失败;收起态不退出 → 1 例失败。
37
+
38
+ ### 致谢
39
+
40
+ 报告人 **JiewiW** 独立完成了实现、430 个测试(含假定时器),并在独立 DSH 实例上做了端到端验证——包括「关闭开关后跨越两个应触发周期点无任何请求」这类定时器泄漏检查,还明确声明未改动版本号与无关代码。他当初在说明里提醒的「该 rc.2 宿主未内联挂载插件配置面板」后来被证明是真实的坑,本版据此把开关放到了可达位置。
41
+
42
+ ## 2.9.4 (2026-10-07)
43
+
44
+ ### 变更
45
+
46
+ - **关闭积分显示后,侧边栏彻底不留任何东西**(`×` 或取消勾选都是如此)。之前关闭后会在原位置留一个「积分」小按钮,理由是可以随时找回开关;但 2.9.3 已经把同一个复选框放进卡片**两处**(卡片顶部 + 模型区),回到开关的路本来就在配置它的地方。**用户要的是清干净,残留一个按钮仍然占着他们想腾空的位置。**
47
+ - 随之删除不再使用的样式(`.dsm-trae-sidebar-points-off` / `-show`)与文案键(`sidebar.show` / `sidebar.showHint`)。
48
+
49
+ ### Tests
50
+
51
+ - `sidebar-points` 的用例改为断言**关闭后容器里没有任何 DOM**(`container.innerHTML === ''`),而不是「还剩一个可恢复的入口」;并断言轮询也随之停止。行内 `×` 的用例同样断言完全清空。
52
+ - **变异验证被抓**:关闭时改为残留一个元素 → 2 例失败。
53
+
54
+ ## 2.9.3 (2026-10-07)
55
+
56
+ > **修复:「显示积分」复选框实际上看不见**——它一直在卡片里渲染,但**没有任何 CSS**,所以读起来像是不存在。
57
+
58
+ ### 问题
59
+
60
+ 2.9.0 移植贡献者的实现时,我只搬了侧边栏积分行的样式,**漏掉了卡片里那个复选框的样式**(`.dsm-trae-mainui-switch*`)。复选框本身渲染了,但没有布局、没有间距、没有配色——在卡片里等同于不可见。用户因此反复找不到「在DSH主界面显示积分状态」这个开关。
61
+
62
+ ### 修复
63
+
64
+ - **补上缺失的样式**(`.dsm-trae-mainui-switch` / `-label` / `input`),复选框现在正常显示、可点、有品牌色。
65
+ - **在「模型」区标题旁再加一个同样的复选框**(就在「从 Trae 刷新」「一键测试启用模型」两个按钮的左侧)。理由:管理模型时视线就在那里,而**需要用户去找的设置等于没有设置**——这正是这个开关此前丢失的原因。两处写**同一个字段**,永不冲突;顶部的那个也保留。
66
+
67
+ ### Tests
68
+
69
+ - 全仓 435(用例数不变,但 `card-mainui-switch` 的两条被**加强**):现在断言卡片里**两处都有**复选框、默认都未勾选、且**勾任意一处另一处同步**(共享字段);「配置已开启」的用例也改为断言**两处都是勾选态**。
70
+ - **变异验证被抓**:删掉模型区那一个复选框 → 2 例失败。
71
+
72
+ ## 2.9.2 (2026-10-07)
73
+
74
+ ### 变更
75
+
76
+ - **侧边栏积分行的标签改为「Trae通用积分」**(英文 `Trae credits`)。原先只写「通用积分」,而侧边栏底部可能同时出现其他插件的数字,读者无法确定这个余额属于谁。加上产品名前缀后一眼可辨。
77
+
78
+ ## 2.9.1 (2026-10-07)
79
+
80
+ > **修复:积分行默认不显示,而它的开关在一个部分宿主上根本打不开。**
81
+
82
+ ### 问题
83
+
84
+ 2.9.0 把积分行做成 **opt-in**(`showPointsInMainUi` 默认 `false`),开关只放在插件卡片里。这在部分宿主上等于**功能不存在**:
85
+
86
+ - 用户装了 **dshmarket**,插件页默认停在市场的 tab,而市场 UI **完全不渲染插件自带的配置卡片**(我在它的客户端 bundle 里搜 `plugins.row.config` / `plugins.bundle.config`,**0 处引用**;它只注册自己的卡片,key 是 `dshmarket`)。
87
+ - 于是积分行被 Gate 挡住不渲染,而唯一能打开它的开关在用户到不了的页面里 —— 既看不到功能,也找不到开关。
88
+
89
+ **根因是我把「可关闭」实现成了「默认关闭」**:用户的原话是「**如果要加这个显示**,再加一个开关让不想显示的用户可以关闭」——那本该是**默认显示 + 可关闭**。
90
+
91
+ ### 修复
92
+
93
+ - **`showPointsInMainUi` 默认改为 `true`**(opt-out)。侧边栏底部直接显示通用积分。
94
+ - **关闭开关移到积分行自己身上**:积分行右侧多一个 `×`,点一下即隐藏;隐藏后**同一位置留一个极小的「积分」按钮**,一点即恢复。
95
+ - 两个控件写的是**同一个字段**(`showPointsInMainUi`),因此与插件卡片里的复选框永不冲突。
96
+ - 关键点:**控制入口和功能渲染在同一处**,所以「宿主能画这一行」就等于「用户能控制它」,不再依赖任何设置页。
97
+ - 侧边栏收起(56px 导轨)时仍然整行不渲染:收起态塞不下标签+数值+按钮,会变成「积…」。
98
+
99
+ ### 兼容性说明
100
+
101
+ - 之前**明确关闭过**该开关的用户不受影响(显式 `false` 仍是关闭);「字段缺失」现在按**开启**处理(原先按关闭),所以升级后会直接看到积分行。
102
+ - 插件卡片里的复选框**保留**:在能渲染卡片的宿主上(如内置插件管理器),它依然可用。
103
+
104
+ ### Tests
105
+
106
+ - 全仓 433 → **435**:新增「默认即显示」「关闭后留下可恢复的入口」「行内关闭按钮写入共享字段」;并把原「关闭时不渲染任何 DOM」的用例改写为**「关闭时仍留一个可操作入口」**——那条旧断言正是把这次的问题固化下来的地方。
107
+ - **变异验证 2 次全部被抓**:把「缺失即开启」改回「必须显式 true」→ 1 例失败;关闭时改为不渲染入口 → 1 例失败。
108
+
109
+ ## 2.9.0 (2026-10-06)
110
+
111
+ > **可选:在 DSH 主界面侧边栏常驻一行通用积分**([#26](https://github.com/dingminhua/dsh-connect-trae/issues/26),**由报告人 JiewiW 提交实现**)。
112
+ >
113
+ > 看积分原先必须打开插件设置卡片、展开、找到额度区;现在勾一个开关,侧边栏底部(工作区列表下方、设置上方)常驻一行「通用积分:<数值>」与刷新按钮。
114
+
115
+ ### Features
116
+
117
+ - **侧边栏积分行**(开关 `showPointsInMainUi`,**默认关闭**):在 `sidebar.footer.action` 槽位渲染「通用积分:<数值>」+ 「刷新」按钮;`title` 显示最近刷新时间;抓取失败保留上一次数值并提示;刷新进行中忽略重复点击。
118
+ - 开关关闭时**不渲染任何 DOM**(不是隐藏),因此不产生任何请求;关闭开关或插件卸载即清除定时器并中止在途请求。
119
+ - 开启后**先立即抓取一次,完成后再启动 5 分钟周期**的定时器,不叠加。
120
+
121
+ ### 合入时相对贡献者实现的三处调整
122
+
123
+ 1. **数字口径:`credits.generalAvailable`(通用积分),不是 `credits.available`。** `available` 是上游汇总(`summary.totalAmount - consumedAmount`),而卡片展示的是按额度包分出的 Work / 通用两桶;汇总**不保证**等于任一桶。一个用户在侧边栏找不到对应数字的余额,比没有数字更糟。通用桶也正是 SOLO 聊天路径消耗的那个。
124
+ 2. **轮询 60s → 5 分钟。** 卡片的轮询本身就是 60s,而每次 usage 读取会打**两个**上游请求;积分以「模型调用」而非秒为单位变化,60s 的边际价值很低。
125
+ 3. **侧边栏收起时整行不渲染**(`wide === false` → `null`)。宿主 CSS 并**不会**在收起时隐藏底部区域,而是把它压成 56px 居中窄条(`SidebarRoot.module.css` 的 `.collapsed .footerActions`);标签 + 余额 + 按钮塞进 56px 会被裁成「积…」。而**收起正是长时间工作的常态**——这个功能最需要成立的时刻不能是它最坏看的时刻。提前退出也意味着收起期间不启动抓取循环。
126
+
127
+ ### Tests
128
+
129
+ - 全仓 423 → **433**:移植贡献者的 7 例(开关默认关闭/持久化/读回/回显;挂载即抓取、首次抓取后启定时器、卸载即清除;失败保留上次数值;进行中点击被忽略;Gate 关闭时零 DOM),并新增 3 例锁定上述调整。
130
+ - **测试加固**:贡献者的 fixture 让 `available` 与 `generalAvailable` **恰好相等**,所以换成哪个字段测试都过——这正是能悄悄退回汇总口径的盲区。现已让两者**不相等**,字段选错会立刻失败。
131
+ - 同时把 `card-region-switch` 的取元素方式从「按下标取前两个 checkbox」改为**按 aria-label 精确匹配区域开关**:新开关让卡片多了第三个 checkbox,按下标会开始断言到错误的控件。
132
+ - **变异验证 3 次全部被抓**:收起态不再退出 → 1 例失败;取字段改回 `available` → 3 例失败;轮询改回 60s → 1 例失败。
133
+
134
+ ### 致谢
135
+
136
+ 报告人 **JiewiW** 独立完成了实现、430 个测试(含假定时器)、并在独立 DSH 实例上做了端到端验证(含「关闭开关后跨越两个应触发周期点无任何请求」这类定时器泄漏检查),还明确声明未改动版本号与无关代码。
137
+
3
138
  ## 2.8.0 (2026-10-06)
4
139
 
5
140
  > **绑定账号额度耗尽时不再是一块没有解释的空面板**([#25](https://github.com/dingminhua/dsh-connect-trae/issues/25),报告人 JiewiW)。
package/README.en.md CHANGED
@@ -44,6 +44,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
44
44
  - **Read-only usage and model management** — Work/general credits on CN accounts, subscription/trial status on international ones; enable Trae models freely. Read-only queries consume nothing.
45
45
  - **One-click test of the enabled models** — sends each **enabled** model one minimal message (a single `ping`, no tools) and reports whether it actually answers: **works** / **needs a higher plan** (Trae `1005`; your plan does not cover it) / **not served** (`4001`/`4011`; no SOLO function serves it) / **throttled** / **sign-in rejected** / **not determined**. The last one is a first-class outcome: a timeout, a dropped connection, a proxy or a 5xx is **never** reported as unusable — only an explicit upstream refusal is a negative verdict. This is the plugin's **only** operation that spends quota (check-in changes account state but spends nothing), it runs only when you click, and its results live in the current session rather than in your settings.
46
46
  - **Daily check-in claiming** — a one-click claim on the CN card (the button shows the daily reward, and reads "Claimed today" and disables itself once that account has been paid). This is the plugin's **only** operation that changes account state, and it runs only when you click. Trae allows **one check-in per device per day**: after switching accounts, if this machine's slot is already spent (by any account), the selected account cannot claim here that day — the card explains that rather than pretending the account was paid. The international side has no check-in activity, so no button is shown there.
47
+ - **Sidebar credits** — a permanent "Trae credits: <n>" line with a refresh button sits at the bottom of the left sidebar (below the workspace list, above Settings), refreshing every 5 minutes and on demand. **Shown by default**; the `×` on its right hides it completely (to bring it back, tick the switch in the plugin card — the same switch appears at the top of the card and beside the model actions). It shows the **general** credit pool — the one SOLO chat actually spends — so the number matches the card, and the line disappears when the sidebar is collapsed. The international region is subscription-based and has no comparable number, so nothing is shown there.
47
48
  - **Secure loopback shim** — one random port + in-process random secret per region; the real Trae token is never handed to pi-ai.
48
49
 
49
50
  ## How it works
package/README.md CHANGED
@@ -44,6 +44,7 @@
44
44
  - **只读用量与模型管理** —— 国内版查看 Work 积分与通用积分,国际版查看订阅/试用状态;刷新/启用 Trae 模型;只读查询不消耗额度。
45
45
  - **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`/`4011`,SOLO 通道没有这个模型)/ **被限流** / **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
46
46
  - **每日签到领取** —— 国内版卡片上一键领取当日签到积分(按钮显示每日奖励,该账号已领取则显示「今日已领取」并禁用);这是插件里**唯一会改变账号状态**的操作,由你主动点击触发。Trae 的签到按「**每台设备每天一次**」限制:切换账号后如果本机今天的名额已经用掉(无论是哪个账号用的),当前账号在本机当天无法再签,卡片会说明原因而不是谎报「已领取」。国际版没有签到活动,因此不显示该按钮。
47
+ - **侧边栏积分** —— DSH 左侧边栏底部(工作区列表下方、设置上方)常驻一行「Trae通用积分:<数值>」与刷新按钮,每 5 分钟自动更新,也可手动刷新。**默认显示**;右侧的 `×` 可隐藏,关闭后**侧边栏不再留任何东西**(要重新显示就回到插件卡片里勾选,卡片顶部与模型区各有一个同一开关)。显示的是**通用积分**(SOLO 聊天实际消耗的那个额度池),与卡片上的数字一致;侧边栏收起时该行自动消失。国际版为订阅制、没有可比数字,因此不显示。
47
48
  - **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 Trae token 不交给 pi-ai。
48
49
 
49
50
  ## 工作原理
package/lib/client.js CHANGED
@@ -263,7 +263,16 @@ window.__ModuleLoader__.load({
263
263
  .dsm-trae-model-name-rate{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:400;line-height:16px;flex:none}
264
264
  /* The two head actions sit together; the probe one SPENDS quota, so it carries
265
265
  the hint tooltip rather than looking like its read-only sibling. */
266
+ /* The sidebar-credit switch. Two instances share this style: the card's own
267
+ row at the top and the copy beside the model actions (2.9.3). Without it the
268
+ checkbox rendered unstyled and read as absent — the whole reason the switch
269
+ looked missing. */
270
+ .dsm-trae-mainui-switch{padding:2px 0 0}
271
+ .dsm-trae-mainui-switch-label{display:flex;align-items:center;gap:9px;font-size:13px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0);cursor:pointer;white-space:nowrap}
272
+ .dsm-trae-mainui-switch-label input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
273
+ .dsm-trae-mainui-switch-label:has(input:disabled){opacity:.6;cursor:default}
266
274
  .dsm-trae-models-actions{display:flex;align-items:center;gap:8px;flex:none}
275
+ }
267
276
  .dsm-trae-usage-alternative{display:flex;align-items:flex-start;gap:10px;margin-top:10px;padding:10px 12px;border:1px solid var(--dsw-alias-state-warning-border,#7a5b12);border-radius:10px;background:var(--dsw-alias-state-warning-bg,#2a2313)}
268
277
  .dsm-trae-usage-alternative-text{margin:0;flex:1 1 auto;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e6e6e6)}
269
278
  .dsm-trae-usage-switch-button{flex:0 0 auto;padding:5px 10px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2c2f36);color:var(--dsw-alias-label-primary,#e6e6e6);font-size:12px;line-height:16px;cursor:pointer}
@@ -309,6 +318,24 @@ window.__ModuleLoader__.load({
309
318
  an error: it is Trae's one-per-device-per-day rule, and the day is simply
310
319
  spent. Rendering it in the error colour would read as a plugin fault. */
311
320
  .dsm-trae-checkin-note{margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
321
+ `;
322
+ /**
323
+ * Sidebar credit line styles, injected by the sidebar component itself.
324
+ *
325
+ * Separate from the card bundle because the line lives in the shell's sidebar,
326
+ * not inside the plugin card; a card that is closed still shows this.
327
+ */
328
+ const TRAE_SIDEBAR_CSS = `
329
+ .dsm-trae-sidebar-points{display:flex;align-items:center;gap:8px;padding:4px 2px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
330
+ .dsm-trae-sidebar-points-line{display:flex;align-items:center;gap:6px;min-width:0;flex:1 1 auto}
331
+ .dsm-trae-sidebar-points-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-variant-numeric:tabular-nums}
332
+ .dsm-trae-sidebar-points-failed{color:var(--dsw-alias-state-error-primary,#ef4444);font-size:11px;line-height:16px;white-space:nowrap}
333
+ .dsm-trae-sidebar-points-hide{appearance:none;flex:none;font:inherit;font-size:14px;line-height:1;padding:2px 6px;border:0;border-radius:6px;color:var(--dsw-alias-label-tertiary,#999);background:transparent;cursor:pointer}
334
+ .dsm-trae-sidebar-points-hide:hover{color:var(--dsw-alias-label-primary,#e6e6e6);background:var(--dsw-alias-bg-layer-3,#2c2f36)}
335
+ .dsm-trae-sidebar-points-refresh{appearance:none;flex:none;font:inherit;font-size:11px;line-height:16px;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:7px;color:var(--dsw-alias-label-secondary,#c6c9d0);background:transparent;cursor:pointer}
336
+ .dsm-trae-sidebar-points-refresh:hover:not(:disabled){color:var(--dsw-alias-label-primary,#e6e6e6);border-color:var(--dsw-alias-label-dimmed,#777)}
337
+ .dsm-trae-sidebar-points-refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
338
+ .dsm-trae-sidebar-points-refresh:disabled{opacity:.5;cursor:default}
312
339
  `;
313
340
  //#endregion
314
341
  //#region src/client/TraeUsageCard.tsx
@@ -374,7 +401,7 @@ window.__ModuleLoader__.load({
374
401
  document.head.appendChild(styleTag);
375
402
  }
376
403
  }
377
- function formatNumber(value) {
404
+ function formatNumber$1(value) {
378
405
  return new Intl.NumberFormat(void 0, {
379
406
  minimumFractionDigits: 0,
380
407
  maximumFractionDigits: 0
@@ -393,7 +420,7 @@ window.__ModuleLoader__.load({
393
420
  if (value === void 0) return unknown;
394
421
  if (value >= 1e6 && value % 1e6 === 0) return `${value / 1e6}M`;
395
422
  if (value >= 1e3 && value % 1e3 === 0) return `${value / 1e3}K`;
396
- return formatNumber(value);
423
+ return formatNumber$1(value);
397
424
  }
398
425
  /**
399
426
  * The subscription row's verdict.
@@ -485,6 +512,24 @@ window.__ModuleLoader__.load({
485
512
  */
486
513
  const [claiming, setClaiming] = (0, react.useState)(false);
487
514
  /** Last claim refusal, shown until the next successful refresh. */
515
+ /**
516
+ * Toggle the global "show credits at the DSH sidebar foot" switch. Unlike the
517
+ * per-region provider switch this is one top-level boolean shared by both
518
+ * regions; only that field is written, and the read-back must show the new
519
+ * value before success is reported.
520
+ */
521
+ const toggleShowPointsInMainUi = async (enabled) => {
522
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
523
+ setTogglingMainUi(true);
524
+ setWriteError(void 0);
525
+ try {
526
+ await writeSettingsField(settingsScope, "showPointsInMainUi", enabled, (readBack) => unwrapVolatileDeep(readBack)?.showPointsInMainUi === enabled);
527
+ } catch (error) {
528
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
529
+ } finally {
530
+ if (mounted.current) setTogglingMainUi(false);
531
+ }
532
+ };
488
533
  const [claimError, setClaimError] = (0, react.useState)(void 0);
489
534
  /**
490
535
  * Outcome of the last "refresh accounts" press (issue #25).
@@ -495,6 +540,7 @@ window.__ModuleLoader__.load({
495
540
  * failure. Reporting the outcome is the difference between "it did nothing"
496
541
  * and "it found the same 2 accounts, still on X".
497
542
  */
543
+ const [togglingMainUi, setTogglingMainUi] = (0, react.useState)(false);
498
544
  const [rescanNote, setRescanNote] = (0, react.useState)(void 0);
499
545
  /**
500
546
  * Whether this MACHINE has spent today's check-in while the selected account
@@ -993,6 +1039,21 @@ window.__ModuleLoader__.load({
993
1039
  children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
994
1040
  className: "dsm-trae-usage",
995
1041
  children: [
1042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1043
+ className: "dsm-trae-mainui-switch",
1044
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1045
+ className: "dsm-trae-mainui-switch-label",
1046
+ title: t("row.showPointsInMainUiHint"),
1047
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1048
+ type: "checkbox",
1049
+ checked: settingsValue?.showPointsInMainUi === true,
1050
+ disabled: settingsScope?.getSnapshot().writable !== true || togglingMainUi,
1051
+ onChange: (event) => {
1052
+ toggleShowPointsInMainUi(event.currentTarget.checked);
1053
+ }
1054
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.showPointsInMainUi") })]
1055
+ })
1056
+ }),
996
1057
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
997
1058
  className: "dsm-trae-tabs",
998
1059
  role: "tablist",
@@ -1105,7 +1166,7 @@ window.__ModuleLoader__.load({
1105
1166
  children: [
1106
1167
  t("row.creditsExhausted", { account: status.accountName }),
1107
1168
  " ",
1108
- t("row.creditsAlternative", { accounts: status.alternatives.map((alt) => `${alt.accountName}(${t(`row.edition${alt.edition === "cn" ? "Cn" : alt.edition === "sg" ? "Sg" : alt.edition === "solo" ? "Solo" : "SoloSg"}`)}:${formatNumber(alt.workAvailable + alt.generalAvailable)})`).join("、") })
1169
+ t("row.creditsAlternative", { accounts: status.alternatives.map((alt) => `${alt.accountName}(${t(`row.edition${alt.edition === "cn" ? "Cn" : alt.edition === "sg" ? "Sg" : alt.edition === "solo" ? "Solo" : "SoloSg"}`)}:${formatNumber$1(alt.workAvailable + alt.generalAvailable)})`).join("、") })
1109
1170
  ]
1110
1171
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1111
1172
  type: "button",
@@ -1183,7 +1244,7 @@ window.__ModuleLoader__.load({
1183
1244
  }),
1184
1245
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1185
1246
  className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-work",
1186
- children: formatNumber(status.credits.workAvailable)
1247
+ children: formatNumber$1(status.credits.workAvailable)
1187
1248
  }),
1188
1249
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1189
1250
  className: "dsm-trae-usage-stat-hint",
@@ -1205,7 +1266,7 @@ window.__ModuleLoader__.load({
1205
1266
  }),
1206
1267
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1207
1268
  className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-general",
1208
- children: formatNumber(status.credits.generalAvailable)
1269
+ children: formatNumber$1(status.credits.generalAvailable)
1209
1270
  }),
1210
1271
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1211
1272
  className: "dsm-trae-usage-stat-hint",
@@ -1222,8 +1283,8 @@ window.__ModuleLoader__.load({
1222
1283
  status.checkin === void 0 ? null : (() => {
1223
1284
  const accountClaimed = status.checkin.checkedIn;
1224
1285
  const deviceSpent = !accountClaimed && (status.checkin.didCheckedIn || deviceSpentNotice);
1225
- const reward = formatNumber(status.checkin.credits);
1226
- const bonus = status.checkin.extraCredits === void 0 ? "" : ` + ${formatNumber(status.checkin.extraCredits)}`;
1286
+ const reward = formatNumber$1(status.checkin.credits);
1287
+ const bonus = status.checkin.extraCredits === void 0 ? "" : ` + ${formatNumber$1(status.checkin.extraCredits)}`;
1227
1288
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1228
1289
  className: "dsm-trae-checkin",
1229
1290
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -1277,27 +1338,42 @@ window.__ModuleLoader__.load({
1277
1338
  children: t("row.modelsSummary", { count: activeEnabledIds.size })
1278
1339
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1279
1340
  className: "dsm-trae-models-actions",
1280
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1281
- type: "button",
1282
- className: "dsm-btn dsm-btn-outline",
1283
- disabled: busy,
1284
- onClick: () => {
1285
- refreshModels();
1286
- },
1287
- children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
1288
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1289
- type: "button",
1290
- className: "dsm-btn dsm-btn-outline",
1291
- disabled: busy || testing !== void 0 || activeEnabledIds.size === 0,
1292
- title: t("row.modelsTestHint"),
1293
- onClick: () => {
1294
- testEnabledModels();
1295
- },
1296
- children: testing === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
1297
- done: testing.done,
1298
- total: testing.total
1341
+ children: [
1342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1343
+ className: "dsm-trae-mainui-switch-label",
1344
+ title: t("row.showPointsInMainUiHint"),
1345
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1346
+ type: "checkbox",
1347
+ checked: settingsValue?.showPointsInMainUi === true,
1348
+ disabled: settingsScope?.getSnapshot().writable !== true || togglingMainUi,
1349
+ onChange: (event) => {
1350
+ toggleShowPointsInMainUi(event.currentTarget.checked);
1351
+ }
1352
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.showPointsInMainUi") })]
1353
+ }),
1354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1355
+ type: "button",
1356
+ className: "dsm-btn dsm-btn-outline",
1357
+ disabled: busy,
1358
+ onClick: () => {
1359
+ refreshModels();
1360
+ },
1361
+ children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
1362
+ }),
1363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1364
+ type: "button",
1365
+ className: "dsm-btn dsm-btn-outline",
1366
+ disabled: busy || testing !== void 0 || activeEnabledIds.size === 0,
1367
+ title: t("row.modelsTestHint"),
1368
+ onClick: () => {
1369
+ testEnabledModels();
1370
+ },
1371
+ children: testing === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
1372
+ done: testing.done,
1373
+ total: testing.total
1374
+ })
1299
1375
  })
1300
- })]
1376
+ ]
1301
1377
  })]
1302
1378
  }),
1303
1379
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1464,6 +1540,236 @@ window.__ModuleLoader__.load({
1464
1540
  });
1465
1541
  }
1466
1542
  //#endregion
1543
+ //#region src/client/SidebarPoints.tsx
1544
+ /**
1545
+ * Compact credit line contributed to the DSH main sidebar foot
1546
+ * (`sidebar.footer.action`).
1547
+ *
1548
+ * Mounted only while the plugin-card switch `showPointsInMainUi` is on, so the
1549
+ * slot renders nothing when the feature is off. On mount it fetches the bound
1550
+ * CN account's remaining credits ONCE and only then starts the periodic
1551
+ * refresh timer; the timer is cleared on unmount (switch off / plugin unload),
1552
+ * so timers never stack.
1553
+ *
1554
+ * A failed refresh keeps the last known value and surfaces a short failure
1555
+ * note; an in-flight refresh ignores further clicks (debounce).
1556
+ */
1557
+ /**
1558
+ * Automatic refresh period. The first fetch happens immediately on enable;
1559
+ * this is the gap between subsequent automatic fetches.
1560
+ *
1561
+ * Five minutes, not the card's 60s: a credit balance moves on the scale of
1562
+ * model calls, not seconds, and this line is a glanceable reminder rather than
1563
+ * a live meter. Each poll is two read-only upstream calls, so a tighter period
1564
+ * would buy nothing a user can act on.
1565
+ */
1566
+ const SIDEBAR_POINTS_REFRESH_INTERVAL_MS = 3e5;
1567
+ /** Inject the sidebar-line CSS once. */
1568
+ if (typeof document !== "undefined") {
1569
+ const cssId = "dsh-connect-trae/sidebar.css";
1570
+ if (!document.querySelector(`style[data-plugin-css="${cssId}"]`)) {
1571
+ const styleTag = document.createElement("style");
1572
+ styleTag.dataset.plugin = "dsh-connect-trae";
1573
+ styleTag.dataset.pluginCss = cssId;
1574
+ styleTag.textContent = TRAE_SIDEBAR_CSS;
1575
+ document.head.appendChild(styleTag);
1576
+ }
1577
+ }
1578
+ function formatNumber(value) {
1579
+ return new Intl.NumberFormat(void 0, { maximumFractionDigits: 0 }).format(value);
1580
+ }
1581
+ function formatClock(value) {
1582
+ return new Intl.DateTimeFormat(void 0, {
1583
+ hour: "2-digit",
1584
+ minute: "2-digit",
1585
+ second: "2-digit"
1586
+ }).format(new Date(value));
1587
+ }
1588
+ /**
1589
+ * The GENERAL credit balance out of one usage-route document.
1590
+ *
1591
+ * Deliberately `generalAvailable`, NOT `credits.available`. The latter is the
1592
+ * upstream aggregate (`summary.totalAmount - consumedAmount`), while the card
1593
+ * shows two buckets derived from the entitlement packs; the aggregate is not
1594
+ * guaranteed to equal the buckets, and a sidebar number the user cannot match
1595
+ * against the card is worse than no number at all. The general bucket is the
1596
+ * one the SOLO chat path spends, so it is the one worth watching.
1597
+ */
1598
+ function generalCreditsOf(usage) {
1599
+ if (usage.status !== "signed-in") return void 0;
1600
+ return usage.credits?.generalAvailable;
1601
+ }
1602
+ /** One fetch+render cycle. Returns false when the request failed. */
1603
+ function SidebarPoints({ t, onHide }) {
1604
+ if (t === void 0) throw new Error("Sidebar points line requires its translation function");
1605
+ const [points, setPoints] = (0, react.useState)(void 0);
1606
+ const [signedIn, setSignedIn] = (0, react.useState)(true);
1607
+ const [busy, setBusy] = (0, react.useState)(false);
1608
+ const [failed, setFailed] = (0, react.useState)(false);
1609
+ const [lastRefresh, setLastRefresh] = (0, react.useState)(void 0);
1610
+ /** True while a request is in flight; debounces repeated refresh clicks. */
1611
+ const inFlight = (0, react.useRef)(false);
1612
+ const mounted = (0, react.useRef)(true);
1613
+ (0, react.useEffect)(() => {
1614
+ mounted.current = true;
1615
+ return () => {
1616
+ mounted.current = false;
1617
+ };
1618
+ }, []);
1619
+ const fetchPoints = (0, react.useCallback)(async (signal) => {
1620
+ if (inFlight.current) return false;
1621
+ inFlight.current = true;
1622
+ if (mounted.current) setBusy(true);
1623
+ try {
1624
+ const response = await fetch(withTraeRegion(TRAE_USAGE_PATH, "cn"), {
1625
+ headers: { accept: "application/json" },
1626
+ credentials: "same-origin",
1627
+ ...signal === void 0 ? {} : { signal }
1628
+ });
1629
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1630
+ const value = await response.json();
1631
+ if (!mounted.current || signal?.aborted === true) return true;
1632
+ const general = generalCreditsOf(value);
1633
+ if (general !== void 0) {
1634
+ setPoints(general);
1635
+ setSignedIn(true);
1636
+ } else setSignedIn(value.status === "signed-in");
1637
+ setFailed(false);
1638
+ setLastRefresh(Date.now());
1639
+ return true;
1640
+ } catch {
1641
+ if (mounted.current && signal?.aborted !== true) setFailed(true);
1642
+ return false;
1643
+ } finally {
1644
+ inFlight.current = false;
1645
+ if (mounted.current) setBusy(false);
1646
+ }
1647
+ }, []);
1648
+ (0, react.useEffect)(() => {
1649
+ let cancelled = false;
1650
+ let timer;
1651
+ const controller = new AbortController();
1652
+ fetchPoints(controller.signal).finally(() => {
1653
+ if (cancelled) return;
1654
+ timer = window.setInterval(() => {
1655
+ fetchPoints(controller.signal);
1656
+ }, SIDEBAR_POINTS_REFRESH_INTERVAL_MS);
1657
+ });
1658
+ return () => {
1659
+ cancelled = true;
1660
+ if (timer !== void 0) window.clearInterval(timer);
1661
+ controller.abort();
1662
+ };
1663
+ }, [fetchPoints]);
1664
+ const titleText = lastRefresh === void 0 ? void 0 : t("sidebar.lastRefresh", { time: formatClock(lastRefresh) });
1665
+ const valueText = !signedIn ? t("sidebar.signedOut") : points === void 0 ? "—" : formatNumber(points);
1666
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1667
+ className: "dsm-trae-sidebar-points",
1668
+ title: titleText,
1669
+ children: [
1670
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1671
+ className: "dsm-trae-sidebar-points-line",
1672
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1673
+ className: "dsm-trae-sidebar-points-text",
1674
+ "aria-live": "polite",
1675
+ children: [
1676
+ t("sidebar.points"),
1677
+ ":",
1678
+ valueText
1679
+ ]
1680
+ }), failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1681
+ className: "dsm-trae-sidebar-points-failed",
1682
+ role: "status",
1683
+ children: t("sidebar.refreshFailed")
1684
+ }) : null]
1685
+ }),
1686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1687
+ type: "button",
1688
+ className: "dsm-trae-sidebar-points-refresh",
1689
+ disabled: busy,
1690
+ "aria-label": busy ? t("sidebar.refreshing") : t("sidebar.refresh"),
1691
+ title: busy ? t("sidebar.refreshing") : t("sidebar.refresh"),
1692
+ onClick: () => {
1693
+ fetchPoints();
1694
+ },
1695
+ children: busy ? t("sidebar.refreshing") : t("sidebar.refresh")
1696
+ }),
1697
+ onHide === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1698
+ type: "button",
1699
+ className: "dsm-trae-sidebar-points-hide",
1700
+ "aria-label": t("sidebar.hide"),
1701
+ title: t("sidebar.hide"),
1702
+ onClick: onHide,
1703
+ children: "×"
1704
+ })
1705
+ ]
1706
+ });
1707
+ }
1708
+ //#endregion
1709
+ //#region src/client/SidebarPointsGate.tsx
1710
+ /**
1711
+ * Gate between the `sidebar.footer.action` slot and the compact credit line.
1712
+ *
1713
+ * Kept in its own module (rather than inside the browser-plugin entry) so it
1714
+ * can be imported from the Node/jsdom test suite: the entry imports
1715
+ * browser-only DSH packages the test environment cannot load, but this gate
1716
+ * depends only on React, the card's settings-scope shape and the line
1717
+ * component.
1718
+ *
1719
+ * TWO states:
1720
+ *
1721
+ * - on → {@link SidebarPoints}, which owns the fetch loop and its timer;
1722
+ * - off → NOTHING. The row is absent from the DOM, not hidden.
1723
+ *
1724
+ * Off used to leave a small "积分" chip behind, on the theory that a switch you
1725
+ * cannot find is no switch at all (2.9.1). That safety net is no longer needed:
1726
+ * the card carries the same checkbox twice — at the top of the card and beside
1727
+ * the model actions — so the way back is always where the feature is
1728
+ * configured. What the user asked for is for the row to be gone, and a leftover
1729
+ * chip still occupies the footer they wanted cleared.
1730
+ *
1731
+ * The collapsed rail also renders nothing: the shell squeezes the footer to a
1732
+ * 56px strip rather than hiding it, and the line does not read well there — see
1733
+ * the bail-out below.
1734
+ */
1735
+ /**
1736
+ * Read the sidebar-credit switch out of a committed settings snapshot.
1737
+ *
1738
+ * Absent means ON: the field defaults to true on the host, and a settings
1739
+ * scope that has not populated yet must not blink the line away. Only an
1740
+ * explicit `false` (the user turned it off, from the line or the card) hides
1741
+ * the balance.
1742
+ */
1743
+ function sidebarPointsEnabledOf(scope) {
1744
+ if (scope === void 0) return true;
1745
+ return unwrapVolatileDeep(scope.getSnapshot().value)?.showPointsInMainUi !== false;
1746
+ }
1747
+ function SidebarPointsGate(props) {
1748
+ const { t, settingsScope, wide } = props;
1749
+ const [enabled, setEnabled] = (0, react.useState)(() => sidebarPointsEnabledOf(settingsScope));
1750
+ (0, react.useEffect)(() => settingsScope?.subscribe(() => {
1751
+ setEnabled(sidebarPointsEnabledOf(settingsScope));
1752
+ }), [settingsScope]);
1753
+ /**
1754
+ * Write the switch back through the same field the card writes, so the two
1755
+ * controls can never disagree. The read-back is not awaited for its value:
1756
+ * the subscription above re-renders when the committed snapshot changes, and
1757
+ * an unwritable scope simply leaves the current state alone.
1758
+ */
1759
+ const setShown = (0, react.useCallback)((next) => {
1760
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
1761
+ settingsScope.set("showPointsInMainUi", next).catch(() => {});
1762
+ }, [settingsScope]);
1763
+ if (wide === false) return null;
1764
+ if (!enabled) return null;
1765
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SidebarPoints, {
1766
+ t,
1767
+ onHide: () => {
1768
+ setShown(false);
1769
+ }
1770
+ });
1771
+ }
1772
+ //#endregion
1467
1773
  //#region src/client/locales.ts
1468
1774
  /**
1469
1775
  * Plugin-card copy registered under the settings.trae locale namespace.
@@ -1537,6 +1843,15 @@ window.__ModuleLoader__.load({
1537
1843
  "row.modelsTestEnabled": "Test enabled models",
1538
1844
  "row.modelsTesting": "Testing {done}/{total}…",
1539
1845
  "row.modelsTestHint": "Sends one minimal message per enabled model to see which ones actually answer. Costs a small amount of quota.",
1846
+ "row.showPointsInMainUi": "Show credits in the DSH main UI",
1847
+ "row.showPointsInMainUiHint": "Keep a general-credit line with a refresh button at the bottom of the DSH sidebar (hidden while the sidebar is collapsed; refreshes every 5 minutes).",
1848
+ "sidebar.points": "Trae credits",
1849
+ "sidebar.refresh": "Refresh",
1850
+ "sidebar.refreshing": "Refreshing…",
1851
+ "sidebar.lastRefresh": "Last refreshed {time}",
1852
+ "sidebar.refreshFailed": "Refresh failed",
1853
+ "sidebar.signedOut": "Not signed in",
1854
+ "sidebar.hide": "Hide credits",
1540
1855
  "row.accountsRefreshed": "Accounts refreshed: {count} found{selected}",
1541
1856
  "row.accountsKept": "still bound to",
1542
1857
  "row.accountsRefreshedNone": "Accounts refreshed: no usable Trae account found",
@@ -1640,6 +1955,15 @@ window.__ModuleLoader__.load({
1640
1955
  "row.modelsTestEnabled": "一键测试启用模型",
1641
1956
  "row.modelsTesting": "正在测试 {done}/{total}…",
1642
1957
  "row.modelsTestHint": "给每个已启用模型发一条最小消息,看哪些真的能应答。会消耗少量额度。",
1958
+ "row.showPointsInMainUi": "在DSH主界面显示积分状态",
1959
+ "row.showPointsInMainUiHint": "在 DSH 侧边栏底部常驻一行通用积分与刷新按钮(收起侧边栏时不显示,每 5 分钟自动刷新)。",
1960
+ "sidebar.points": "Trae通用积分",
1961
+ "sidebar.refresh": "刷新",
1962
+ "sidebar.refreshing": "刷新中…",
1963
+ "sidebar.lastRefresh": "最近刷新 {time}",
1964
+ "sidebar.refreshFailed": "刷新失败",
1965
+ "sidebar.signedOut": "未登录",
1966
+ "sidebar.hide": "隐藏积分",
1643
1967
  "row.accountsRefreshed": "已刷新账号:找到 {count} 个账号{selected}",
1644
1968
  "row.accountsKept": "仍绑定",
1645
1969
  "row.accountsRefreshedNone": "已刷新账号:未找到可用的 Trae 账号",
@@ -1800,6 +2124,32 @@ window.__ModuleLoader__.load({
1800
2124
  };
1801
2125
  registerCard("plugins.bundle.config", "dsh-connect-trae");
1802
2126
  registerCard("plugins.row.config", "dsh-connect-trae#dsh-connect-trae");
2127
+ /**
2128
+ * General-credit line at the sidebar foot (`sidebar.footer.action`, a
2129
+ * root-scope list slot), contributed by JiewiW (issue #26).
2130
+ *
2131
+ * Registration is unconditional for the slot's lifetime; the component
2132
+ * renders nothing until the card switch is on, and nothing again while the
2133
+ * sidebar is collapsed — so an off switch (or a collapsed rail) leaves zero
2134
+ * DOM and no fetch loop.
2135
+ */
2136
+ const registerSidebarPoints = () => {
2137
+ try {
2138
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
2139
+ name: "sidebar.footer.action",
2140
+ id: "dsh-connect-trae-points",
2141
+ order: 4,
2142
+ locale: namespace,
2143
+ inject: () => ({
2144
+ t,
2145
+ settingsScope
2146
+ })
2147
+ }, SidebarPointsGate));
2148
+ } catch (error) {
2149
+ console.error("[dsh-connect-trae] sidebar points slot failed to register (host provider unaffected):", error);
2150
+ }
2151
+ };
2152
+ registerSidebarPoints();
1803
2153
  } catch (error) {
1804
2154
  console.error("[dsh-connect-trae] client card failed to load (host provider unaffected):", error);
1805
2155
  }
package/lib/index.d.ts CHANGED
@@ -2022,6 +2022,22 @@ interface Config {
2022
2022
  * its own slot; tokens remain outside settings.
2023
2023
  */
2024
2024
  accounts?: Partial<Record<TraeRegion, string>>;
2025
+ /**
2026
+ * Show the CN account's general credit balance as a permanent line at the
2027
+ * bottom of the DSH sidebar (issue #26, contributed by JiewiW).
2028
+ *
2029
+ * ON by default and opt-out since 2.9.1. It was opt-in first, on the theory
2030
+ * that a user who never asked for a periodic usage read should not get one —
2031
+ * but the switch lived only in the plugin card, and the card is not reachable
2032
+ * in every host: a market plugin can own the Plugins tab and never dispatch
2033
+ * `plugins.bundle.config`, so the line was permanently off with no discoverable
2034
+ * way to turn it on. The line now carries its own on/off control in the
2035
+ * sidebar, which is reachable exactly where the feature renders.
2036
+ *
2037
+ * Declared volatile alongside `accounts`/`regions` so the settings form can
2038
+ * both read and write it.
2039
+ */
2040
+ showPointsInMainUi?: boolean;
2025
2041
  /**
2026
2042
  * Per-region model state, keyed `cn` | `ai`. The CN and international apps
2027
2043
  * expose different rosters, so each keeps its own directory and selection
package/lib/index.js CHANGED
@@ -4769,6 +4769,7 @@ const Config = z.object({
4769
4769
  "solo-sg"
4770
4770
  ]).default("auto").description("Trae edition hint")),
4771
4771
  accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
4772
+ showPointsInMainUi: asVolatile(z.boolean().default(true).description("在DSH主界面显示积分状态(可在侧边栏那一行上随时关闭)")),
4772
4773
  accounts: asVolatile(accountSelectionConfig.description("Per-region account selections, keyed cn | ai")),
4773
4774
  regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai")),
4774
4775
  lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
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": "2.8.0",
5
+ "version": "2.10.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",