dsh-sidecard-ask 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1
4
+
5
+ **主题:核对 DSH 0.2.0-rc.1 的适配性,并修掉由此暴露的一个真实缺陷。**
6
+
7
+ 核对(本机运行中的就是 0.2.0-rc.1,`@deepseek-ai/dsh` 的 `next` 通道):
8
+
9
+ - 包内容层面:`node tools/compat-probe.mjs 0.1.7-rc.2 0.2.0-rc.1` → 16 项能力**逐项一致**,没有任何依赖被移除或改名;
10
+ - 运行实例上:宿主半 `v1.2.0` 正常(`sideEngine`、provider `spawn,fork`、`capabilities.parent` 均可用);
11
+ 客户端半在 `shell.overlay` / `conversation.input.right` 都已注册;`standardProps` 仍含 `sessionId` 与 `inputActions`;
12
+ 真机端到端作答 `start→delta×31→status→done`(1.36 s)通过。新增的 `sessions.fork`/`uiWorkspace.forkSession`
13
+ 可选参数 `onCreated` 是纯增量,不影响既有调用。
14
+
15
+ **修掉的缺陷**:`sidebar.right.pane.tab` 的运行清单里只有 `dsh-better-sidebar:sidecard-ask:card`,
16
+ 没有本插件原生右侧栏的 `sidecard-ask:card`——两个适配器用了同一个 tab kind,而 better-sidebar 会把每个 tab 描述符
17
+ 镜像注册进**同一个**原生注册表(band `extension`),该注册表对同 band 同 kind 的二次注册抛错,后注册的一方静默失败。
18
+
19
+ - better-sidebar 承载面改用独立类型 `sidecard-ask:card:workbench`(`CARD_KIND_BETTER`),两端不再重叠;
20
+ - `contract-test` 新增"surface kinds must not collide"一节:**在测试里模拟该注册表的重复 kind 抛错规则**
21
+ 与 better-sidebar 的镜像注册行为,若两者再共用 kind,测试立即失败;
22
+ - 设置页自检行改为同时报告**两个适配器**(原先原生适配器的错误在界面上不可见,只能靠翻槽位清单发现)。
23
+
24
+ 自测 289 → **295 项**(verify 53 / contract 124 / smoke 118)。
25
+
26
+ ## 1.2.0
27
+
28
+ **主题:核对并加固与 dsh-better-sidebar 0.22.1 的适配。**
29
+
30
+ 核对方式(可复现):把 0.22.0 与 0.22.1 的发布产物都拉下来解包,逐文件 SHA256 比对。结论:
31
+
32
+ - 消费端契约 `lib/types/client/service.d.ts` **除 `SIDEBAR_SERVICE_VERSION` 常量外逐字节相同**;
33
+ `SIDEBAR_FEATURES`、`dsh.client.inject`、peer 依赖均未变;
34
+ - 0.22.1 唯一的大体量改动是它**自己** `src/client/native/index.ts` 的健壮性修复(`disposeSafely` +
35
+ 槽位注册失败时回滚已注册的 tab 类型),不涉及本插件调用的任何字段;
36
+ - 本插件用到的 `registerTab / openTab(seed, scope) / closeTab / features / version` 全部仍在。
37
+
38
+ 即 **0.22.1 下无需改动即兼容**。据此做了三处加固:
39
+
40
+ 1. **按能力而非版本判断**:适配器仅在 `features` 含 `tabMeta` 时可用(该能力自 better-sidebar 0.12 起提供,
41
+ 更早的版本会打开一个读不到 `meta.cardId` 的空 tab);不满足时 `auto` 跳到原生右侧栏/内置浮层,
42
+ 并在自检里给出 `no-tab-meta`。
43
+ 2. **原生承载面注册回滚**:先注册 tab 类型再注册槽位,槽位注册抛错时释放已占用的类型 id
44
+ (正是 0.22.1 在自己原生胶水里修的同类问题,否则该 kind 会永久占用、渲染宿主"无实现"的空面)。
45
+ 3. **运行时可见**:设置页「运行自检」新增一行,显示侧边卡片插件**自己报告的 `version` 与能力项数**,
46
+ 以及本插件对它的判定(可用 / 版本过旧 / 未检测到)。
47
+
48
+ 自测 276 → **289 项**(verify 53 / contract 118 / smoke 118),新增用例覆盖:0.22.1 形状的服务被接受、
49
+ `openTab(seed, scope)` 的字段与作用域、缺 `tabMeta` 时被拒绝并落到浮层、缺 `registerTab` 时被拒绝、
50
+ 槽位注册失败时类型被释放且适配器转为不可用。
51
+
3
52
  ## 1.1.0
4
53
 
5
54
  **改名:`dsh-selection-followup` → `dsh-sidecard-ask`**(显示名「划词追问」→「侧边卡片追问」)。
package/README.md CHANGED
@@ -152,14 +152,66 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
152
152
 
153
153
  | 顺序 | 承载面 | 依赖 | 失败时 |
154
154
  |---|---|---|---|
155
- | 1 | DSH 原生右侧栏 | `ctx.sidebarRightTabs`(注册 tab 类型)+ `ctx.sidebarRight`(`openTab`),并占用槽位 `sidebar.right.pane.tab` / `…tab.title` | 记 `surfaces.native.error`,落到下一层 |
156
- | 2 | **dsh-better-sidebar**(侧边卡片插件) | 客户端服务 `ctx.betterSidebar`:`registerTab` + `openTab(seed, scope)`;卡片 id 走 `tab.meta.cardId`(依赖其 `features` 含 `tabMeta`) | 同上 |
155
+ | 1 | DSH 原生右侧栏 | `ctx.sidebarRightTabs`(注册 tab 类型)+ `ctx.sidebarRight`(`openTab`),并占用槽位 `sidebar.right.pane.tab` / `…tab.title` | 记 `surfaces.native.error`,落到下一层;**类型注册会回滚**(见下) |
156
+ | 2 | **dsh-better-sidebar**(侧边卡片插件) | 客户端服务 `ctx.betterSidebar`:`registerTab` + `openTab(seed, scope)`;卡片 id 走 `tab.meta.cardId`,因此**要求其 `features` 含 `tabMeta`** | 同上(缺 `tabMeta` 时记 `no-tab-meta` 并跳过,而不是开一个空 tab) |
157
157
  | 3 | **内置浮层卡片**(默认兜底) | 只需要 `shell.overlay` 槽位 | ——(这是保底面,永远可用) |
158
158
 
159
159
  - 装了 `dsh-better-sidebar`:卡片以它的 tab 形式出现在它的面板里,关闭卡片会同时 `closeTab`,不残留空 tab。
160
160
  - 没装:自动使用内置浮层卡片(右下角卡片栈),功能完全一致——**这条路径是本插件的默认与保底路径**。
161
161
  - 强制指定了一个不可用的承载面:回退到内置浮层,并在卡片上标注「已回退」。
162
162
 
163
+ ### 5.1 与 dsh-better-sidebar 0.22.1 的适配核对(2026-09-28)
164
+
165
+ 做法:把 0.22.0 与 0.22.1 的发布产物都拉下来、解包、逐文件比对(`npm pack` + SHA256)。
166
+
167
+ | 检查项 | 结果 |
168
+ |---|---|
169
+ | 消费端契约 `lib/types/client/service.d.ts` | **除 `SIDEBAR_SERVICE_VERSION` 常量外逐字节相同** → `BetterSidebarService` 的方法/参数/返回类型没变 |
170
+ | `SIDEBAR_FEATURES`(能力清单) | **未变**(`tabMeta` 等仍在;契约承诺"Features are never removed") |
171
+ | `dsh.client.inject` / peer 依赖 | 未变(仍是 locale / ui-slots / ui-conversation / ui-sidebar-right / client-modules) |
172
+ | `src/client/native/index.ts`(0.22.1 唯一大体量改动) | 是它**自己原生胶水的健壮性修复**:`disposeSafely` + 槽位注册失败时回滚已注册的 tab 类型;不涉及我们调用的任何字段 |
173
+ | 本插件实际用到的面 | `registerTab({id,title,description,order,dedupeKey,component})`、`openTab({type,id,title,meta},{sessionId})`、`closeTab(id)`、`features`、`version` —— 全部仍在 |
174
+
175
+ **因此 0.22.1 下适配为「无需改动即兼容」**;本轮据此做了三处加固:
176
+
177
+ 1. **能力门控而非版本判断**:适配器只在 `features` 含 `tabMeta` 时可用(0.12 起才有,旧版会开出空 tab);
178
+ 不满足时 `auto` 直接跳到原生右侧栏/内置浮层,设置页自检里写明原因(`no-tab-meta`)。
179
+ 2. **原生承载面类型注册回滚**:先注册 tab 类型、再注册槽位;槽位注册抛错(reload/teardown 期的失活 context)时
180
+ **释放已占用的类型 id**——这正是 0.22.1 在自己原生胶水里修的同一类问题(否则该 kind 会永久占用并显示宿主"没有实现"的空面)。
181
+ 3. **运行时可见**:设置页「运行自检」新增一行,直接显示**侧边卡片插件自己的 `version` 与能力项数**,
182
+ 以及本插件对它的判定(可用 / 版本过旧 / 未检测到),不再依赖 package.json 的声明。
183
+
184
+ > 注意:`version` 是加载到浏览器里的那份模块报告的版本。页面刷新后即可用它确认"跑的是不是 0.22.1"。
185
+
186
+ ### 5.2 与 DSH 0.2.0-rc.1 的适配核对(2026-09-29)
187
+
188
+ 本机实际运行的就是 **0.2.0-rc.1**(`@deepseek-ai/dsh` 的 `next` 通道;`latest` 仍是 0.1.7-rc.2)。核对分两层:
189
+
190
+ **① 包内容层面**(`node tools/compat-probe.mjs 0.1.7-rc.2 0.2.0-rc.1`):
191
+ 16 项能力**逐项与 0.1.7-rc.2 完全一致**——`shell.overlay`、`conversation.input.right`、`settings.section`、
192
+ `data-slot` 锚点、原生右侧栏、`sessions.using/retain`、归档门、`agent/assistant-stream`、子代理、`tools.schemas`、
193
+ `webServer` 路由……全部 ✅。
194
+
195
+ **② 在运行中的实例上实测**:
196
+
197
+ | 检查 | 结果 |
198
+ |---|---|
199
+ | 宿主半是否加载 | ✅ `/sidecard-ask/api/state` 返回 `dsh-sidecard-ask v1.2.0`,`sideEngine=true`(provider `spawn,fork`)、`capabilities.parent` 正常 |
200
+ | 客户端半是否注册 | ✅ `shell.overlay` 有 `sidecard-ask`(order 45、active)、`conversation.input.right` 有 `sidecard-ask-session-probe`、`settings.section` 有设置页 |
201
+ | 依赖的槽位与标准 prop | ✅ `conversation.input.right` 仍是 list/session,`standardProps` 仍含 **`sessionId`** 与 **`inputActions`** |
202
+ | 依赖的客户端服务 | ✅ `sessions`(`retain`/`using`/`scope`)、`slots`、`locale`、`layout`、`theme` 全在;`sessions.fork`/`uiWorkspace.forkSession` 只是**新增**了可选 `onCreated`,不影响既有调用 |
203
+ | 端到端作答 | ✅ 真机 `start→delta×31→status→done`(1.36 s,`streamSource=frames`),只读工具白名单照常生效 |
204
+
205
+ **③ 由此发现并修掉的真实缺陷(1.2.1)**:运行中的 `sidebar.right.pane.tab` 清单里只有
206
+ `dsh-better-sidebar:sidecard-ask:card`,**没有我的原生 `sidecard-ask:card`**。原因是两个适配器用了**同一个 tab kind**:
207
+ better-sidebar 会把收到的每个 tab 描述符**镜像注册进同一个原生注册表**(`sidebarRightTabs.register`,band `extension`),
208
+ 而该注册表对*同 band 同 kind 的二次注册直接抛错*(源码注释:*"Everything else colliding on a kind throws"*)——
209
+ 后注册的那一方就此失败,而且**在界面上不可见**。修法:
210
+
211
+ 1. better-sidebar 承载面改用独立类型 **`sidecard-ask:card:workbench`**,两端 kind 不再重叠;
212
+ 2. `contract-test` **模拟该注册表的"重复 kind 抛错"规则**,此类冲突今后会直接让测试失败;
213
+ 3. 设置页自检行改为**同时报告两个适配器**的状态(此前原生适配器的错误压根看不到)。
214
+
163
215
  ---
164
216
 
165
217
  ## 六、未知 DSH API:占位接口与替换方式
@@ -330,7 +382,7 @@ node test/smoke-test.mjs # 端到端:流式/截断/取消/持久化/失
330
382
  ```
331
383
 
332
384
  三个脚本都以 `process.exitCode` 反映结果,失败会列出具体条目;测试会把 `DSH_HOME` 指向临时目录,不会污染真实配置。
333
- 当前规模:verify 53 项 + contract 105 项 + smoke 118 项 = **276 项全部通过**。
385
+ 当前规模:verify 53 项 + contract 118 项 + smoke 118 项 = **289 项全部通过**。
334
386
 
335
387
  ### 10.2 版本能力探测(§7 矩阵的来源)
336
388
 
@@ -410,14 +462,27 @@ Host: ctx.subagents.start('spawn')
410
462
  ## 十三、发布流程
411
463
 
412
464
  ```powershell
413
- npm whoami # 先确认登录态(改过 2FA/密码会让旧 token 失效)
465
+ npm whoami # 先确认登录态;本机用的是 granular Publish token
414
466
  node test/verify.mjs; node test/contract-test.mjs; node test/smoke-test.mjs
415
- npm publish --access public
467
+ npm publish --access public # 1.0.1 那次被 npm 暂存(staged)后才转正;再遇到可用 --otp=<验证码>
468
+ git push origin main # 仓库:https://github.com/HERO476/dsh-sidecard-ask
469
+ ```
470
+
471
+ 改名/迁移时的额外两步(1.1.0 实际做过):
472
+
473
+ ```powershell
474
+ plugin_manager(action="remove_bundle", target="<旧包名>") # 卸旧 bundle(同时移除 profile 里的 link)
475
+ plugin_manager(action="install_bundle", target="D:\...\<新目录>")
476
+ npm deprecate "<旧包名>@*" "Renamed to <新包名> - <原因>" # 旧名指路,避免别人再装到废弃名
416
477
  ```
417
478
 
418
479
  版本号同时出现在三处,必须一致:`package.json` 的 `version`、`index.js` 的 `PLUGIN_VERSION`、`client.js` 的 `api.version`
419
480
  (`test/verify.mjs` 会断言前两处;第三处在契约测试中同样被断言)。
420
481
 
482
+ **发布前必查"地址类信息"**(1.0.0 的教训):包名不能只查 npm —— 还要查 GitHub 仓库名是否已被占用,以及
483
+ [`awesome-dsh-plugin` 收录索引](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/tree/main/data/plugins)
484
+ 里是否已有同名插件(本插件第一次改名就是因为漏查了后者)。
485
+
421
486
  ---
422
487
 
423
488
  ## 十四、已知限制(含未在本机验证的部分)
@@ -435,5 +500,5 @@ npm publish --access public
435
500
  本仓库当前文件比运行中的宿主模块新(1.0.1 的「归档父会话不再一刀切 / 持久化 chunk 流式源 / 能力门控 / 路由降级」),
436
501
  **重启后**这些改动才生效;`/state` 的 `version` 与 `capabilities.parent` 字段可以直接确认是否已加载新版。
437
502
  6. **除 0.1.7-rc.2 外,其余版本只做了产物层验证**:见 §7.3——插件在 0.1.2-rc.1 … 0.1.6-alpha.2 上安装启动过**没有**得到验证,
438
- 验证到的是"这些版本缺哪些 API + 插件对每个缺失都有分支",分支本身由 276 项自测覆盖。
503
+ 验证到的是"这些版本缺哪些 API + 插件对每个缺失都有分支",分支本身由 289 项自测覆盖。
439
504
 
package/client.js CHANGED
@@ -42,9 +42,27 @@ window.__ModuleLoader__.load({
42
42
  settings: 'sidecard-ask-settings',
43
43
  sessionProbe: 'sidecard-ask-session-probe',
44
44
  }
45
- /** Side-card tab type registered in a side-card host. */
45
+ /**
46
+ * Tab kind served by the DSH native right rail (also its implementation id).
47
+ */
46
48
  const CARD_KIND = 'sidecard-ask:card'
47
49
 
50
+ /**
51
+ * Tab type served by the dsh-better-sidebar plugin.
52
+ *
53
+ * It MUST differ from {@link CARD_KIND}: better-sidebar mirrors every tab
54
+ * descriptor it is given into the SAME native registry
55
+ * (`sidebarRightTabs.register`, id `dsh-better-sidebar:<id>`, band
56
+ * `extension`), and that registry THROWS when a kind already has a
57
+ * registration in the same band ("Everything else colliding on a kind
58
+ * throws"). Sharing one kind made the two adapters collide: whichever
59
+ * registered second failed — observed live on 0.2.0-rc.1, where the pane
60
+ * listed `dsh-better-sidebar:sidecard-ask:card` and no native
61
+ * `sidecard-ask:card` at all. `test/contract-test.mjs` now emulates that
62
+ * registry rule so the collision cannot come back.
63
+ */
64
+ const CARD_KIND_BETTER = 'sidecard-ask:card:workbench'
65
+
48
66
  /**
49
67
  * Client-side mirror of the Host `DEFAULT_CONFIG`. The client needs values
50
68
  * before `/state` answers (first paint) and when the Host is unreachable;
@@ -167,6 +185,10 @@ window.__ModuleLoader__.load({
167
185
  diagZones: '区域锚点(data-slot)',
168
186
  diagZonesMissing: '缺失,区域过滤已停用',
169
187
  diagSlots: '槽位落点',
188
+ diagSidecard: '侧边卡片插件(dsh-better-sidebar)',
189
+ diagFeatures: '项能力',
190
+ diagNoTabMeta: '版本过旧:没有 tabMeta 能力,已跳过它改用其它承载面',
191
+ diagNativeKind: 'kind',
170
192
  toolGuardUnavailable: '该 provider 不支持工具白名单,本次作答继承了当前会话的工具',
171
193
  toolGuardInherited: '按配置继承当前会话的工具',
172
194
  diagUnreachable: '宿主接口不可达:{msg}',
@@ -266,6 +288,10 @@ window.__ModuleLoader__.load({
266
288
  diagZones: 'Zone anchors (data-slot)',
267
289
  diagZonesMissing: 'missing — region filtering disabled',
268
290
  diagSlots: 'Slot landing',
291
+ diagSidecard: 'Side-card plugin (dsh-better-sidebar)',
292
+ diagFeatures: 'capabilities',
293
+ diagNoTabMeta: 'too old: no tabMeta capability, skipped in favour of another surface',
294
+ diagNativeKind: 'kind',
269
295
  toolGuardUnavailable: 'This provider supports no tool allow-list, so the answer inherited the session tools',
270
296
  toolGuardInherited: 'Inherits the session tools, as configured',
271
297
  diagUnreachable: 'Host API unreachable: {msg}',
@@ -859,21 +885,44 @@ window.__ModuleLoader__.load({
859
885
  error: null,
860
886
  dispose: null,
861
887
  features: [],
888
+ /**
889
+ * Why this adapter is (not) usable, in the settings page's own words.
890
+ * `null` means "usable"; a string explains the refusal.
891
+ */
892
+ reason: 'not-detected',
893
+ /**
894
+ * Reported by the plugin itself (`BetterSidebarService.version`), so the
895
+ * settings page can say which side-card plugin version is really in use.
896
+ */
897
+ version: null,
898
+ /**
899
+ * Usable only when the service advertises the `tabMeta` capability: the
900
+ * card id travels in `tab.meta`, and `SIDEBAR_FEATURES` has listed
901
+ * `tabMeta` since v0.12.0 with newer versions only ever ADDING features
902
+ * ("Features are never removed"). A pre-0.12 instance would open an
903
+ * empty tab, so it is skipped and the ladder continues to the native
904
+ * right rail or the built-in flow card instead.
905
+ */
862
906
  available() {
863
- return this.service !== null && this.error === null
907
+ if (this.service === null || this.error !== null) return false
908
+ return Array.isArray(this.features) && this.features.includes('tabMeta')
864
909
  },
865
910
  open(card) {
866
911
  if (!this.available() || typeof this.service.openTab !== 'function') return false
867
- const tabId = `${CARD_KIND}:${card.id}`
912
+ const tabId = `${CARD_KIND_BETTER}:${card.id}`
913
+ // `meta` is the transport for the card id (feature `tabMeta`), and
914
+ // `dedupeKey` on our descriptor is what collapses repeat opens onto
915
+ // the same tab; both are part of the stable consumer contract
916
+ // (`lib/types/client/service.d.ts`, unchanged across 0.22.0 → 0.22.1).
868
917
  this.service.openTab(
869
- { type: CARD_KIND, id: tabId, title: card.question.slice(0, 32), meta: { cardId: card.id } },
918
+ { type: CARD_KIND_BETTER, id: tabId, title: card.question.slice(0, 32), meta: { cardId: card.id } },
870
919
  store.state.sessionId === null ? undefined : { sessionId: store.state.sessionId },
871
920
  )
872
921
  return true
873
922
  },
874
923
  close(card) {
875
924
  if (!this.available() || typeof this.service.closeTab !== 'function') return false
876
- this.service.closeTab(`${CARD_KIND}:${card.id}`)
925
+ this.service.closeTab(`${CARD_KIND_BETTER}:${card.id}`)
877
926
  return true
878
927
  },
879
928
  },
@@ -1523,6 +1572,9 @@ window.__ModuleLoader__.load({
1523
1572
  h('div', { key: 'session' }, `${t('diagSession')}: ${state.sessionId ?? t('none')}`),
1524
1573
  h('div', { key: 'zones' }, `${t('diagZones')}: ${state.zoneAnchors === false ? t('diagZonesMissing') : t('yes')}`),
1525
1574
  h('div', { key: 'slots' }, `${t('diagSlots')}: ${Object.entries(state.slots).map(([k, v]) => `${k}=${v}`).join(' · ')}`),
1575
+ // The side-card plugin's own report: which version is loaded and
1576
+ // whether it advertises the capability this adapter needs.
1577
+ h('div', { key: 'sidecard' }, `${t('diagSidecard')}: ${describeSurfaces()}`),
1526
1578
  h('div', { key: 'path' }, `${t('settingsTitle')} → ${diag.provenance?.persistedPath ?? ''}`),
1527
1579
  ])
1528
1580
  : null)
@@ -1549,6 +1601,39 @@ window.__ModuleLoader__.load({
1549
1601
  return capturedComposerActions === null ? null : { actions: capturedComposerActions }
1550
1602
  }
1551
1603
 
1604
+ /**
1605
+ * One line describing how each optional side-card surface resolved, for the
1606
+ * settings page's capability check. Both adapters are reported, because a
1607
+ * failing NATIVE registration was previously invisible here (it only showed
1608
+ * up as a missing entry in the pane's own slot inventory).
1609
+ * @returns {string} human-readable state.
1610
+ */
1611
+ function describeSurfaces() {
1612
+ const native = surfaces.native.available()
1613
+ ? `${t('yes')} · ${t('diagNativeKind')}=${CARD_KIND}`
1614
+ : `${t('no')}${surfaces.native.error === null ? '' : `(${surfaces.native.error})`}`
1615
+ return `${t('surfaceNative')}: ${native} / ${t('surfaceBetter')}: ${describeSidecardAdapter()}`
1616
+ }
1617
+
1618
+ /**
1619
+ * The optional side-card plugin's own report: which version is loaded and
1620
+ * whether it advertises the capability this adapter needs.
1621
+ * @returns {string} human-readable state.
1622
+ */
1623
+ function describeSidecardAdapter() {
1624
+ const better = surfaces.better
1625
+ if (better.error !== null) return `${t('no')}(${better.error})`
1626
+ if (better.version === null && better.service === null) return t('none')
1627
+ const version = better.version ?? '?'
1628
+ if (better.available()) {
1629
+ return `v${version} · ${better.features.length} ${t('diagFeatures')} · ${t('yes')}`
1630
+ }
1631
+ if (better.reason === 'no-tab-meta') {
1632
+ return `v${version} · ${t('diagNoTabMeta')}`
1633
+ }
1634
+ return `v${version} · ${better.reason ?? t('no')}`
1635
+ }
1636
+
1552
1637
  // ─────────────────────────────────────────────────────────────────────
1553
1638
  // §10 plugin face
1554
1639
  // ─────────────────────────────────────────────────────────────────────
@@ -2099,6 +2184,18 @@ window.__ModuleLoader__.load({
2099
2184
  if (registry === null || registry === undefined) return
2100
2185
  surfaces.native.registry = registry
2101
2186
  surfaces.native.controller = controller ?? null
2187
+ // Register the tab TYPE first, then its body/title slots — and
2188
+ // roll the type back if a slot refuses to register.
2189
+ //
2190
+ // The host takes the id the moment `register` returns and refuses
2191
+ // a second registration of the same id; a slot registration that
2192
+ // throws (an already-inactive context during a reload/disposal)
2193
+ // would then leave the kind taken for the rest of the page's life,
2194
+ // rendering the host's "nothing can view this" face forever. This
2195
+ // mirrors what dsh-better-sidebar 0.22.1 fixed in its own native
2196
+ // glue (`disposeSafely` + partial-set release in
2197
+ // `src/client/native/index.ts`).
2198
+ let releaseType = null
2102
2199
  if (typeof registry.register === 'function') {
2103
2200
  const off = registry.register({
2104
2201
  id: CARD_KIND,
@@ -2111,20 +2208,40 @@ window.__ModuleLoader__.load({
2111
2208
  description: () => t('settingsDesc'),
2112
2209
  }],
2113
2210
  })
2114
- surfaces.native.dispose = typeof off === 'function' ? off : null
2211
+ releaseType = typeof off === 'function' ? off : null
2212
+ surfaces.native.dispose = releaseType
2213
+ }
2214
+ const slotDisposers = []
2215
+ try {
2216
+ slotDisposers.push(ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
2217
+ name: 'sidebar.right.pane.tab',
2218
+ key: CARD_KIND,
2219
+ inject: sessionId => ({ sessionId }),
2220
+ }, (props) => h(CardHost, { props, actions }))))
2221
+ slotDisposers.push(ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
2222
+ name: 'sidebar.right.pane.tab.title',
2223
+ key: CARD_KIND,
2224
+ }, () => h('span', null, t('answerTitle')))))
2225
+ } catch (error) {
2226
+ for (const dispose of slotDisposers) {
2227
+ try {
2228
+ dispose()
2229
+ } catch { /* releasing must not mask the failure */ }
2230
+ }
2231
+ if (releaseType !== null) {
2232
+ try {
2233
+ releaseType()
2234
+ } catch { /* releasing must not mask the failure */ }
2235
+ surfaces.native.dispose = null
2236
+ }
2237
+ surfaces.native.registry = null
2238
+ surfaces.native.controller = null
2239
+ throw error
2115
2240
  }
2116
- const offBody = ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
2117
- name: 'sidebar.right.pane.tab',
2118
- key: CARD_KIND,
2119
- inject: sessionId => ({ sessionId }),
2120
- }, (props) => h(CardHost, { props, actions })))
2121
- const offTitle = ctx.slots.inject('sidebar.right.pane.tab.title', () => ctx.slots.register({
2122
- name: 'sidebar.right.pane.tab.title',
2123
- key: CARD_KIND,
2124
- }, () => h('span', null, t('answerTitle'))))
2125
2241
  disposers.push(() => {
2126
- try { offBody() } catch { /* already disposed */ }
2127
- try { offTitle() } catch { /* already disposed */ }
2242
+ for (const dispose of slotDisposers) {
2243
+ try { dispose() } catch { /* already disposed */ }
2244
+ }
2128
2245
  try { surfaces.native.dispose?.() } catch { /* already disposed */ }
2129
2246
  })
2130
2247
  } catch (error) {
@@ -2137,9 +2254,15 @@ window.__ModuleLoader__.load({
2137
2254
  }
2138
2255
 
2139
2256
  // The dsh-better-sidebar plugin, when installed: its client service
2140
- // registers a tab type and opens one per card. `features.tabMeta`
2141
- // gates the meta-carried card id, so an older version of that plugin
2142
- // degrades to the built-in flow card instead of showing an empty tab.
2257
+ // registers a tab type and opens one per card.
2258
+ //
2259
+ // Verified against the published consumer contract: between 0.22.0 and
2260
+ // 0.22.1 `lib/types/client/service.d.ts` is byte-identical apart from the
2261
+ // version constant, and 0.22.1's `src/client/native/index.ts` changes are
2262
+ // robustness fixes in ITS own native glue — no field we use changed. The
2263
+ // `features` array is the forward-compatible half of that contract
2264
+ // ("Features are never removed"), so the adapter gates on `tabMeta`
2265
+ // instead of on a version number.
2143
2266
  try {
2144
2267
  ctx.inject(['betterSidebar'], (injected) => {
2145
2268
  try {
@@ -2147,9 +2270,18 @@ window.__ModuleLoader__.load({
2147
2270
  if (service === null || service === undefined) return
2148
2271
  const features = Array.isArray(service.features) ? service.features : []
2149
2272
  surfaces.better.features = features
2150
- if (typeof service.registerTab !== 'function') return
2273
+ surfaces.better.version = typeof service.version === 'string' ? service.version : null
2274
+ surfaces.better.reason = features.includes('tabMeta') ? null : 'no-tab-meta'
2275
+ if (typeof service.registerTab !== 'function') {
2276
+ surfaces.better.reason = 'no-register-tab'
2277
+ return
2278
+ }
2151
2279
  const off = service.registerTab({
2152
- id: CARD_KIND,
2280
+ // A DIFFERENT type from the native rail's: better-sidebar mirrors
2281
+ // this descriptor into `sidebarRightTabs` as an `extension`-band
2282
+ // registration, and a second registration of the same kind in
2283
+ // that band throws (see CARD_KIND_BETTER).
2284
+ id: CARD_KIND_BETTER,
2153
2285
  title: () => t('answerTitle'),
2154
2286
  description: () => t('settingsDesc'),
2155
2287
  order: 120,
@@ -2241,13 +2373,14 @@ window.__ModuleLoader__.load({
2241
2373
  * `window` and exercises these without a browser.
2242
2374
  */
2243
2375
  api: Object.freeze({
2244
- version: '1.1.0',
2376
+ version: '1.2.1',
2245
2377
  /** Read-only state accessor for diagnostics and the test harness. */
2246
2378
  snapshot: () => store.state,
2247
2379
  pure: Object.freeze({
2248
2380
  CLIENT_DEFAULTS,
2249
2381
  IDS,
2250
2382
  CARD_KIND,
2383
+ CARD_KIND_BETTER,
2251
2384
  parseSseBlock,
2252
2385
  truncateSelection,
2253
2386
  parseShortcut,
package/index.js CHANGED
@@ -42,7 +42,7 @@ export const name = 'dsh-sidecard-ask'
42
42
  export const inject = ['webServer']
43
43
 
44
44
  /** Version of this plugin (kept in step with package.json by test/verify.mjs). */
45
- export const PLUGIN_VERSION = '1.1.0'
45
+ export const PLUGIN_VERSION = '1.2.1'
46
46
 
47
47
  /** Route prefix of the plugin's own API. */
48
48
  export const ROUTE_PREFIX = '/sidecard-ask/api'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sidecard-ask",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "DSH 划词追问(侧边卡片作答):在聊天区与任务区选中文本就地追问,答案由独立子代理在侧边卡片里流式呈现,也可选择落回主对话。",
5
5
  "type": "module",
6
6
  "main": "index.js",