dsh-connect-trae 4.15.0 → 4.16.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,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.16.0 (2026-11-08)
4
+
5
+ ### 模型标签槽:按宿主的声明注册,为 `keyed` 做好准备
6
+
7
+ `conversation.input.model.label` 的 kind 写在宿主包里(`ui-model-selection`)。
8
+ 在它改口之前是 `single`:只有一格,三家连接器只能按 `-101` / `-102` / `-105`
9
+ 互相压价。目标形态是 `keyed`——每个 `options.key` 自成一格,宿主按 `provider.id`
10
+ 派发 `{ entryKey }`,没人贡献的 provider 落回宿主自己的 fallback。
11
+
12
+ **本版做的是一件插件侧唯一该做的事:注册几条跟着宿主声明的 kind 走。**
13
+
14
+ - 新增 `src/client/model-label-seat.ts`:运行时读 `ctx.slots.spec(key)?.kind`,
15
+ `keyed` 就为 `trae` / `trae-enterprise` / `trae-global` 各注册一格,其余情况
16
+ (含读不到)保持历史上的单条。**一个构建同时服务两条宿主线**,宿主改口那天
17
+ 不需要重新发版。
18
+ - `registerSlotEntry` 现在接受「多个 disposer」,并把它们合成一个按**反序**释放的
19
+ 组合 disposer:注册是一条命,拆开释放会留下「某几格还占着、兄弟格已消失」的半死状态。
20
+
21
+ **为什么不能只改宿主。** 两个方向都会**静默**失败,而本仓库的注册路径把一个
22
+ try/catch 包在回调里(issue #74 的修复),于是它们都变成一行 `console.error`:
23
+
24
+ | 情况 | 结果 |
25
+ | --- | --- |
26
+ | keyed 的两条打到 `single` 宿主 | 第二条抛 `already has a registration`,第一条已落地 → 半注册 |
27
+ | 无 key 的一条打到 `keyed` 宿主 | 抛 `keyed slot … requires options.key` → 标签消失 |
28
+
29
+ `keyed` **强制要求 `key`**,这是上游提案里没写的第一道陷阱:**此前已发布**的本族每个
30
+ 版本都注册一条无 key 的条目,只改宿主声明会让它们一起失效(本版之后的新构建会跟着
31
+ 声明走,所以受影响的是「已经装在用户机器上」的那些版本)。第二道陷阱见下一节。
32
+
33
+ **改声明不够:宿主还必须传 `entryKey`。** 复核实机产物
34
+ (`ui-model-selection@0.2.2-rc.1`)发现,渲染该槽只有一个调用点,第三个参数只有
35
+ `fallback`、**没有 `entryKey`**;而 keyed 分支是
36
+ `find(e => e.options.key === opts?.entryKey)`——`undefined` 匹配不到任何条目,于是
37
+ **每一行**都落到 fallback。本插件的注册会成功、回调也跑、无异常无日志,只是组件再也
38
+ 不被调用。上游的最小改动是**两处**:声明 + 调用点补 `{ entryKey: provider.id }`。
39
+
40
+ `MODEL_LABEL_PRIORITY = -102` 不改。keyed 下它不再决定可见性,只剩「同一格内谁先」;
41
+ 三家之间不再有压制关系——停战靠的是换槽的基数,不是再压一次价。
42
+
43
+ 外来行分支保留:keyed 下运行时走不到,但一个构建要同时服务两种宿主,而 `single`
44
+ 宿主上它就是整行文字。它同时也是最便宜的哨兵——自己的行若走进那一支,说明 `key` 写错了。
45
+
3
46
  ## 4.15.0 (2026-11-08)
4
47
 
5
48
  ### 新增:模型选择器里的胶囊,改成「谁都能显示」
package/lib/client.js CHANGED
@@ -9554,6 +9554,17 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9554
9554
  *
9555
9555
  * @module dsh-connect-trae/client/TraeModelLabel
9556
9556
  */
9557
+ /**
9558
+ * Every provider id this plugin owns, in panel order.
9559
+ *
9560
+ * Exported because on a `keyed` host the label seat is one registration PER
9561
+ * provider id — the registration side needs the same list the component uses to
9562
+ * decide "is this row mine", and a second literal would drift from
9563
+ * `providerOfPanel`. Derived rather than typed out: `trae-enterprise` is the id
9564
+ * most easily forgotten, and forgetting it means that panel's rows silently fall
9565
+ * through to the host's plain fallback.
9566
+ */
9567
+ const TRAE_MODEL_LABEL_PROVIDERS = TRAE_PANELS.map(providerOfPanel);
9557
9568
  /** How long a fetched snapshot is trusted, in milliseconds. */
9558
9569
  const CACHE_MS = 6e4;
9559
9570
  /** How often a mounted label re-reads the clock, in milliseconds. */
@@ -9825,6 +9836,17 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9825
9836
  * host's own presentation — catalog name plus that model's optional badge — for
9826
9837
  * any provider that is not ours.
9827
9838
  *
9839
+ * WHAT CHANGES WHEN THE HOST KEYS THE SLOT — and why the number stays. On a host
9840
+ * that declares this slot `keyed`, the seat is registered once per provider id
9841
+ * (`./model-label-seat.ts`), each `key` is its own cell, and priority stops
9842
+ * deciding WHO IS VISIBLE: qoder's `-101` and workbuddy's `-105` no longer share
9843
+ * a cell with this `-102`, so none of the three can shadow another and the
9844
+ * armistice below stops being a race. Priority keeps exactly one meaning inside
9845
+ * a cell — the tie-breaker between two registrations claiming the SAME provider
9846
+ * id, where an equal value throws and the lowest renders. The value is therefore
9847
+ * kept as this plugin's rank for its own rows, not as a bid in an auction that
9848
+ * no longer happens.
9849
+ *
9828
9850
  * WHY A DISTINCT PRIORITY IS NOT ENOUGH ON ITS OWN. It removes the cross-plugin
9829
9851
  * collision, but issue #74's error was a SECOND registration of THIS plugin
9830
9852
  * against its own first one (the report's stack shows
@@ -9833,7 +9855,9 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9833
9855
  * slot's declaration lifetime collapses and re-declares, so a callback that
9834
9856
  * registers unconditionally double-registers. `index.tsx` therefore also
9835
9857
  * disposes its previous entry before every attempt. Priority alone would have
9836
- * made the throw rarer and harder to read, not absent.
9858
+ * made the throw rarer and harder to read, not absent. (On a `keyed` host the
9859
+ * same rule applies per key: re-registering one provider's seat twice at one
9860
+ * priority throws just as loudly.)
9837
9861
  */
9838
9862
  const MODEL_LABEL_PRIORITY = -102;
9839
9863
  /** The four tones the host understands, as a lookup for narrowing. */
@@ -10030,6 +10054,114 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
10030
10054
  });
10031
10055
  }
10032
10056
  //#endregion
10057
+ //#region src/client/model-label-seat.ts
10058
+ /**
10059
+ * Read the declared kind of a slot, or `undefined` when it cannot be read.
10060
+ *
10061
+ * `spec()` is the documented read-only accessor (`SlotRegistry.spec`), present
10062
+ * on 0.1.7 and on 0.2.2. It is also the only way to learn the kind: the
10063
+ * declaration lives in the host package, and a plugin's own `SlotMap`
10064
+ * augmentation says what this plugin HOPES the slot is, not what the running
10065
+ * host declared.
10066
+ *
10067
+ * Anything unexpected — no `spec` method on an older line, a throw from a
10068
+ * service that is not ready, a kind this build does not know — returns
10069
+ * `undefined`, and the caller falls back to the single-cell shape. That is the
10070
+ * conservative direction: it is what every released version of this plugin
10071
+ * already does.
10072
+ *
10073
+ * @param slots - The `slots` service (typed loosely: the accessor is optional).
10074
+ * @param key - The slot key to inspect.
10075
+ * @returns The declared kind, or `undefined`.
10076
+ */
10077
+ function declaredSlotKind(slots, key) {
10078
+ const face = slots;
10079
+ if (face === void 0 || typeof face.spec !== "function") return void 0;
10080
+ let spec;
10081
+ try {
10082
+ spec = face.spec(key);
10083
+ } catch {
10084
+ return;
10085
+ }
10086
+ const kind = spec?.kind;
10087
+ if (kind === "single" || kind === "list" || kind === "keyed" || kind === "chain") return kind;
10088
+ }
10089
+ /**
10090
+ * The registrations this plugin should make for the model-label seat.
10091
+ *
10092
+ * `keyed` yields ONE seat per provider id: each is its own cell, each renders
10093
+ * only the rows whose owner props carry that provider id, and a provider this
10094
+ * plugin does not own falls through to the owner's own fallback (the catalog
10095
+ * name, plus that model's `badge` on hosts that have one).
10096
+ *
10097
+ * Every other kind — including `undefined`, i.e. a host that cannot be
10098
+ * inspected — yields the single historical seat. That is not a degraded mode:
10099
+ * it is what `single` requires, and on a not-yet-declared slot it is what the
10100
+ * inject callback is about to find.
10101
+ *
10102
+ * An empty provider list under `keyed` would silently disable the label, so it
10103
+ * falls back to the single seat instead: a wrong-shaped registration that still
10104
+ * draws something beats a correct-shaped registration that draws nothing.
10105
+ *
10106
+ * @param params - Slot key, declared kind, this plugin's provider ids, priority.
10107
+ * @returns One or more `register()` option objects, in registration order.
10108
+ */
10109
+ function modelLabelSeats(params) {
10110
+ const { name, kind, providerIds, priority } = params;
10111
+ if (kind === "list" || kind === "chain") return [];
10112
+ if (kind === "keyed" && providerIds.length > 0) return providerIds.map((id) => ({
10113
+ name,
10114
+ key: id,
10115
+ priority
10116
+ }));
10117
+ return [{
10118
+ name,
10119
+ priority
10120
+ }];
10121
+ }
10122
+ /**
10123
+ * Claim every planned seat, atomically, and hand back ONE disposer.
10124
+ *
10125
+ * WHY ATOMIC. The seat list is 2-3 entries on a `keyed` host, and `register()`
10126
+ * can throw on any ONE of them — a rival plugin claiming the same provider id at
10127
+ * the same priority, or the declaration collapsing mid-loop. A plain `.map()`
10128
+ * would then leave the earlier seats landed on the ledger with their disposers
10129
+ * unreachable (they were only ever held by the array that the throw discarded),
10130
+ * which is worse than failing outright: the component stays mounted, the caller
10131
+ * believes registration failed, and nothing will ever release those cells. On a
10132
+ * `single` host it also means the identity keeps a live entry that the next
10133
+ * `inject` re-run cannot displace without a priority clash.
10134
+ *
10135
+ * So a partial failure is rolled back in reverse order before the error is
10136
+ * rethrown, exactly as if it had never been attempted. The rethrow is important:
10137
+ * each caller already has a catch that logs, and swallowing here would hide the
10138
+ * refusal from the diagnostics that exist to explain it.
10139
+ *
10140
+ * A disposal that itself throws must not mask the original error, hence the
10141
+ * inner catch — the registry treats a second dispose as a no-op, and a broken
10142
+ * one must not turn one failure into a crash.
10143
+ *
10144
+ * @param seats - The plan from {@link modelLabelSeats}, in registration order.
10145
+ * @param register - Claims one seat and returns its disposer.
10146
+ * @returns One disposer that releases every seat, in reverse registration order.
10147
+ */
10148
+ function registerModelLabelSeats(seats, register) {
10149
+ const claimed = [];
10150
+ try {
10151
+ for (const seat of seats) claimed.push(register(seat));
10152
+ } catch (error) {
10153
+ for (const dispose of [...claimed].reverse()) try {
10154
+ dispose();
10155
+ } catch {}
10156
+ throw error;
10157
+ }
10158
+ return () => {
10159
+ for (const dispose of [...claimed].reverse()) try {
10160
+ dispose();
10161
+ } catch {}
10162
+ };
10163
+ }
10164
+ //#endregion
10033
10165
  //#region src/client/index.tsx
10034
10166
  /** Stable browser-plugin name. */
10035
10167
  const name = "dsh-connect-trae-client";
@@ -10081,10 +10213,19 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
10081
10213
  * the same priority. That refusal is OUR fault and we can fix it rather than
10082
10214
  * report it, so each run releases whatever this identity already holds.
10083
10215
  *
10216
+ * ONE DISPOSER PER CALL, HOWEVER MANY ENTRIES IT OWNS. A `keyed` host makes this
10217
+ * plugin claim one cell per provider id (see `./model-label-seat.ts`), and those
10218
+ * cells must live and die together — a partial teardown would leave a cell
10219
+ * occupied by a component whose siblings are gone, visible as some rows
10220
+ * decorated and others not. Folding them into one disposer is
10221
+ * `registerModelLabelSeats`'s job, so the contract here stays the simple one:
10222
+ * whatever `register` returns is the whole ownership of this identity, and
10223
+ * `SlotHold` keeps exactly one.
10224
+ *
10084
10225
  * @param ctx - The applying plugin's context (module scope has none).
10085
10226
  * @param identity - Stable key for this seat.
10086
10227
  * @param slotKey - The host slot to register into.
10087
- * @param register - Performs the registration, returning its disposer.
10228
+ * @param register - Performs the registration(s), returning ONE disposer for all.
10088
10229
  * @param describe - Names the seat in the failure log.
10089
10230
  */
10090
10231
  function registerSlotEntry(ctx, identity, slotKey, register, describe) {
@@ -10349,16 +10490,31 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
10349
10490
  * non-interactive inline content such as a coloured name or badge" while the
10350
10491
  * picker keeps owning clicks, focus and check marks.
10351
10492
  *
10493
+ * HOW MANY SEATS, AND WHY THAT IS READ AT MOUNT. On a `single` host this slot
10494
+ * holds ONE cell: a second registration at the same priority throws, a second
10495
+ * registration at any other priority is silently discarded, and whoever holds
10496
+ * the cell must draw EVERY provider's rows. On a `keyed` host each
10497
+ * `options.key` is its own cell, so one seat per provider id gives this
10498
+ * plugin exactly its own rows and leaves every other provider to the owner's
10499
+ * own fallback. Which shape is legal is the HOST's declaration, so the kind
10500
+ * is read from `ctx.slots.spec(...)` at mount and the seat list is derived
10501
+ * from it (`./model-label-seat.ts`). One build serves both lines.
10502
+ *
10352
10503
  * Registered unconditionally: the component renders the plain name for a
10353
10504
  * provider that is not ours, so there is nothing to gate on. Its own
10354
10505
  * try/catch keeps a host that does not serve this slot from taking the card
10355
10506
  * down with it.
10356
10507
  */
10357
10508
  const registerModelLabel = () => {
10358
- registerSlotEntry(ctx, "conversation.input.model.label", "conversation.input.model.label", () => ctx.slots.register({
10359
- name: "conversation.input.model.label",
10360
- priority: MODEL_LABEL_PRIORITY
10361
- }, TraeModelLabel), "model label slot");
10509
+ registerSlotEntry(ctx, "conversation.input.model.label", "conversation.input.model.label", () => {
10510
+ const registry = ctx.slots;
10511
+ return registerModelLabelSeats(modelLabelSeats({
10512
+ name: "conversation.input.model.label",
10513
+ kind: declaredSlotKind(ctx.slots, "conversation.input.model.label"),
10514
+ providerIds: TRAE_MODEL_LABEL_PROVIDERS,
10515
+ priority: MODEL_LABEL_PRIORITY
10516
+ }), (seat) => registry.register(seat, TraeModelLabel));
10517
+ }, "model label slot");
10362
10518
  };
10363
10519
  registerModelLabel();
10364
10520
  /**
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.15.0",
5
+ "version": "4.16.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",