dsh-connect-trae 4.8.0 → 4.9.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,100 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.9.0 (2026-11-06)
4
+
5
+ ### 变更
6
+
7
+ - **配置入口从「插件详情页的卡片」搬到「设置」侧边栏的独立页面**。原来这张卡片注册在
8
+ 插件市场的 `plugins.bundle.config` / `plugins.row.config` 槽位,也就是插件详情页里、
9
+ README 下方——那一页的职责是介绍和安装一个包,把配置也塞进去,用户得先划过一整篇说明
10
+ 才能找到一个勾选框。现在改注册 `settings.section`(DSH 0.2.1-rc.5 起的槽位),入口在
11
+ 设置面板左侧导航里,与宿主自己的「账号 / 通用 / 模型 / 插件 / 智能体预设」并列。
12
+ - 页面 id 用包名 `dsh-connect-trae`,`order: 30`(排在宿主的 `account -10`、`general 0`、
13
+ `models 10`、`plugins 15`、`agent-presets 20` 之后,不与任何内置项冲突)。
14
+ - 同时注册 `settings.section.icon`,**id 与页面完全相同**:宿主按 `{ only: row.id }` 过滤
15
+ 图标,id 不匹配时图标不显示、默认齿轮静默留下——这类失配在界面上没有任何报错。
16
+ - 页面与图标在**同一个 `inject` 回调**里注册、disposer 成对返回:图标只在页面的 id 存在时
17
+ 才有意义,把配对做成结构而不是约定。图标自带独立 `inject` 与 catch——宿主没声明该槽位时
18
+ 丢的是图标,不是配置页。
19
+ - **不做能力探测**。`slots.inject()` 本身就是探测:槽位已声明就立即执行回调,未声明就等待,
20
+ 宿主永远不声明则什么都不做。外面再套一层 `typeof` 判断只会新增一条「注册永远不发生且无声」
21
+ 的失败路径(同族 `dsh-subagent-default-model` 的 2.1.16/2.1.17 就是这么丢过一整页)。
22
+ - **输入框积分浮层恢复「设置」按钮,指路改到设置面板的分栏定位**。上一轮曾把按钮删掉,
23
+ 理由写成「宿主没有任何公开的客户端服务能打开设置面板并定位到某个 section」——**这个理由是
24
+ 错的**,本轮已撤销。宿主确实没有这样的*服务*,但设置面板把自己的 store 座位钉死在注册声明上
25
+ (`shellStore.create: () => shellInstance`),所以
26
+ `slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` 是一条真实可用的
27
+ 路径,宿主自己的测试就是这么驱动那个对话框的。「刷新」(账号池测试)保留。
28
+ - 探测放在**按下那一刻**而不是注册时:`entries('sidebar.settings')` 在
29
+ `whenAccess('settings/describe')` 通过之前是空的,注册时探测会永久拿到空数组。
30
+ - `.store` 缺失、没有 `create`、或拿到的 `create()` 不带 `actions.openSection`,三者都视为
31
+ 「没有这条路」:按钮照常画出,按下后显示一行失败说明,而不是画一个必然失败的按钮。
32
+ - 这是读另一个 bundle 声明的 store 座位,不是宿主发布的插件 API(见 `settings-route.ts` 的
33
+ JSDoc);接口变了,最坏结果是那行失败提示出现,不会崩。
34
+ - **「账号」分栏重新设计,纵向空间还给内容**(参考同族 `dsh-connect-workbuddy` 5.19.0 的
35
+ 同一次迁移,它踩过完全一样的坑)。
36
+ - **不接管滚动**。设置面板的 `.options` 列本来就负责滚动,本插件在里面再套一层
37
+ `overflow:auto` 就是两根滚动条。所以既不写 `height:100%`,也不写 `overflow`。
38
+ - **分栏条改为 `position:sticky`**(`top:-1px`,带外壳 layer-2 底色与 `z-index:2`)。
39
+ 账号池在折叠线以下,原来滚到那里时「模型 / 诊断」已经跟着滚出视野,想切分栏只能把整页
40
+ 滚回顶部。
41
+ - **页头压成两行网格**:标题 18px→15px/22px,简介 13px→12px/18px,行距由 `row-gap:2px`
42
+ 精确控制,页头 70px→60px。面板自己已有一层标题栏,这里不需要第二层大标题。
43
+ - **供应商芯片行的说明行视觉隐藏**(`clip` 而非 `display:none`,读屏仍可读到):它是三个
44
+ 芯片的注解,独占 36px,只在同时启用两个供应商时才有用,而那正是用户在看芯片的场景。
45
+ - 实测:首个可点控件从 y=351 上移到 y=287。
46
+ - **账号池表格在窄列下改为堆叠卡片**(`@container (max-width:700px)`)。
47
+ - 根因:本文件所有 `@media` 断点测的都是**视口**,而设置面板的内容列只有 564px,视口媒体
48
+ 查询在那里永不触发——五列表格被塞进 564px,固定轨道 `128+96+128` 加上四条 10px 间隙与
49
+ 两倍 14px 内边距后,两条弹性轨道只剩约 144px,**账号名列实测被压到 76px**,
50
+ 「用户7520607992」显示成「用户75」。
51
+ - 用 `container-type:inline-size` 让断点改测**表格自身**的宽度。特意放在表格上而不是祖先上:
52
+ `container-type` 会让该元素成为 `position:fixed` 后代的包含块,而忙碌遮罩
53
+ `.dsm-trae-veil` 与账号导入对话框都是这张表的**兄弟**节点,放在表格上让它们继续相对视口
54
+ 定位。表格内部只有表头与成员行,没有 fixed 后代。
55
+ - **一个阈值,不是两个**:中间再插一档「稍微窄一点就少一列」没有价值——设置面板那一列正好
56
+ 落在中档里,用户会拿到表头对不上的 2×2 网格。要么放得下这些列,要么放不下。
57
+ - 放不下时每行变成三行堆叠卡片:账号与操作各占满一行,中间三个事实共享一行。整体
58
+ `flex-direction:column` 会把每个单元格推成独立一行,行高翻倍且读起来像表单。
59
+ - **折叠的是内层单元格,不只是行包装**。这是本轮最容易漏的一处:第一版只把
60
+ `.dsm-trae-pool-row` 改成可换行的 flex 行,但真正竖排的是更里层——
61
+ `.dsm-trae-pool-probe-cell` 自己带 `flex-direction:column`,`.dsm-trae-pool-account`
62
+ 自己是三行网格。它们不改,状态列仍然是「正常 / 测得 3 天前 / 来自测速 /
63
+ upstream answered」四行,账号名反而被挤到「LaoDin」「用户75」——行更高且更窄。
64
+ (这个缺陷是用户截图发现的:本地 fixture 里三个账号的探针都是「尚未测速」,
65
+ 只渲染一行,正好把问题掩盖掉了。fixture 已补上真实探针记录。)
66
+ - 实测:账号名列 76px → 517px,不再截断;状态列 4 行 → 1 行(17px);
67
+ 账号池表格 412px → 304px,单行高度 138px → 102px。
68
+ - README 中英文同步更新配置位置说明。
69
+
70
+ ## 4.8.1 (2026-11-05)
71
+
72
+ ### 适配
73
+
74
+ - **DSH 0.2.2-rc.1 同步**。宿主这一版随包换代到 `cordis@4.1.0`、`schemastery@3.19.0`、
75
+ `@earendil-works/pi-ai@1.3.0`,插件消费的 13 个宿主包里有 8 个带实质变化。
76
+ - **唯一的契约破坏**:`ResolvedPiAiProviderProfile` 新增两个必填字段
77
+ `maxRequestImages` / `imageOffloadCountQuantum`。本插件从实时目录手工构造这个 profile,
78
+ 升级依赖后 `pnpm typecheck` 报 `TS2739`;已在 `REQUEST_IMAGE_BUDGETS` 补上宿主默认值
79
+ `50 / 10`(旧宿主忽略多余键,0.2.1 线仍类型通过)。
80
+ - **运行期无需适配**:4.8.0 在 0.2.2-rc.1 上照常工作——宿主只在配置解析路径给这两个字段
81
+ 兜底,手写 profile 传 `undefined` 时 0.2.2 走「不限制张数」分支(本机实测:usage 路由
82
+ 200、已登录)。本版把这两个字段显式声明,图像张数超限的流程才跟着宿主一起生效。
83
+ - **目录变化公告**:区域目录变化时按 provider 发一次 `llm/models-updated`(0.2.2 新增)。
84
+ `llm/adapters-updated` 只在路由拓扑变化时发,覆盖不到「同一路由换了 roster」;签名去重
85
+ 保证一次设置写入重建出相同行时不会惊动选择器。旧宿主上这是无人监听的未知事件。
86
+ - **模型标签补回别人的 badge**:`conversation.input.model.label` 是 `single` 槽,本插件
87
+ 占住后宿主的「名字 + `model.badge`」内置渲染对所有 provider 都不再生效。现在非 Trae 行
88
+ 按宿主顺序补回 badge(trigger 在前、菜单在后)并做语言回退;本插件自己的行行为不变。
89
+ - 不采纳:新语言 ja/es/fr/de/ru/pt(只维护 zh/en)、`settings.section.icon`(不拥有
90
+ `settings.section`)、`llm-pi-ai/request-headers`、`supportsMidConvoEffort`。
91
+ - 三大价值(双区域供应商 / 用量积分 / 每日签到)不受影响,对应测试全绿。
92
+ - **开发树同步**:13 个 `@deepseek-ai/dsh-*` devDependency → `0.2.2-rc.1`,`cordis` →
93
+ `^4.1.0`,`schemastery` → `3.19.0`(含 `pnpm-workspace.yaml` override),`pi-ai` →
94
+ `1.3.0`;`pnpm-lock.yaml` 重新生成。peerDependencies 零改动。
95
+ - `tests/dsh-peer-range.spec.ts` 的 `MUST_ADMIT` 补入 `0.2.2-rc.1`;新增目录公告与
96
+ badge 渲染用例(103 文件 / 1335 用例全绿)。
97
+
3
98
  ## 4.8.0 (2026-11-05)
4
99
 
5
100
  > **从 npm 升级的用户,这一版把 v4.7.0 也一起带上了。** v4.7.0 的 tag 与 GitHub
package/README.en.md CHANGED
@@ -24,7 +24,7 @@
24
24
  <a href="https://dshfind.com/plugins/dingminhua/dsh-connect-trae"><img src="https://dshfind.com/api/badge/dingminhua/dsh-connect-trae" alt="dshfind plugin"></a>
25
25
  </p>
26
26
 
27
- A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in Trae models (**CN, enterprise, and international installs**) to the DSH model picker. Trae generates structured tool calls while DSH executes its own local tools, with a usage overview (Work/general credits on CN, tenant allowance on enterprise, subscription status on international), daily check-in claiming, and a model-management panel. **The CN, enterprise, and international sides are three parallel providers (`trae` / `trae-enterprise` / `trae-global`) that can be used at the same time**; the settings card separates them with tabs for convenient management.
27
+ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in Trae models (**CN, enterprise, and international installs**) to the DSH model picker. Trae generates structured tool calls while DSH executes its own local tools, with a usage overview (Work/general credits on CN, tenant allowance on enterprise, subscription status on international), daily check-in claiming, and a model-management panel. **The CN, enterprise, and international sides are three parallel providers (`trae` / `trae-enterprise` / `trae-global`) that can be used at the same time**; the settings page (**Settings → Trae**, its own page in the settings nav) separates them with tabs for convenient management.
28
28
 
29
29
  **Runs on macOS, Windows, and Linux**: CI runs the full typecheck / test / build matrix on `ubuntu-latest` and `windows-latest`, and Windows gets purpose-built handling for directory probing, app version headers, and lock behavior — see [Windows notes](#windows-notes).
30
30
 
@@ -54,13 +54,13 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
54
54
  > `4011` is a **rate limit**, not "this model is unavailable" (measured for issue #33): an exhausted balance reports a different code, `4008`, so "I still have plenty of credits" and "I got a 4011" are both true at once. A throttled model answers again after a short wait — do not delete it.
55
55
  - **One-click test of all models** — the same probe applied to **every visible model** of the current region, not just the enabled ones. Use it when you want to know whether the models you have switched off could still answer — after a plan change, or after Trae reshuffles its lineup.
56
56
  - **Daily check-in claiming (per account)** — on the CN card **every account row carries its own check-in button**, with the day's reward written on it (`Check in +150`). A row reads **Claimed** and disables itself once *that account* has been paid, and reads **Device used** and disables itself when *this machine* has already spent today's slot through some other account — the two are reported honestly, and the second is never dressed up as "claimed". This is the plugin's **only** operation that changes account state, and it runs only when you click; one click targets that one row's account and sends exactly one request. There is deliberately **no "claim for every account"**: Trae allows **one check-in per device per day** (switching accounts does not reset the slot), so a sweep could only manufacture a row of false failures. The international side has no check-in activity, so no button is shown there.
57
- - **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes. **Both regions show it, but they do not show the same quantity, and that is the data's doing**: the CN side is credit-based, so it reads "Trae CN · <general credits>"; the international side is subscription-based and its usage document carries no credit field at all, so it reads "Trae Global · <plan>" (e.g. `Pro` / `Free`). The whole readout is clickable and opens a panel showing a TABLE: one row per account in the region — general credits on CN, plan on international — where clicking a row SWITCHES accounts (through the same merged `accounts` write the card uses, so the other region's selection survives). The panel's top right carries **two controls, Refresh and Settings, styled differently on purpose** — Settings navigates and carries the brand fill, while Refresh re-runs a batch in place and stays colourless until hovered. While Refresh runs its icon turns: the batch is serial, so the slowest account decides when it stops. **Refresh** asks every account checked into this panel's account pool to answer once (through the pool's `action=test` route — the same batch the pool section's own button runs) and reports the outcome in the panel's foot: `Pool tested · 2 of 2 answered`. **Two things about that press the label cannot say, so both live in the tooltip and in the foot line**: it tests the **pool's checked-in members**, not every account in the table you are looking at, and it is the **only press in this plugin that spends credits** (one minimal ping per member) — a bare "Refresh" reads as free, so the tooltip is worth reading. **Settings** opens this plugin's own settings page and closes the panel, so "where do I change this?" no longer means closing the popover and hunting the Plugins row. The balance needs no refresh button of its own — it re-reads when the panel opens, on every account switch, and every five minutes, which is why that button was replaced rather than joined by another. It appears **only while the session's selected model belongs to this plugin**, and renders nothing for any other provider — which is what keeps two connector plugins from competing for one shared row (the sidebar foot is a shared root row; the composer row is per-session and provider-scoped).
57
+ - **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes. **Both regions show it, but they do not show the same quantity, and that is the data's doing**: the CN side is credit-based, so it reads "Trae CN · <general credits>"; the international side is subscription-based and its usage document carries no credit field at all, so it reads "Trae Global · <plan>" (e.g. `Pro` / `Free`). The whole readout is clickable and opens a panel showing a TABLE: one row per account in the region — general credits on CN, plan on international — where clicking a row SWITCHES accounts (through the same merged `accounts` write the card uses, so the other region's selection survives). The panel's top right carries **two controls, Refresh and Settings** (as of 4.9.0 the Settings button targets the settings panel; an interim revision removed it on the belief that this was impossible, which was wrong — the host publishes no id-targeted *service*, but the settings shell pins its store seat to the registration so `slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` is a real route, and the host's own tests drive the dialog exactly that way). While Refresh runs its icon turns: the batch is serial, so the slowest account decides when it stops. **Refresh** asks every account checked into this panel's account pool to answer once (through the pool's `action=test` route — the same batch the pool section's own button runs) and reports the outcome in the panel's foot: `Pool tested · 2 of 2 answered`. **Two things about that press the label cannot say, so both live in the tooltip and in the foot line**: it tests the **pool's checked-in members**, not every account in the table you are looking at, and it is the **only press in this plugin that spends credits** (one minimal ping per member) — a bare "Refresh" reads as free, so the tooltip is worth reading. To change anything, open **Settings → Trae** — the plugin's only configuration surface. The balance needs no refresh button of its own — it re-reads when the panel opens, on every account switch, and every five minutes, which is why that button was replaced rather than joined by another. It appears **only while the session's selected model belongs to this plugin**, and renders nothing for any other provider — which is what keeps two connector plugins from competing for one shared row (the sidebar foot is a shared root row; the composer row is per-session and provider-scoped).
58
58
 
59
59
  <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="560" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
60
60
 
61
61
  **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 panel** (all three 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).
62
62
 
63
- - **Optional: put Trae in the sidebar** (learned from the sibling `dsh-connect-workbuddy` PR #188, and **off by default** here too). Ticking "Show Trae in the sidebar" in the card's settings block adds a Trae row to the DSH sidebar; it opens the **full-page version of this same card** — same region tabs, same account / models / diagnostics sections, minus the card frame, with a title, a description and "Back to conversation" at the top. **Why it is off by default**: the sidebar's global rows are the host's own pages, and this row claims a seat **every installed bundle shares** while the Plugins page already reaches the same content — a nav row nobody asked for re-arranges a surface this plugin does not own. **It takes effect on the click** with no Save: writing `sidebarPanel` registers or disposes the row in the same second (it is not a component rendering null — that would leave a blank but still clickable button), and switching it off while the page is open sends the centre column back to the conversation by itself. The page's back control goes through the host's `layout.selectPanel(null)`; on a host without that service the control is **omitted rather than drawn dead**.
63
+ - **Optional: put Trae in the sidebar** (learned from the sibling `dsh-connect-workbuddy` PR #188, and **off by default** here too). Ticking "Show Trae in the sidebar" in the card's settings block adds a Trae row to the DSH sidebar; it opens the **full-page version of this same card** — same region tabs, same account / models / diagnostics sections, minus the card frame, with a title, a description and "Back to conversation" at the top. **Why it is off by default**: the sidebar's global rows are the host's own pages, and this row claims a seat **every installed bundle shares** while the settings page already reaches the same content — a nav row nobody asked for re-arranges a surface this plugin does not own. **It takes effect on the click** with no Save: writing `sidebarPanel` registers or disposes the row in the same second (it is not a component rendering null — that would leave a blank but still clickable button), and switching it off while the page is open sends the centre column back to the conversation by itself. The page's back control goes through the host's `layout.selectPanel(null)`; on a host without that service the control is **omitted rather than drawn dead**.
64
64
  <img src="docs/assets/dsh-connect-trae-sidebar-page.png" width="720" alt="The sidebar page: back control, title and description at the top, then the same card body below" />
65
65
 
66
66
 
@@ -197,7 +197,7 @@ Restart the DSH process after install/update/uninstall.
197
197
  ## DSH version compatibility
198
198
 
199
199
  **Requires DSH 0.1.7-rc.1 and up** (since 2.3.0 the plugin no longer targets hosts older than 0.1.7; the peer dependency ranges are narrowed accordingly).
200
- **The peer upper bound is `<0.3.0-0`, so both the 0.1.7 and the 0.2 line are supported (including 0.2.1-rc.5, verified).**
200
+ **The peer upper bound is `<0.3.0-0`, so both the 0.1.7 and the 0.2 line are supported (including 0.2.2-rc.1, verified).**
201
201
 
202
202
  ### Both pi-ai context generations are supported (since 3.4.2)
203
203
 
@@ -257,6 +257,22 @@ For **this plugin**:
257
257
 
258
258
  Details: [section 11 of `docs/DSH_0.2.1_RC1_SYNC.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.1_RC1_SYNC.md).
259
259
 
260
+ ### `0.2.2-rc.1` sync (since 4.8.1)
261
+
262
+ The host moved to `0.2.2-rc.1`. This release is **not just version bumps**: the bundled dependencies move as a set (`cordis` 4.0.5 → **4.1.0**, `schemastery` 3.18.5 → **3.19.0**, `@earendil-works/pi-ai` 1.1.0 → **1.3.0**), and **8 of the 13 host packages** this plugin consumes carry real changes.
263
+
264
+ For **this plugin**:
265
+
266
+ - **One contract break**: `dsh-llm-pi-ai` added two **required** fields to `ResolvedPiAiProviderProfile` — `maxRequestImages` and `imageOffloadCountQuantum`. This plugin builds that profile **by hand** from the live catalog (it bypasses the host's config resolution), so `pnpm typecheck` fails with `TS2739` after the bump. Both are now declared in `REQUEST_IMAGE_BUDGETS` with the host's own defaults, `50 / 10`.
267
+ - **Runtime was never broken**: the host only defaults those fields on its config-resolution path; a hand-built profile that omits them takes the "image count unchecked" branch on 0.2.2. That is why 4.8.0 keeps working there (verified on the desktop host: `GET /plugins/dsh-connect-trae/usage` → 200, `signed-in`).
268
+ - **New capabilities adopted where they matter**: the plugin now emits `llm/models-updated(provider)` when a region's catalog changes (deduplicated by a signature, so a settings write that rebuilds identical rows does not wake every consumer). And because `conversation.input.model.label` is a `single` slot, holding it suppresses the host's built-in "name + `model.badge`" label for **every** provider — so the component now renders another provider's `model.badge` (with language fallback) instead of dropping it. Its own rows are unchanged.
269
+ - **Deliberately not adopted**: the six new UI languages (this plugin keeps zh/en and falls back to English), `llm-pi-ai/request-headers` (no custom inference headers), `supportsMidConvoEffort` (not on the openai-completions route).
270
+ - **The three core values are unaffected**: dual-region providers, usage/credits overview, daily check-in. Their tests are green; this change only touches the image budget, catalog announcements, and the model label.
271
+ - The other six changed packages (`dsh-host-webserver` resource routes, `dsh-attachment` admission variants, the new languages in `dsh-client-locale` / `dsh-client-ui-settings-plugins`, the `dsh-client-ui-settings` icon slot, `dsh-client-ui-primitives` additions) are **additive**; this plugin either does not use them or already has an equivalent. The five unchanged packages (`dsh-atomic-write`, `dsh-client-ui-renderer`, `dsh-client-ui-slots`, `dsh-home-paths`, `dsh-settings`) are **byte-for-byte identical**, so every slot contract this plugin registers is unchanged.
272
+ - The peer range **needs no change**: `0.2.2-rc.1` is admitted by `>=0.1.7-rc.1 <0.3.0-0`, and `MUST_ADMIT` in `tests/dsh-peer-range.spec.ts` now includes it.
273
+
274
+ Details: [`docs/DSH_0.2.2_RC1_SYNC.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.2_RC1_SYNC.md).
275
+
260
276
 
261
277
  ### Why the upper bound is not `<0.2.0`
262
278
 
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  <a href="https://dshfind.com/plugins/dingminhua/dsh-connect-trae"><img src="https://dshfind.com/api/badge/dingminhua/dsh-connect-trae" alt="dshfind plugin"></a>
25
25
  </p>
26
26
 
27
- 一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 Trae 账号(**国内版、企业版与国际版均支持**)接到 DSH 的模型选择器:模型负责生成结构化工具调用,`bash` / `read` / `write` / `edit` 等工具由 DSH 本地执行;同时提供用量概览(国内版 Work/通用积分、企业版租户额度、国际版订阅状态)、每日签到领取与模型管理界面。**国内版、企业版与国际版是三个并行的供应商(`trae` / `trae-enterprise` / `trae-global`),可同时使用**;插件设置卡片以 tab 区分三者,方便统一管理。
27
+ 一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 Trae 账号(**国内版、企业版与国际版均支持**)接到 DSH 的模型选择器:模型负责生成结构化工具调用,`bash` / `read` / `write` / `edit` 等工具由 DSH 本地执行;同时提供用量概览(国内版 Work/通用积分、企业版租户额度、国际版订阅状态)、每日签到领取与模型管理界面。**国内版、企业版与国际版是三个并行的供应商(`trae` / `trae-enterprise` / `trae-global`),可同时使用**;插件设置页(**设置 → Trae**,侧边栏导航里的独立一页)以 tab 区分三者,方便统一管理。
28
28
 
29
29
  **支持 macOS、Windows 与 Linux**:CI 在 `ubuntu-latest` 与 `windows-latest` 上跑完整 typecheck / 测试 / 构建;Windows 侧的目录探测、应用版本头与锁行为均有针对性处理,详见 [Windows 说明](#windows-说明)。
30
30
 
@@ -37,7 +37,7 @@
37
37
  - **三供应商并行接入** —— 国内版注册为 DSH 的 `trae` provider(`DeepSeek-V4-Flash`、`DeepSeek-V4-Pro` 等),企业版注册为 `trae-enterprise`,国际版注册为 `trae-global`(Gemini / GPT / MiniMax 阵容),**三边模型同时出现在 DSH 模型选择器里**:不同会话可以各选一边,互不干扰。
38
38
  - **企业版是独立计费通道,不是国内版的别名** —— 同一张 CN 凭据若属于 `tob_online` 租户,会被**两个**网关同时服务:消费池(扣个人积分)与租户池(扣企业额度)。同一模型、两种付费方,所以「谁付钱」由你显式选,插件不替你猜。`trae` 永不碰企业网关;`trae-enterprise` **不回落**,被拒绝就如实报错,不会悄悄改用个人额度重跑。企业版**默认关闭**——纯个人账号用不了企业池,默认放开等于塞一条一用就失败的 provider。
39
39
  - **可单独关闭任一面板** —— 三个 tab 各带一个勾选(国内版与国际版默认勾选,企业版默认不勾):取消勾选即把该面板的供应商**整个从 DSH 模型选择器撤掉**(不是只藏起 tab),并且不再发起任何请求;重新勾选即恢复,账号、目录、勾选与上下文预算全部保留。用不到国际版就把国际版关掉即可。
40
- - **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 企业版 / 国际版」三个 tab,各含独立的账号选择、用量概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。国内版与企业版共用同一份 CN 凭据池,但**各有各的账号绑定与模型勾选**:两个池的名录并不相同(`DeepSeek-V4.1-Flash` 只有企业池列),所以勾选互不覆盖。
40
+ - **设置页 tab 切换** —— 设置页顶部为「国内版 / 企业版 / 国际版」三个 tab,各含独立的账号选择、用量概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。国内版与企业版共用同一份 CN 凭据池,但**各有各的账号绑定与模型勾选**:两个池的名录并不相同(`DeepSeek-V4.1-Flash` 只有企业池列),所以勾选互不覆盖。
41
41
  - **卡片内再按「账号 / 模型 / 诊断」分栏**(学自同族 `dsh-connect-workbuddy`)—— 上面那排地区是**作用域**(这张卡片讲的是哪个供应商),这一排是同一张卡片里的**分栏**:账号栏管「谁在服务、还剩多少、签到与账号池」,模型栏管「勾哪些模型、图片输入、上下文预算、保存」,诊断栏只读地列出面板名、供应商 id、服务账号、两种令牌到期时间、目录/已启用模型数、账号池与签到支持与否,以及本面板最近一次报错的**原文**。**4.4.7 起诊断栏还多一行「损坏副本留存」**:读不出来、因此被改名留存的账号副本会在这里被点名(连同它曾经保存了几个账号)——被顶掉的账号「去哪了」这个问题,从此不用再去 `~/.dsh` 里翻文件名;未登录那一屏也会显示同一条通知,因为账号消失之后用户落地的正是那一屏。分栏的理由是**提交语义不同**:账号改动点一下即生效,模型改动是草稿、必须点「保存」——一列长页会把保存按钮放到离它要保存的东西三屏之外。切栏不重新拉取、不丢未保存的模型草稿、账号池日志也不重建;未登录时不出分栏,那一屏说明不会被看不见的 tab 藏起来。**模型栏把所有模型一次性铺开,没有内层滚动条**——不再是一个套在滚动页面里的小窗口,唯一滚动的就是卡片自己那一页。
42
42
  - **账号池:自动选号 + 失败换号(默认关闭)** —— 本机读完几个账号后,「该用谁的额度」可以交给插件按规则排序:**先排除不可用的**(凭据被拒、套餐不覆盖、账号已不存在、限流未到重试时间、额度耗尽),再在可用账号里**优先快到期的那点额度**(与其放着过期,不如先花掉),最后才比总额、token 新鲜度,并以账号 id 兜底成**全序**(同样的输入永远得到同样的顺序,不会在两行之间来回抖)。三个面板**各有各的池**:国内版与企业版共用同一份 CN 凭据,但企业版只挑租户账号、国内版不会漂到租户号上。开着池时,发请求前先按上面的顺序选中一个账号,并且**真实请求失败会换下一个账号重试(最多 3 个)**——只有账号级失败才换(`401` 凭据被拒、额度耗尽、限流、上游 5xx);模型 id 写错、请求本身不合法**不换号**,因为那不是账号的问题,换号只会把同一个错误烧两遍、还冤枉一个没机会服务的账号。换号失败的真相会写进池的探针记录,下一次排序直接把它排到后面,而每一次结论都能在卡片上看到:`可用` / `限流` / `额度耗尽` / `凭据被拒` / `套餐不覆盖` / `上游不提供` / `本次未测出`,以及「什么时候可以重试」。**关掉池就回到严格绑定**,不会有任何残留 override。卡片里的「全部测速」会把每个成员按目标模型各测一次,结果**边跑边显示**(一行一个答案,不等整批跑完),并落盘成下次排序的依据。
43
43
 
@@ -59,13 +59,13 @@
59
59
  > `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011」两句话可以同时成立。被限流的模型**等你一会儿再试**即可,不要删。
60
60
  - **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
61
61
  - **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求。Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),所以逐账号签到始终是主路径。账号池里另有一个「全部签到」,它**不是**把同一件事盲目重复:一旦上游回 `9095`(本机今日已领),整批立刻停止并把剩余账号如实标成「本机已签」,产出的是真相而不是一排假失败。国际版没有签到活动,因此不显示该按钮。
62
- - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择)。浮层右上角是**「刷新」和「设置」两个控件**(不同造型,因为它们不是同一类动作:设置是导航、带品牌色底;刷新是就地重跑一批、悬停前不着色)。按「刷新」时图标会转,直到这批探测结束——它是串行跑的,最慢的账号决定什么时候结束。**「刷新」**把账号池里勾选的成员逐个问一遍「能不能应答」(走账号池那条 `action=test` 路由,与账号池分栏里的测试是同一个动作),结果直接报在浮层底部:`账号池已测 · 2 of 2 可应答`。**这个按钮有两件事标签说不出来,都写在悬停提示与结果行里**:它测的是**账号池里勾选的成员**,不是你眼前这张表里的每个账号;而且它是**插件里唯一消耗额度**的按钮(每个成员一条最小 `ping`)——一个光秃秃的「刷新」读起来是免费的,所以工具提示必须留着看。**「设置」**一点就跳到本插件自己的设置页,浮层同时关闭——「这数字去哪改」从此不必先关浮层、再去插件页里找这一行。积分本身不需要刷新按钮:**打开浮层、切换账号、每 5 分钟**都会重新读一次,所以那个按钮被换掉而不是并排加进来。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
62
+ - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择)。浮层右上角是**「刷新」与「设置」两个控件**(4.9.0 起「设置」改指设置面板;中途曾以为做不到而删掉,是错的——宿主确实没有 id 定向的服务,但设置面板把自己的 store 座位钉在注册声明上,`slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` 是真实可用的路径,宿主自己的测试就是这么驱动的)。按「刷新」时图标会转,直到这批探测结束——它是串行跑的,最慢的账号决定什么时候结束。**「刷新」**把账号池里勾选的成员逐个问一遍「能不能应答」(走账号池那条 `action=test` 路由,与账号池分栏里的测试是同一个动作),结果直接报在浮层底部:`账号池已测 · 2 of 2 可应答`。**这个按钮有两件事标签说不出来,都写在悬停提示与结果行里**:它测的是**账号池里勾选的成员**,不是你眼前这张表里的每个账号;而且它是**插件里唯一消耗额度**的按钮(每个成员一条最小 `ping`)——一个光秃秃的「刷新」读起来是免费的,所以工具提示必须留着看。要改配置就去「设置 → Trae」:那是本插件唯一一处配置界面。积分本身不需要刷新按钮:**打开浮层、切换账号、每 5 分钟**都会重新读一次,所以那个按钮被换掉而不是并排加进来。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
63
63
 
64
64
  <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="560" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
65
65
 
66
66
  **读数显示在哪里可以选**(学自同族 `dsh-connect-workbuddy` issue #62)。默认在**输入框工具栏**(即上面说的位置);如果那里被别的插件占了、读数不显示,改成**输入框下方**——那是「轮次 / 步数 / token」统计所在的那一行,读数是**追加**在官方统计之后,不会顶掉它们。位置**按面板分别设置**(三个面板各存各的),在卡片「设置」区紧挨「在输入框工具栏显示积分读数」那个勾选框。之所以要有第二个位置:输入框工具栏是**共享的 list 插槽**,外壳自己的控件和其他插件都注册在那里,而共享插槽**无法保证每个占用者都可见**——同族的报告者装了第三方插件后读数就整个消失了(本插件早年放在侧边栏底部时也被挤成过一个「Tr」的残片)。
67
67
 
68
- - **可选:把 Trae 放进侧边栏**(学自同族 `dsh-connect-workbuddy` 的 PR #188,同样**默认关闭**)。在卡片「设置」区勾上「在侧边栏显示 Trae」,DSH 侧边栏就多一个 Trae 行,点开是**这张卡片的整页版**:同样的地区页签、同样的账号 / 模型 / 诊断分栏,只是没有卡片边框,顶部是标题、说明和「返回会话」。**默认关闭的理由**:侧边栏的全局行是宿主自己的页面,这一行占的是**所有已安装插件共用**的座位,而「插件」页本来就能看到同样的内容——一行没人要求的导航会改变一个插件并不拥有的界面。**点一下即生效**,不用保存:开关写入 `sidebarPanel` 后,注册与注销在同一秒发生(不是把组件渲染成 null——那会留下一个空白却仍可点的按钮);页面开着时关掉它,中心栏会自己回到会话。整页的「返回会话」走宿主的 `layout.selectPanel(null)`,宿主没这个服务时**不画**这个按钮,而不是画一个点不动的。
68
+ - **可选:把 Trae 放进侧边栏**(学自同族 `dsh-connect-workbuddy` 的 PR #188,同样**默认关闭**)。在卡片「设置」区勾上「在侧边栏显示 Trae」,DSH 侧边栏就多一个 Trae 行,点开是**这张卡片的整页版**:同样的地区页签、同样的账号 / 模型 / 诊断分栏,只是没有卡片边框,顶部是标题、说明和「返回会话」。**默认关闭的理由**:侧边栏的全局行是宿主自己的页面,这一行占的是**所有已安装插件共用**的座位,而「设置 → Trae」本来就能看到同样的内容——一行没人要求的导航会改变一个插件并不拥有的界面。**点一下即生效**,不用保存:开关写入 `sidebarPanel` 后,注册与注销在同一秒发生(不是把组件渲染成 null——那会留下一个空白却仍可点的按钮);页面开着时关掉它,中心栏会自己回到会话。整页的「返回会话」走宿主的 `layout.selectPanel(null)`,宿主没这个服务时**不画**这个按钮,而不是画一个点不动的。
69
69
  <img src="docs/assets/dsh-connect-trae-sidebar-page.png" width="720" alt="Trae 侧边栏整页版:顶部是返回会话、标题与说明,下面是同一张卡片的地区页签和账号栏" />
70
70
 
71
71
 
@@ -216,7 +216,7 @@ npm install dsh-connect-trae
216
216
  ## DSH 版本兼容
217
217
 
218
218
  **要求 DSH 0.1.7-rc.1 及以上**(2.3.0 起不再支持 0.1.7 之前的宿主;peer 依赖范围已按此收窄)。
219
- **peer 上界为 `<0.3.0-0`:0.1.7 与 0.2 两条线均受支持(含 0.2.1-rc.5,已实测)。**
219
+ **peer 上界为 `<0.3.0-0`:0.1.7 与 0.2 两条线均受支持(含 0.2.2-rc.1,已实测)。**
220
220
 
221
221
  ### 同时支持 pi-ai 的两代上下文(3.4.2 起)
222
222
 
@@ -317,6 +317,40 @@ stderr、View 标签的 tooltip 只在文字溢出时显示、More 菜单 toolti
317
317
  细节见 [`docs/DSH_0.2.1_RC1_SYNC.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.1_RC1_SYNC.md) 第十一节。
318
318
 
319
319
 
320
+ ### `0.2.2-rc.1` 同步结论(4.8.1 起)
321
+
322
+ 宿主推进到 `0.2.2-rc.1`。这一版**不只是版本号**:随包依赖整体换代(`cordis` 4.0.5 →
323
+ **4.1.0**、`schemastery` 3.18.5 → **3.19.0**、`@earendil-works/pi-ai` 1.1.0 → **1.3.0**),
324
+ 插件消费的 13 个宿主包里 **8 个有实质变化**。
325
+
326
+ 对**本插件**而言:
327
+
328
+ - **唯一的契约破坏**:`dsh-llm-pi-ai` 给 `ResolvedPiAiProviderProfile` 新增了两个**必填**
329
+ 字段 `maxRequestImages` / `imageOffloadCountQuantum`。本插件从实时目录**手工构造**这个
330
+ profile(不走宿主的配置解析),所以升级依赖后 `pnpm typecheck` 报 `TS2739`。已在
331
+ `REQUEST_IMAGE_BUDGETS` 补上宿主默认值 `50 / 10`。
332
+ - **运行期原本就没坏**:宿主只在配置解析路径里给这两个字段兜底,手写 profile 传
333
+ `undefined` 时 0.2.2 走「不限制张数」,因此 4.8.0 在 0.2.2 上照常工作(本机实测:桌面端
334
+ 0.2.2-rc.1 + 插件 4.8.0,`GET /plugins/dsh-connect-trae/usage` 返回 200 且 `signed-in`)。
335
+ - **新增能力按需采纳**:目录变化时按 provider 发 `llm/models-updated`(签名去重,避免每次
336
+ 设置写入都惊动选择器);`conversation.input.model.label` 是 `single` 槽,本插件占住后会
337
+ 压掉宿主的「名字 + `model.badge`」内置渲染,因此对**别的 provider** 的模型补回了 badge
338
+ (含语言回退),本插件自己的行行为不变。
339
+ - **明确不采纳**:新语言 ja / es / fr / de / ru / pt(只维护 zh / en,其余回退英文)、
340
+ `llm-pi-ai/request-headers`
341
+ (不需要额外请求头)、`supportsMidConvoEffort`(openai-completions 路线用不到)。
342
+ - **三大价值不受影响**:双区域供应商、用量与积分概览、每日签到领取——对应测试全绿,本次
343
+ 只碰图像预算、目录公告与模型标签三处。
344
+ - 其余 6 个有变化的包(`dsh-host-webserver` 的资源路由、`dsh-attachment` 的准入类型扩展、
345
+ `dsh-client-locale` / `dsh-client-ui-settings-plugins` 的新语言、`dsh-client-ui-settings`
346
+ 的 icon 槽位、`dsh-client-ui-primitives` 的新增属性)**全部是新增**,本插件未使用或已有
347
+ 等价实现;`dsh-atomic-write` / `dsh-client-ui-renderer` / `dsh-client-ui-slots` /
348
+ `dsh-home-paths` / `dsh-settings` 这 5 个包**逐字节未变**,槽位契约不变。
349
+ - peer 范围**无需改动**:`0.2.2-rc.1` 已被 `>=0.1.7-rc.1 <0.3.0-0` 放行,
350
+ `tests/dsh-peer-range.spec.ts` 的 `MUST_ADMIT` 已补入。
351
+
352
+ 细节见 [`docs/DSH_0.2.2_RC1_SYNC.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.2_RC1_SYNC.md)。
353
+
320
354
  ### peer 上界为什么不写 `<0.2.0`
321
355
 
322
356
  2.3.1 曾写 `<0.2.0-0`,意图是「先不支持 0.2.0 正式版」。这个写法**是错的**:
package/lib/client.js CHANGED
@@ -999,7 +999,16 @@ window.__ModuleLoader__.load({
999
999
  differs, so it sits inside the tab cell without crowding the label. */
1000
1000
  .dsm-trae-tab-switch{flex:none;margin-left:auto;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24))}
1001
1001
  .dsm-trae-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:7px;vertical-align:baseline}
1002
- .dsm-trae-tab-hint{margin:0;padding:0 2px;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
1002
+ /* 供应商行的说明:视觉隐藏,读屏仍在(learned from workbuddy 5.19.0 的窄列实践)。
1003
+ *
1004
+ * 这一行是三个芯片的注解(「企业版与国内版共用同一份 Trae CN 登录」),原本独占
1005
+ * 一整行 36px,把账号池往下推。在 564px 的列里,纵向空间是稀缺资源,而这句话
1006
+ * 只在「同时启用两个供应商」时才有用——那个场景下用户已经在看两个芯片了。
1007
+ *
1008
+ * 用 clip 而不是 display:none:display:none 会把它从无障碍树里摘掉,读屏用户
1009
+ * 就永远拿不到这条例外说明。视觉隐藏保留了它,且不占布局高度。
1010
+ * 完整文案仍可通过芯片行的 title 悬停看到(见 TraeUsageCard 的 tabSwitchHint)。 */
1011
+ .dsm-trae-tab-hint{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
1003
1012
  /* SECTION TABS: the card's own top-level split (account / models / diagnostics).
1004
1013
  *
1005
1014
  * Deliberately NOT the boxed chips above. Those are the REGION scope — a
@@ -1014,7 +1023,20 @@ window.__ModuleLoader__.load({
1014
1023
  * inactive panel out of the accessibility tree, which is exactly what
1015
1024
  * role="tabpanel" wants — unlike display:none reached by a CSS class, the user
1016
1025
  * agent's own rule for [hidden] cannot be overridden by a later declaration. */
1017
- .dsm-trae-panels{display:flex;align-items:stretch;gap:2px;margin:4px 0 0;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24))}
1026
+ /* 分栏条钉在外壳的滚动列顶部(STICKY, not a second scroller)。
1027
+ *
1028
+ * 站点:本插件**不**接管滚动。设置面板的 .options 列本来就负责滚动(它自己的
1029
+ * CSS 注释写明了这一点),在里面再套一层 overflow:auto 就是两根滚动条,滚轮
1030
+ * 会在两者之间来回跳。所以这里既不写 height:100%,也不写 overflow。
1031
+ *
1032
+ * 但「不接管滚动」不等于「分栏条可以滚走」:账号池在折叠线以下,滚到那里时
1033
+ * 「模型 / 诊断」已经跟着滚出了视野,想切分栏只能先把整页滚回顶部。sticky 让它
1034
+ * 在任何滚动位置都留在顶部,代价只是 z-index 与一层不透明底色——否则滚上来的
1035
+ * 账号卡片会从字缝里透出来(外壳的 layer-2 正是这一列自己的底色)。
1036
+ *
1037
+ * top:-1px 而不是 0:分栏条自己的 border-bottom 画在盒子外侧,写成 0 会在它上方
1038
+ * 留一条 1px 的缝,滚动的行会从这条缝里露出来。 */
1039
+ .dsm-trae-panels{position:sticky;top:-1px;z-index:2;display:flex;align-items:stretch;gap:2px;margin:4px 0 0;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));background:var(--dsw-alias-bg-layer-2,#24262c)}
1018
1040
  .dsm-trae-panel-tab{appearance:none;font:inherit;cursor:pointer;border:0;background:transparent;padding:7px 11px;margin-bottom:-1px;border-bottom:2px solid transparent;color:var(--dsw-alias-label-tertiary,#999);font-size:13px;font-weight:600;line-height:20px;white-space:nowrap;transition:color .15s,border-color .15s}
1019
1041
  .dsm-trae-panel-tab:hover:not(.dsm-trae-panel-tab-active){color:var(--dsw-alias-label-primary,#e6e6e6)}
1020
1042
  .dsm-trae-panel-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
@@ -1577,7 +1599,25 @@ window.__ModuleLoader__.load({
1577
1599
  .dsm-trae-pool-subnote{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#9aa0a8)}
1578
1600
  .dsm-trae-pool-select-actions{display:flex;flex:none;gap:6px}
1579
1601
  .dsm-trae-pool-select-actions .dsm-btn,.dsm-trae-pool-row-actions .dsm-btn{padding:4px 10px;font-size:12px;line-height:1.4}
1580
- .dsm-trae-pool-table{--dsm-trae-pool-cols:minmax(0,1.4fr) 128px minmax(0,1.2fr) 96px 128px;display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:var(--dsm-trae-pool-radius);overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
1602
+ /* CONTAINMENT ROOT(learned from workbuddy 5.19.0 的同一次迁移)。
1603
+ *
1604
+ * 这张卡同时画在三处,宽度与窗口毫无关系:输入框卡片、整页版(上限 860px)、
1605
+ * 以及设置面板的内容列(1400px 屏上是 564px)。本文件里所有 @media 断点测的
1606
+ * 都是**视口**,所以在设置面板里它们永远不触发——564px 的列拿到了五列桌面表格,
1607
+ * 每个单元格都在截断(实测账号列被压到 76px,「用户7520607992」只剩「用户75」)。
1608
+ *
1609
+ * container-type:inline-size 让下面的断点改测**这张表**的宽度,那才是行真正要
1610
+ * 塞进去的宽度。
1611
+ *
1612
+ * 特意放在表格上而不是祖先上:container-type 会让这个元素成为 position:fixed
1613
+ * 后代的包含块。忙碌遮罩 .dsm-trae-veil 与账号导入对话框都是这张表的兄弟节点
1614
+ * (挂在 .dsm-trae-pool 或 cardBody 上,不在表格里),放在表格上就让它们继续
1615
+ * 相对视口定位——全屏遮罩本来该在那里。表格内部只有表头与成员行,没有 fixed 后代。
1616
+ *
1617
+ * overflow:hidden 与 container-type 冲突:容器查询要求这个盒子能被"测量",而
1618
+ * overflow 会改变它的块格式化上下文。这里原本就有 overflow:hidden(用来裁圆角),
1619
+ * 二者在本例中兼容——表格自身不滚动,只是裁掉子元素的直角。 */
1620
+ .dsm-trae-pool-table{--dsm-trae-pool-cols:minmax(0,1.4fr) 128px minmax(0,1.2fr) 96px 128px;container-type:inline-size;display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:var(--dsm-trae-pool-radius);overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
1581
1621
  .dsm-trae-pool-table-no-checkin{--dsm-trae-pool-cols:minmax(0,1.4fr) 128px minmax(0,1.2fr) 128px}
1582
1622
  .dsm-trae-pool-row{display:grid;grid-template-columns:var(--dsm-trae-pool-cols);align-items:start;gap:10px;padding:var(--dsm-trae-pool-pad-y) var(--dsm-trae-pool-pad-x);border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}
1583
1623
  .dsm-trae-pool-row:first-child{border-top:0}
@@ -1645,6 +1685,59 @@ window.__ModuleLoader__.load({
1645
1685
  .dsm-trae-pool-save-row{display:flex;align-items:center;justify-content:space-between;gap:12px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:12px;position:sticky;bottom:0;z-index:2;padding-bottom:8px;background:var(--dsw-alias-bg-layer-3,#2a2c33)}
1646
1686
  .dsm-trae-pool-save-buttons{display:flex;align-items:center;justify-content:flex-end;gap:8px}
1647
1687
  .dsm-trae-pool-save-row .dsm-btn{padding:5px 14px;font-size:13px;line-height:1.5}
1688
+
1689
+ /* THE TABLE'S NARROW FORM — measured against the table, not the window
1690
+ * (learned from workbuddy 5.19.0, which hit the identical bug in the identical
1691
+ * move from a plugin card to a settings.section page).
1692
+ *
1693
+ * 上面的 @media 断点测的都是视口。这张卡现在画在设置面板的内容列里(564px),
1694
+ * 视口媒体查询在那里永远不触发:五列表格被塞进 564px,固定轨道 128+96+128 加
1695
+ * 四条 10px 间隙与两倍 14px 内边距之后,留给账号名与探针两条弹性轨道的只剩
1696
+ * ~144px,实测账号名列 = 76px。
1697
+ *
1698
+ * 一个阈值,不是两个。中间再插一档「稍微窄一点就少一列」是没有价值的分支:
1699
+ * 设置面板那一列正好落在中档里,用户会拿到一个表头对不上的 2×2 网格。表格没有
1700
+ * 「稍微窄」的形态值得保留——要么放得下这些列,要么放不下。
1701
+ *
1702
+ * 放不下的时候,每行变成一张**堆叠卡片**:同样是那五个事实,宽的时候从左往右
1703
+ * 读,现在从上往下读。这是让窄行可读而不只是能塞下的关键。
1704
+ *
1705
+ * 三行,不是五行:整体 flex-direction:column 会把每个单元格推成独立一行,行高翻
1706
+ * 倍、读起来像表单而不是一个账号。账号与操作各占满一整行(最需要辨认的一端与
1707
+ * 最需要操作的一端),中间三个事实共享中间一行,只有真的放不下时才在自己之间换行。
1708
+ *
1709
+ * .dsm-trae-pool-row-head 隐藏:它的标签描述的是已经不存在的轨道。 */
1710
+ @container (max-width:700px){
1711
+ .dsm-trae-pool-row{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;padding:var(--dsm-trae-pool-pad-y) 12px}
1712
+ .dsm-trae-pool-row-head{display:none}
1713
+ .dsm-trae-pool-account{flex:1 1 100%;align-items:center}
1714
+ .dsm-trae-pool-credit{flex:0 1 auto;flex-direction:row;flex-wrap:wrap;align-items:baseline;gap:2px 8px}
1715
+ .dsm-trae-pool-probe{flex:1 1 auto}
1716
+ .dsm-trae-pool-checkin{flex:none}
1717
+ .dsm-trae-pool-row-actions{flex:1 1 100%;justify-content:flex-start;padding-top:4px}
1718
+ /* THE PROBE CELL'S OWN COLUMN, collapsed to one line.
1719
+ *
1720
+ * 这一条是最容易漏掉的:.dsm-trae-pool-probe 只是个 min-width:0 的外层包装,
1721
+ * 真正竖排的是它内部的 .dsm-trae-pool-probe-cell(flex-direction:column)。
1722
+ * 只改外层包装的话,状态列仍然一句一行,被挤成「正常 / 测得 3 天前 / 来自测速 /
1723
+ * upstream answered」四行——旁边只有 12px 间距,整行高度就被这一格撑到 100px
1724
+ * 以上,账号名也跟着被压窄(实测截成「LaoDin」「用户7」)。
1725
+ *
1726
+ * 改成横排并允许在自己内部换行:这四个事实读起来本来就是一句话
1727
+ * (「正常 · 测得 3 天前 · 来自测速 · upstream answered」),宽时从左排到右,
1728
+ * 窄时在它们自己之间折行,而不是各自独占一行。 */
1729
+ .dsm-trae-pool-probe-cell{flex-direction:row;flex-wrap:wrap;align-items:baseline;gap:2px 8px}
1730
+ /* THE ACCOUNT CELL'S OWN GRID, collapsed the same way.
1731
+ *
1732
+ * 宽的时候它是个三行网格(名字 / 标签 / 探针时间),第一列固定给勾选框。
1733
+ * 在窄列里这三行没有理由继续各占一行——它们本来就是一句话
1734
+ * (「LaoDing · 已在池中 · 测试于 11/03, 06:35」),横排后名字与这两条短事实
1735
+ * 共享一行,只有真的放不下时才折行。勾选框仍然排在最前,并保持垂直居中对齐
1736
+ * 到第一行基线(align-self:baseline 会让它沉到行底)。 */
1737
+ .dsm-trae-pool-account{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 8px}
1738
+ .dsm-trae-pool-account-name,.dsm-trae-pool-account-tags,.dsm-trae-pool-account-note{grid-column:auto;grid-row:auto}
1739
+ .dsm-trae-pool-check{align-self:center}
1740
+ }
1648
1741
  .dsm-trae-pool-dirty{padding:0 6px;border-radius:999px;background:var(--dsw-alias-state-warn-tertiary,#fef3c7);color:var(--dsw-alias-state-warn-primary,#b45309);font-size:10px;line-height:16px}
1649
1742
  .dsm-trae-pool-save-error{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary,#d92d20)}
1650
1743
  .dsm-trae-pool-target{display:flex;flex-direction:column;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
@@ -1683,6 +1776,24 @@ window.__ModuleLoader__.load({
1683
1776
  .dsm-trae-page-title{margin:0;font-size:20px;font-weight:600;line-height:1.4}
1684
1777
  .dsm-trae-page-desc{margin:0;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#999)}
1685
1778
  .dsm-trae-page-body .dsm-trae-usage{padding:20px 20px 8px;border:1px solid var(--dsw-alias-border-l2,#36373b);background:var(--dsw-alias-bg-layer-3,#202126);border-radius:12px}
1779
+ /* The settings.section page. The shell already owns the panel, the nav row and
1780
+ * the scrolling .options column (which brings its own 24px side padding), so
1781
+ * this surface adds NO frame of its own: no second border, no second card, and
1782
+ * deliberately no negative margins to "reclaim" the shell's padding. It is a
1783
+ * plain column so the sections below stack the same way they do in the card. */
1784
+ .dsm-trae-section{display:flex;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
1785
+ /* 页头两行:标题一行,简介一行(learned from workbuddy 5.19.0)。
1786
+ *
1787
+ * 原来是 18px/600 的大标题加一行 13px 说明,在 564px 的列里要占 70px。压到
1788
+ * 15px/22px 的标题加 12px/18px 的说明后是 60px,省下的 10px 直接给了账号池——
1789
+ * 而面板本身已有一层标题栏,「设置」已经写在最上面,这里再喊一遍大标题是本插件
1790
+ * 自己的第二层标题,不是外壳的。
1791
+ *
1792
+ * 用 grid 而不是两个 flex 子项:标题和说明各自成行,中间不留 flex gap,行距由
1793
+ * row-gap:2px 精确控制(flex 的 gap 会同时作用在两者之间,量出来是 6px)。 */
1794
+ .dsm-trae-section-head{display:grid;grid-template-columns:minmax(0,1fr);row-gap:2px;align-items:start}
1795
+ .dsm-trae-section-title{margin:0;font-size:15px;font-weight:600;line-height:22px}
1796
+ .dsm-trae-section-desc{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
1686
1797
  .dsm-trae-panel-icon{display:block;border-radius:4px;flex:none}
1687
1798
  `;
1688
1799
  /**
@@ -4538,14 +4649,17 @@ window.__ModuleLoader__.load({
4538
4649
  function TraeUsageCard({ t, settingsScope, view, surface, backToConversation }) {
4539
4650
  if (t === void 0) throw new Error("Trae usage card requires its translation function");
4540
4651
  /**
4541
- * A PAGE surface (the optional sidebar row's `main` panel) has no card chrome
4542
- * to collapse: it is always expanded, and the collapse button is not rendered,
4543
- * so `setOpen` is simply unused there. Keeping ONE `open` value lets every
4544
- * polling effect below stay untouched.
4652
+ * The two FULL-BODY surfaces (the sidebar row's `main` panel and the
4653
+ * `settings.section` page) have no card chrome to collapse: they are always
4654
+ * expanded, and the collapse button is not rendered, so `setOpen` is simply
4655
+ * unused there. Keeping ONE `open` value lets every polling effect below stay
4656
+ * untouched.
4545
4657
  */
4546
4658
  const pageSurface = surface === "page";
4659
+ const sectionSurface = surface === "section";
4660
+ const fullSurface = pageSurface || sectionSurface;
4547
4661
  const [cardOpen, setCardOpen] = (0, react.useState)(view === "page");
4548
- const open = pageSurface || cardOpen;
4662
+ const open = fullSurface || cardOpen;
4549
4663
  /** The region whose tab is on screen; each tab is its own provider stack. */
4550
4664
  const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
4551
4665
  /**
@@ -6768,6 +6882,30 @@ window.__ModuleLoader__.load({
6768
6882
  })
6769
6883
  ]
6770
6884
  }) : null;
6885
+ /**
6886
+ * The section head, drawn only by the `settings.section` page.
6887
+ *
6888
+ * Deliberately NOT {@link pageHead} minus a button: the settings shell already
6889
+ * paints the nav row (which carries the icon registered into
6890
+ * `settings.section.icon`), the close control and the scroll column, so a
6891
+ * second 40px brand mark and a second title here would say the same thing
6892
+ * twice in one dialog. What the shell does NOT supply is a statement of what
6893
+ * the page is for, which is this heading plus the one-line description.
6894
+ *
6895
+ * No back control: the shell's close button is the way out of a modal, and a
6896
+ * "back to conversation" in here would either duplicate it or — worse — move
6897
+ * the centre column behind an open dialog.
6898
+ */
6899
+ const sectionHead = sectionSurface ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6900
+ className: "dsm-trae-section-head",
6901
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
6902
+ className: "dsm-trae-section-title",
6903
+ children: title
6904
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6905
+ className: "dsm-trae-section-desc",
6906
+ children: t("row.desc")
6907
+ })]
6908
+ }) : null;
6771
6909
  if (pageSurface) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6772
6910
  className: "dsm-trae-page",
6773
6911
  children: [
@@ -6782,6 +6920,17 @@ window.__ModuleLoader__.load({
6782
6920
  })
6783
6921
  ]
6784
6922
  });
6923
+ if (sectionSurface) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6924
+ className: "dsm-trae-section",
6925
+ children: [
6926
+ sectionHead,
6927
+ cardBody,
6928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BusyOverlay, {
6929
+ visible: saving || savingSettings,
6930
+ message: t("row.veilSaving")
6931
+ })
6932
+ ]
6933
+ });
6785
6934
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
6786
6935
  className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
6787
6936
  children: [
@@ -6961,12 +7110,15 @@ window.__ModuleLoader__.load({
6961
7110
  * per account in the region and that account's general balance, where a row
6962
7111
  * click switches to it.
6963
7112
  *
6964
- * The panel's header carries a SETTINGS button, not a refresh button: the
6965
- * question a reader actually has at this panel is "where do I change this?", and
6966
- * the answer used to require closing the popover, finding the Plugins row and
6967
- * clicking the bundle. Refreshing is not lost — the readout polls every five
6968
- * minutes and every account switch re-reads — but it no longer needs a button of
6969
- * its own. One press now lands on this plugin's own page (`./settings-route.ts`).
7113
+ * The panel's header carries the account-pool test batch, labelled 「刷新」, and
7114
+ * a SETTINGS pill that opens this plugin's own page in the wide Settings dialog
7115
+ * (`./settings-route.ts`).
7116
+ *
7117
+ * The pill was briefly removed, on the belief that no host seam could open the
7118
+ * dialog on a given section. That belief was wrong — the settings shell
7119
+ * publishes its state as the `sidebar.settings` store seat, whose `openSection(id)`
7120
+ * is exactly that seam — so the pill is back and now lands on this plugin's own
7121
+ * `settings.section` page. The route module records the finding.
6970
7122
  *
6971
7123
  * Two deliberate choices in that panel:
6972
7124
  *
@@ -7135,9 +7287,8 @@ window.__ModuleLoader__.load({
7135
7287
  * pool at once is how the upstream's volume limit gets tripped), so it can run
7136
7288
  * for a minute while a credits read would be merely slow.
7137
7289
  *
7138
- * `testFailed` is its own line for the same reason `settingsFailed` is: three
7139
- * different failures rendered into one slot would take turns overwriting each
7140
- * other. `testNote` carries the last outcome summary, which is what makes the
7290
+ * `testFailed` is its own line so the two failures cannot take turns
7291
+ * overwriting each other in one slot. `testNote` carries the last outcome summary, which is what makes the
7141
7292
  * press feel answered even when the table's marks are subtle.
7142
7293
  */
7143
7294
  const [testing, setTesting] = (0, react.useState)(false);
@@ -7332,8 +7483,8 @@ window.__ModuleLoader__.load({
7332
7483
  * The header's settings control: open this plugin's page, then close.
7333
7484
  *
7334
7485
  * CLOSING IS PART OF THE JOB. The panel is portaled to `document.body` and
7335
- * floats over the composer; navigating the centre column does not remove it,
7336
- * so leaving it open would park a credits table on top of the page the user
7486
+ * floats over the composer; opening the Settings dialog does not remove it, so
7487
+ * leaving it open would park a credits table on top of the dialog the user
7337
7488
  * just asked for. It is not reopened on return — a popover that comes back by
7338
7489
  * itself after navigation is a popover the user cannot get rid of.
7339
7490
  *
@@ -7751,6 +7902,123 @@ window.__ModuleLoader__.load({
7751
7902
  });
7752
7903
  }
7753
7904
  //#endregion
7905
+ //#region src/client/settings-route.ts
7906
+ /**
7907
+ * The route from the composer credit readout to THIS plugin's settings page.
7908
+ *
7909
+ * ## The finding this module encodes (m02517)
7910
+ *
7911
+ * The 4.9.0 line deleted the panel's settings pill on the stated reasoning that
7912
+ * "the host exposes no client service that opens the Settings dialog on a given
7913
+ * section, so the pill had nowhere to go". That reasoning was WRONG, and the
7914
+ * owner said so: clicking through the sidebar DOES open the wide settings on a
7915
+ * chosen page. The route exists; it was simply not the one the old module used.
7916
+ *
7917
+ * The old module (`openBundle` → `layout.selectPanel('plugins')`) targeted the
7918
+ * **Plugins** page, which stopped holding this plugin's configuration the moment
7919
+ * it moved to its own `settings.section` page. So the previous implementation is
7920
+ * not restorable, and a replacement was needed — this is it.
7921
+ *
7922
+ * ## The route: the settings shell's own store seat
7923
+ *
7924
+ * `@deepseek-ai/dsh-client-ui-settings-general` registers the `sidebar.settings`
7925
+ * slot with a **store seat**, and that seat is the shell's public state handle:
7926
+ *
7927
+ * const shellHandle = createSettingsShellStore()
7928
+ * const shellInstance = shellHandle.create()
7929
+ * const shellStore = { ...shellHandle, create: () => shellInstance }
7930
+ * ctx.slots.register({ name: 'sidebar.settings', store: shellStore, … })
7931
+ *
7932
+ * `createSettingsShellStore().actions` contains exactly the four shell
7933
+ * mutations, and `openSection(id)` is the one that answers this button: it sets
7934
+ * `activeId = id` AND `open = true`, i.e. "open the Settings dialog on the
7935
+ * named section" in a single call. `id` is the same string this plugin already
7936
+ * registers the page under (`TRAE_SETTINGS_ID`), so no translation is needed.
7937
+ *
7938
+ * Note the `create: () => shellInstance` override. A bare handle would mint a
7939
+ * FRESH instance per `create()` call — a private copy whose `openSection` moves
7940
+ * a dialog nobody renders. The shell pins the seat to its one live instance
7941
+ * precisely so a reader of the seat gets the dialog the user actually sees. This
7942
+ * module is that reader.
7943
+ *
7944
+ * ## Why reading the seat is legitimate, and not a private reach-through
7945
+ *
7946
+ * - `StoredEntry.store` is a documented public field: "Declared store seat
7947
+ * (instance resolution and lifecycle live with the host machinery)".
7948
+ * - `slots.entries(key)` is a documented public service method, and reading a
7949
+ * seat through it is the pattern the harness's OWN client tests use for this
7950
+ * exact store, e.g. `ui-shortcuts/tests/commands.client.spec.ts`:
7951
+ * `ctx.slots.entries('sidebar.settings')[0].store.create()`.
7952
+ * - No other id-targeted seam exists. `settings.launcher` hands `openSettings`
7953
+ * with no id, `settings.section` receives only `close`, and `openSection` is
7954
+ * otherwise given only to `settings.onboarding` registrants — reaching it
7955
+ * from there would mean registering a fake first-run step, which is far more
7956
+ * intrusive than reading the seat the shell deliberately published.
7957
+ *
7958
+ * ## Why the probe is at PRESS time
7959
+ *
7960
+ * The seat belongs to ANOTHER bundle whose apply order is not ours to assume.
7961
+ * Probing once during `apply()` would capture `undefined` on a page whose
7962
+ * settings bundle applied later, leaving the button dead for the whole session
7963
+ * with nothing to re-evaluate it. `entries()` answers `[]` for a slot that is
7964
+ * not declared yet, so probing per press is free of state and always current.
7965
+ *
7966
+ * ## Why `false` rather than an exception
7967
+ *
7968
+ * The caller is a popover button. There is no supported host line lacking the
7969
+ * shell, but "cannot" must not become a thrown error inside a click handler on
7970
+ * a user's machine; it becomes a `false` the caller reports, and a button that
7971
+ * stays honest about it.
7972
+ */
7973
+ /**
7974
+ * The `settings.section` id this plugin registers its page under.
7975
+ *
7976
+ * One spelling, in one place: the page registration, the nav row and
7977
+ * `openSection(id)` must agree, or the dialog opens on the shell's fallback
7978
+ * (first) section instead of this plugin's — silently, since an unknown id is
7979
+ * not an error to the store.
7980
+ */
7981
+ const TRAE_SETTINGS_ID = "dsh-connect-trae";
7982
+ /** The shipped slot whose store seat is the settings shell's state handle. */
7983
+ const SETTINGS_SLOT = "sidebar.settings";
7984
+ /**
7985
+ * Open this plugin's settings page — the wide Settings dialog, on this
7986
+ * plugin's section.
7987
+ *
7988
+ * @param get - the entry's soft service probe (`ctx.get`), which returns
7989
+ * `undefined` for an absent service instead of throwing. Called HERE, on every
7990
+ * invocation, so a service that appears after `apply` is still found.
7991
+ * @returns whether a route was taken. `false` means the caller's control should
7992
+ * report that this host has no way there — see the module note.
7993
+ */
7994
+ function openPluginSettingsPage(get) {
7995
+ const slots = get("slots");
7996
+ if (slots === void 0 || typeof slots.entries !== "function") return false;
7997
+ let seat;
7998
+ try {
7999
+ seat = slots.entries(SETTINGS_SLOT)[0]?.store;
8000
+ } catch {
8001
+ return false;
8002
+ }
8003
+ if (seat === null || typeof seat !== "object") return false;
8004
+ const handle = seat;
8005
+ if (typeof handle.create !== "function") return false;
8006
+ try {
8007
+ const instance = handle.create();
8008
+ const openSection = instance?.actions?.openSection;
8009
+ if (typeof openSection === "function") {
8010
+ openSection(TRAE_SETTINGS_ID);
8011
+ return true;
8012
+ }
8013
+ const open = instance?.actions?.open;
8014
+ if (typeof open === "function") {
8015
+ open();
8016
+ return true;
8017
+ }
8018
+ } catch {}
8019
+ return false;
8020
+ }
8021
+ //#endregion
7754
8022
  //#region src/client/locales.ts
7755
8023
  /**
7756
8024
  * Plugin-card copy registered under the settings.trae locale namespace.
@@ -8611,8 +8879,9 @@ window.__ModuleLoader__.load({
8611
8879
  *
8612
8880
  * WHY THE HOST STILL OWNS THE NAME. The contract says to keep `model.name` as
8613
8881
  * the text and render the addition beside it, so this component returns the name
8614
- * first — it never replaces it — and it renders nothing extra for a provider
8615
- * that is not ours.
8882
+ * first — it never replaces it. For a provider that is not ours it reproduces
8883
+ * the host's own label: the catalog name and, since 0.2.2, that model's optional
8884
+ * `badge` (see {@link HostModelBadge}).
8616
8885
  *
8617
8886
  * @module dsh-connect-trae/client/TraeModelLabel
8618
8887
  */
@@ -8869,8 +9138,9 @@ window.__ModuleLoader__.load({
8869
9138
  *
8870
9139
  * WHAT WINNING/HOLDING COSTS. This cell is `single`, so the other registrants'
8871
9140
  * decorations on the rows this plugin claims are not composed. That is the
8872
- * documented, accepted trade-off, and it is why the component returns the plain
8873
- * catalog name for any provider that is not ours — the slot's own minimum.
9141
+ * documented, accepted trade-off, and it is why the component reproduces the
9142
+ * host's own presentation — catalog name plus that model's optional badge — for
9143
+ * any provider that is not ours.
8874
9144
  *
8875
9145
  * WHY A DISTINCT PRIORITY IS NOT ENOUGH ON ITS OWN. It removes the cross-plugin
8876
9146
  * collision, but issue #74's error was a SECOND registration of THIS plugin
@@ -8883,10 +9153,88 @@ window.__ModuleLoader__.load({
8883
9153
  * made the throw rarer and harder to read, not absent.
8884
9154
  */
8885
9155
  const MODEL_LABEL_PRIORITY = -.25;
9156
+ /** The tone colours, mapped onto the same theme tokens the host's Tag paints. */
9157
+ const HOST_BADGE_TONES = {
9158
+ neutral: {
9159
+ color: "var(--dsw-alias-label-secondary)",
9160
+ background: "var(--dsw-alias-bg-layer-2)",
9161
+ border: "var(--dsw-alias-border-l2)"
9162
+ },
9163
+ info: {
9164
+ color: "var(--dsw-alias-brand-primary)",
9165
+ background: "var(--dsw-alias-bg-layer-2)",
9166
+ border: "var(--dsw-alias-border-l2)"
9167
+ },
9168
+ success: {
9169
+ color: "var(--dsw-alias-state-success-primary)",
9170
+ background: "var(--dsw-alias-bg-layer-2)",
9171
+ border: "var(--dsw-alias-border-l2)"
9172
+ },
9173
+ warning: {
9174
+ color: "var(--dsw-alias-state-warn-secondary)",
9175
+ background: "var(--dsw-alias-bg-layer-2)",
9176
+ border: "var(--dsw-alias-border-l2)"
9177
+ }
9178
+ };
9179
+ /**
9180
+ * The one line a badge shows, in the reader's language.
9181
+ *
9182
+ * The host contract requires an English entry, so that is the last resort; the
9183
+ * active language and its base tag (`de-DE` → `de`) come first, matching what
9184
+ * the host's own resolver does. An empty string is treated as absent — a blank
9185
+ * chip would be a decoration that says nothing.
9186
+ */
9187
+ function resolveHostBadgeText(text) {
9188
+ if (typeof text === "string") return text.trim() === "" ? void 0 : text;
9189
+ const language = uiLanguage().toLowerCase();
9190
+ const base = language.split("-")[0] ?? "";
9191
+ const picked = text[language] ?? (base === "" ? void 0 : text[base]) ?? text.en;
9192
+ return picked === void 0 || picked.trim() === "" ? void 0 : picked;
9193
+ }
9194
+ /** One provider-authored selector badge, drawn where the host's own label draws it. */
9195
+ function HostBadge(props) {
9196
+ const tone = HOST_BADGE_TONES[props.badge.tone] ?? HOST_BADGE_TONES.neutral;
9197
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9198
+ className: "dsm-trae-model-label-host-badge",
9199
+ style: {
9200
+ flex: "none",
9201
+ borderRadius: "999px",
9202
+ padding: "0 6px",
9203
+ fontSize: "10px",
9204
+ fontWeight: 500,
9205
+ lineHeight: "15px",
9206
+ whiteSpace: "nowrap",
9207
+ border: `1px solid ${tone.border}`,
9208
+ color: tone.color,
9209
+ background: tone.background,
9210
+ maxWidth: "8em",
9211
+ overflow: "hidden",
9212
+ textOverflow: "ellipsis"
9213
+ },
9214
+ children: props.label
9215
+ });
9216
+ }
8886
9217
  function TraeModelLabel(props) {
8887
9218
  const { provider, model } = props;
8888
9219
  const panel = panelOfProvider(provider.id);
8889
- if (panel === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
9220
+ if (panel === void 0) {
9221
+ const badgeInfo = model.badge;
9222
+ const label = badgeInfo === void 0 ? void 0 : resolveHostBadgeText(badgeInfo.text);
9223
+ if (badgeInfo === void 0 || label === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
9224
+ const badge = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostBadge, {
9225
+ badge: badgeInfo,
9226
+ label
9227
+ });
9228
+ return props.placement === "trigger" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
9229
+ badge,
9230
+ " ",
9231
+ model.name
9232
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
9233
+ model.name,
9234
+ " ",
9235
+ badge
9236
+ ] });
9237
+ }
8890
9238
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraeModelLabelInner, {
8891
9239
  modelId: model.id,
8892
9240
  name: model.name,
@@ -8936,92 +9284,6 @@ window.__ModuleLoader__.load({
8936
9284
  });
8937
9285
  }
8938
9286
  //#endregion
8939
- //#region src/client/settings-route.ts
8940
- /**
8941
- * The route from the composer credit readout to THIS plugin's settings page.
8942
- *
8943
- * ## Why a module and not three lines inside `apply`
8944
- *
8945
- * The entry (`src/client/index.tsx`) imports browser-only DSH packages the Node
8946
- * test environment cannot load, so anything worth asserting on has to live
8947
- * outside it — the same seam `panel-switch.ts` provides for the sidebar opt-in.
8948
- * The spec then exercises the REAL decision instead of a mirror of it.
8949
- *
8950
- * ## Why two routes
8951
- *
8952
- * On this host line a plugin's settings are not a "page in Settings": the
8953
- * Plugins page owns them, and each bundle's page (`plugins.bundle.config` /
8954
- * `plugins.row.config`) IS the configuration surface — Settings keeps only the
8955
- * read-only built-in inventory. So "open the plugin settings" means "open the
8956
- * Plugins page on this bundle", and the supported way to ask for that is the
8957
- * `pluginNavigation` service the Plugins page provides: `openBundle(packageName)`
8958
- * selects that page without touching the Session.
8959
- *
8960
- * `pluginNavigation` is provided by `@deepseek-ai/dsh-client-ui-plugin-manager`,
8961
- * NOT by the shell. A profile can be composed without it, and plugin load order
8962
- * is not something a contributor may assume — so the probe happens at CALL time
8963
- * rather than at registration time. Probing once in `apply` would capture
8964
- * `undefined` on a page whose Plugins bundle simply applied after ours, and the
8965
- * button would be dead for the rest of the session with nothing to re-evaluate it.
8966
- *
8967
- * The fallback reaches the SAME page through the lower-level `layout` service.
8968
- * `selectPanel('plugins')` is the shipped main key of the Plugins page (the
8969
- * plugin-manager bundle registers `sidebar.panellist` id / `main` key
8970
- * `'plugins'`), so a host without `pluginNavigation` still lands the user where
8971
- * the settings are, one click from the row. It is a fallback and not the primary
8972
- * route deliberately: `'plugins'` is another package's private key, while
8973
- * `openBundle` is that package's published seam. `selectPanel` THROWS when the
8974
- * key is not registered (Plugins page disabled, trimmed profile), so the call is
8975
- * guarded rather than trusted.
8976
- *
8977
- * ## Why `false` rather than an exception
8978
- *
8979
- * The caller is a popover button. There is no supported host line lacking both
8980
- * routes, but "cannot" must not become a thrown error inside a click handler on
8981
- * a user's machine; it becomes a `false` the caller reports, and a button that
8982
- * stays honest about it.
8983
- */
8984
- /**
8985
- * The package name the Plugins page knows this plugin by.
8986
- *
8987
- * `openBundle` matches the inventory's `pkg.name` by EXACT equality, and that
8988
- * name is the profile's package name — the npm package name — not the Loader
8989
- * entry id (which on a live Desktop profile can be `include:dsh-connect-trae`).
8990
- * This is the same reason the card registers under every spelling the host has
8991
- * been observed to dispatch: the comparison is exact and the deployment picks
8992
- * the spelling. The inventory side of that comparison is the package name in
8993
- * every composition observed, so that is what `openBundle` is asked for.
8994
- *
8995
- * Deliberately ONE name, not a list: `openBundle` selects a target and an
8996
- * unknown target renders the LIST, so a second call with a spelling the
8997
- * inventory does not hold would undo the first one's navigation.
8998
- */
8999
- const TRAE_PACKAGE_NAME = "dsh-connect-trae";
9000
- /** The shipped `main` / `sidebar.panellist` key of the Plugins page. */
9001
- const PLUGINS_PANEL_ID = "plugins";
9002
- /**
9003
- * Open this plugin's settings page.
9004
- *
9005
- * @param get - the entry's soft service probe (`ctx.get`), which returns
9006
- * `undefined` for an absent service instead of throwing. Called HERE, on every
9007
- * invocation, so a service that appears after `apply` is still found.
9008
- * @returns whether a route was taken. `false` means the caller's control should
9009
- * report that this host has no way there — see the module note.
9010
- */
9011
- function openPluginSettingsPage(get) {
9012
- const navigation = get("pluginNavigation");
9013
- if (navigation !== void 0 && typeof navigation.openBundle === "function") try {
9014
- navigation.openBundle(TRAE_PACKAGE_NAME);
9015
- return true;
9016
- } catch {}
9017
- const layout = get("layout");
9018
- if (layout !== void 0 && typeof layout.selectPanel === "function") try {
9019
- layout.selectPanel(PLUGINS_PANEL_ID);
9020
- return true;
9021
- } catch {}
9022
- return false;
9023
- }
9024
- //#endregion
9025
9287
  //#region src/client/index.tsx
9026
9288
  /** Stable browser-plugin name. */
9027
9289
  const name = "dsh-connect-trae-client";
@@ -9128,11 +9390,11 @@ window.__ModuleLoader__.load({
9128
9390
  /**
9129
9391
  * The composer panel's route to this plugin's own settings page.
9130
9392
  *
9131
- * Wrapped in a closure, not resolved here: `pluginNavigation` belongs to the
9132
- * Plugins bundle and its apply order is not ours to assume, so probing once
9133
- * at `apply` could capture `undefined` permanently. The decision itself (and
9134
- * its `layout` fallback) lives in `settings-route.ts`, where the spec can
9135
- * exercise the real chain instead of a mirror.
9393
+ * Deliberately a CLOSURE over `softGet` rather than a resolved service: the
9394
+ * seam it uses (the settings shell's `sidebar.settings` store seat) belongs
9395
+ * to another bundle whose apply order is not ours to assume, so the probe
9396
+ * happens on every press and a shell that appears later is still found. See
9397
+ * `settings-route.ts` for the finding that established which seam this is.
9136
9398
  */
9137
9399
  const onOpenSettings = () => openPluginSettingsPage(softGet);
9138
9400
  const forms = softGet("configForms");
@@ -9204,19 +9466,90 @@ window.__ModuleLoader__.load({
9204
9466
  forms?.describe().subscribe?.(rebind);
9205
9467
  return scope;
9206
9468
  })();
9207
- const registerCard = (slotName, key) => {
9208
- registerSlotEntry(ctx, `${slotName}#${key}`, slotName, () => ctx.slots.register({
9209
- name: slotName,
9210
- key,
9211
- priority: 30,
9212
- inject: () => ({
9213
- t,
9214
- settingsScope
9215
- })
9216
- }, TraeUsageCard), `card slot "${slotName}"`);
9469
+ /**
9470
+ * The configuration card, now drawn as its own `settings.section` page.
9471
+ *
9472
+ * WHY THE SEAT MOVED. It used to register into the Plugins page's
9473
+ * `plugins.bundle.config` / `plugins.row.config` slots, which put the whole
9474
+ * configuration inside the plugin's market detail page, below its README.
9475
+ * That page's job is to describe and install a package; a page that both
9476
+ * advertises and configures makes the user scroll past marketing to reach a
9477
+ * checkbox. `settings.section` is the shell's seat for "this plugin owns a
9478
+ * settings page", addressed by the same id the nav row uses.
9479
+ *
9480
+ * WHY `id` AND `order: 30`. The id is this plugin's package name, and it is
9481
+ * the SAME string the nav glyph below registers under, because the shell
9482
+ * renders `settings.section.icon` filtered by `{ only: section.id }` — an
9483
+ * icon whose id does not match never appears and the shell's default glyph
9484
+ * silently stays. `order: 30` places this page after the shipped sections
9485
+ * (account -10, general 0, models 10, plugins 15, agent-presets 20) and
9486
+ * alongside the other third-party pages, without colliding with a built-in.
9487
+ *
9488
+ * WHY NO CAPABILITY PROBE. `slots.inject()` already IS the probe: it runs
9489
+ * the callback now when the slot is declared and subscribes otherwise,
9490
+ * doing nothing at all on a host that never declares `settings.section`.
9491
+ * Wrapping it in a `typeof` test on some accessor is how a page ends up
9492
+ * never registering anywhere, silently — the failure mode this plugin's
9493
+ * sibling shipped twice. Let the host answer.
9494
+ *
9495
+ * The page and its glyph are registered from ONE `inject` callback and both
9496
+ * disposers are returned as a pair: the glyph is only meaningful while the
9497
+ * page's id exists, so the pairing is structural rather than a convention,
9498
+ * and Cordis disposes both with this fiber.
9499
+ */
9500
+ const traeSettingsId = TRAE_SETTINGS_ID;
9501
+ const registerSettingsPage = () => {
9502
+ registerSlotEntry(ctx, "settings.section#dsh-connect-trae", "settings.section", () => {
9503
+ const disposers = [];
9504
+ try {
9505
+ disposers.push(ctx.slots.register({
9506
+ name: "settings.section",
9507
+ id: traeSettingsId,
9508
+ order: 30,
9509
+ label: () => t("nav.panel"),
9510
+ locale: namespace,
9511
+ inject: () => ({
9512
+ t,
9513
+ settingsScope,
9514
+ surface: "section"
9515
+ })
9516
+ }, TraeUsageCard));
9517
+ /**
9518
+ * The nav glyph, addressed by the SAME id as the page.
9519
+ *
9520
+ * The host renders this cell with `{ size, active }` inside its own
9521
+ * button and label, so only the picture belongs here. Same shape as
9522
+ * the sidebar row's glyph (a brand-mark `<img>`), because it is the
9523
+ * same identity in a second place.
9524
+ *
9525
+ * Its own `inject`: `settings.section.icon` is a SEPARATE slot that a
9526
+ * host line may not declare, and the page must still appear when only
9527
+ * `settings.section` exists. A missing glyph is the shell's default
9528
+ * gear — a degraded look, not a missing page.
9529
+ */
9530
+ try {
9531
+ disposers.push(ctx.slots.inject("settings.section.icon", () => ctx.slots.register({
9532
+ name: "settings.section.icon",
9533
+ id: traeSettingsId,
9534
+ locale: namespace
9535
+ }, SettingsSectionIcon)));
9536
+ } catch (error) {
9537
+ console.error("[dsh-connect-trae] settings nav icon failed to register (page unaffected):", error);
9538
+ }
9539
+ } catch (error) {
9540
+ for (const dispose of disposers) try {
9541
+ dispose();
9542
+ } catch {}
9543
+ throw error;
9544
+ }
9545
+ return () => {
9546
+ for (const dispose of disposers) try {
9547
+ dispose();
9548
+ } catch {}
9549
+ };
9550
+ }, "settings section page");
9217
9551
  };
9218
- registerCard("plugins.bundle.config", "dsh-connect-trae");
9219
- registerCard("plugins.row.config", "dsh-connect-trae#dsh-connect-trae");
9552
+ registerSettingsPage();
9220
9553
  /**
9221
9554
  * Credit readout in the composer row (`conversation.input.right`, a
9222
9555
  * session-scope list slot rendered immediately before the model picker).
@@ -9302,6 +9635,35 @@ window.__ModuleLoader__.load({
9302
9635
  });
9303
9636
  }
9304
9637
  /**
9638
+ * The nav glyph for the `settings.section` page.
9639
+ *
9640
+ * A separate component from {@link TraePanelIcon} even though it draws the
9641
+ * same picture, because the two slots pass DIFFERENT owner props and a shared
9642
+ * component would have to paper over that: `sidebar.panellist` sends
9643
+ * `{ size, active }` on a row the shell paints, `settings.section.icon` sends
9644
+ * `{ size, active }` inside the shell's own nav button, and the second one
9645
+ * also has to survive `size` being absent (the shell always sends it, but a
9646
+ * host line that renames the prop must degrade to the default rather than
9647
+ * divide by `undefined`).
9648
+ *
9649
+ * `active` is applied as a slight opacity step instead of a colour: the
9650
+ * brand PNG carries its own colours, and recolouring someone else's mark to
9651
+ * show selection would misrepresent the product. The shell already paints
9652
+ * the selected row.
9653
+ */
9654
+ function SettingsSectionIcon(props) {
9655
+ const edge = typeof props.size === "number" && props.size > 0 ? props.size : 16;
9656
+ return (0, react.createElement)("img", {
9657
+ className: "dsm-trae-section-icon",
9658
+ src: TRAE_PLUGIN_ICON,
9659
+ alt: "",
9660
+ width: edge,
9661
+ height: edge,
9662
+ style: { opacity: props.active === false ? .75 : 1 },
9663
+ "aria-hidden": "true"
9664
+ });
9665
+ }
9666
+ /**
9305
9667
  * Back to the conversation, for the page's header.
9306
9668
  *
9307
9669
  * Soft-probed: `layout` is a host service this entry deliberately does not
package/lib/index.js CHANGED
@@ -1034,10 +1034,33 @@ const INERT_AUTH = {
1034
1034
  }
1035
1035
  }
1036
1036
  };
1037
+ /**
1038
+ * The image-request policy this hand-built profile hands to `dsh-llm-pi-ai`.
1039
+ *
1040
+ * WHY THIS OBJECT IS SPELLED OUT. DSH's own config resolution (`resolveProfiles`
1041
+ * in `dsh-llm-pi-ai`) defaults every route-level image bound. This plugin never
1042
+ * runs that path: it builds `ResolvedPiAiProviderProfile` by hand from a live
1043
+ * Trae catalog, so every field the host expects has to be named here or the
1044
+ * bound silently does not apply.
1045
+ *
1046
+ * `maxRequestImages` / `imageOffloadCountQuantum` joined that contract in DSH
1047
+ * 0.2.2-rc.1 (the required fields on `ResolvedPiAiProviderProfile`). The count
1048
+ * bound is what turns a request carrying more images than a route accepts into
1049
+ * the host's `IMAGE_OFFLOAD_REQUIRED` flow instead of an upstream rejection, and
1050
+ * the quantum is how many of the oldest occurrences that flow offloads at once.
1051
+ * Without them the host treats the route as count-unbounded — the pre-0.2.2
1052
+ * behaviour, which is why 4.8.0 kept working — but the type contract no longer
1053
+ * compiles, so they are declared here with the host's own defaults (50 / 10).
1054
+ *
1055
+ * `imageOffloadCountQuantum` must be a positive integer no greater than
1056
+ * `maxRequestImages`; 10 ≤ 50 keeps the host's default ratio.
1057
+ */
1037
1058
  const REQUEST_IMAGE_BUDGETS = {
1038
1059
  maxRequestImageBytes: 20971520,
1039
1060
  requestImagePixelBudget: 4194304,
1040
- requestImageMaxBytes: 1048576
1061
+ requestImageMaxBytes: 1048576,
1062
+ maxRequestImages: 50,
1063
+ imageOffloadCountQuantum: 10
1041
1064
  };
1042
1065
  function toPiModel(info, baseUrl, providerId, withRate = true) {
1043
1066
  return {
@@ -12190,6 +12213,64 @@ function apply(ctx, config) {
12190
12213
  delete registration.cn.directory;
12191
12214
  };
12192
12215
  /**
12216
+ * The last catalog announced to the host, per region.
12217
+ *
12218
+ * DSH 0.2.2-rc.1 added `llm/models-updated(provider)`: "a provider's advertised
12219
+ * models or display metadata changed; consumers reload `listModels()`". The
12220
+ * model picker listens for it next to the payload-free `llm/adapters-updated`
12221
+ * — and the latter only fires when a route's TOPOLOGY changes, which is not
12222
+ * the same event as "the models behind an unchanged route changed". The live
12223
+ * directory this plugin refreshes from upstream is exactly the second kind:
12224
+ * the same provider, a different roster.
12225
+ *
12226
+ * The map is the dedupe ledger. Every settings write rebuilds the catalog
12227
+ * object even when the served rows are identical, so an unconditional emit
12228
+ * would make every picker (and every other consumer) re-read the catalog on
12229
+ * each card keystroke.
12230
+ */
12231
+ const announcedCatalog = /* @__PURE__ */ new Map();
12232
+ /**
12233
+ * What a picker can tell apart about one catalog.
12234
+ *
12235
+ * Must cover every field the host exposes for a model — id, display name,
12236
+ * context window, output cap, input modalities, reasoning tiers, and the
12237
+ * credit multiplier the display name is derived from — or a refresh that
12238
+ * changed only one of them would stay stale in the picker. Order and
12239
+ * omission are both meaningful, so `null` marks an absent key instead of it
12240
+ * collapsing into the empty string.
12241
+ */
12242
+ function catalogSignature(models) {
12243
+ return models.map((model) => JSON.stringify([
12244
+ model.id,
12245
+ model.name,
12246
+ model.contextWindow ?? null,
12247
+ model.maxTokens ?? null,
12248
+ model.input ?? null,
12249
+ model.reasoningEfforts ?? null,
12250
+ model.creditMultiplier ?? null
12251
+ ])).join("\n");
12252
+ }
12253
+ /**
12254
+ * Tell the host that one region's catalog changed, once per change.
12255
+ *
12256
+ * Called from `invalidateAdapter` — the single seam every `catalog.set` path
12257
+ * already ends with (a settings write through `applySelection`, and the
12258
+ * startup discovery) — so a new catalog-set path cannot forget to announce.
12259
+ * A switched-off region advertises nothing: its route is withdrawn and
12260
+ * `syncRegionRegistration` announces that topology change itself, so this
12261
+ * only records the signature.
12262
+ *
12263
+ * Emitting on a host older than 0.2.2 is deliberately harmless: cordis
12264
+ * dispatches events by name and simply has no listener for this one.
12265
+ */
12266
+ function announceCatalog(panel) {
12267
+ const signature = catalogSignature(stacks[panel].catalog.current());
12268
+ if (announcedCatalog.get(panel) === signature) return;
12269
+ announcedCatalog.set(panel, signature);
12270
+ if (!regionEnabled(current(), panel)) return;
12271
+ ctx.emit("llm/models-updated", providerOfPanel(panel));
12272
+ }
12273
+ /**
12193
12274
  * Publish each region's on/off state to the harness (issue #11). Both swaps
12194
12275
  * are single synchronous sections, so no request can observe a half-applied
12195
12276
  * state, and `replace` announces itself through `llm/adapters-updated`, which
@@ -12355,6 +12436,7 @@ function apply(ctx, config) {
12355
12436
  });
12356
12437
  stack.invalidateAdapter = () => {
12357
12438
  trae.invalidate();
12439
+ announceCatalog(panel);
12358
12440
  };
12359
12441
  registration[panel].adapters.push({
12360
12442
  provider,
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": "4.8.0",
5
+ "version": "4.9.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -100,22 +100,22 @@
100
100
  }
101
101
  },
102
102
  "devDependencies": {
103
- "@deepseek-ai/cordis": "^4.0.5",
104
- "@deepseek-ai/dsh-atomic-write": "0.2.1-rc.5",
105
- "@deepseek-ai/dsh-attachment": "0.2.1-rc.5",
106
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.5",
107
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
108
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
109
- "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.5",
110
- "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.1-rc.5",
111
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
112
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.5",
113
- "@deepseek-ai/dsh-host-webserver": "0.2.1-rc.5",
114
- "@deepseek-ai/dsh-llm": "0.2.1-rc.5",
115
- "@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.5",
116
- "@deepseek-ai/dsh-settings": "0.2.1-rc.5",
117
- "@deepseek-ai/schemastery": "3.18.5",
118
- "@earendil-works/pi-ai": "1.1.0",
103
+ "@deepseek-ai/cordis": "^4.1.0",
104
+ "@deepseek-ai/dsh-atomic-write": "0.2.2-rc.1",
105
+ "@deepseek-ai/dsh-attachment": "0.2.2-rc.1",
106
+ "@deepseek-ai/dsh-client-locale": "0.2.2-rc.1",
107
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.2-rc.1",
108
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.2-rc.1",
109
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.2-rc.1",
110
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.2-rc.1",
111
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.2-rc.1",
112
+ "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
113
+ "@deepseek-ai/dsh-host-webserver": "0.2.2-rc.1",
114
+ "@deepseek-ai/dsh-llm": "0.2.2-rc.1",
115
+ "@deepseek-ai/dsh-llm-pi-ai": "0.2.2-rc.1",
116
+ "@deepseek-ai/dsh-settings": "0.2.2-rc.1",
117
+ "@deepseek-ai/schemastery": "3.19.0",
118
+ "@earendil-works/pi-ai": "1.3.0",
119
119
  "@testing-library/dom": "^10.4.2",
120
120
  "@testing-library/react": "^16.3.3",
121
121
  "@types/node": "^22.20.0",