Files
apes-sidebar/docs/plugin-conventions.md
T
figmar 6b4a29ba24
CI / ci (pull_request) Canceled after 0s
CI / ci-windows (pull_request) Canceled after 0s
CI / plugin-mount (pull_request) Canceled after 0s
feat(subagent): open a child's chat in the native right sidebar
The host's own catalog puts an "open beside" action on every non-current
child row; this port had never wired it. Add it: the row grows a hover
button that opens the child's chat as a `subagentchat` resource in the
native right Sidebar, so a user can watch a child work without giving up
the pane he is in (the in-pane jump stays the plain row click).

- src/client/native/subagent-chat.ts: the resource-address codec, copied
  from @deepseek-ai/dsh-client-ui-subagent rather than imported (the
  package exports no helpers and the plugin must not reach into it).
- native/surface.ts: carry `kind` / `preferNewPane` on an open.
- SubagentView: `openChildAside` via `ctx.get?.('sidebarRight')` — an
  optional probe, since a fake ctx without `get` otherwise crashes the
  whole render (9 specs caught it) — threaded through all three
  CatalogRows call sites.
- locales: `subagentOpenAside` in zh/en plus all 20 third-language
  dictionaries (tests/locales.spec.ts enforces key-set parity).
- docs: the conventions page now documents the resource-open recipe.

Gates: typecheck / lint / build / test 132 files 1390 passed /
check:consumer-types, plus the repo's runtime-contract symbol check.
2026-10-14 09:32:16 +08:00

88 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 侧边栏插件操作规范(给其他插件的 Agent)
> 版本基线:DSH Desktop 2.0.17 / 宿主 `@deepseek-ai/*` 0.2.0-rc.2
> 用途:任何往 DSH 右侧栏挂 tab 的插件都按这份规范做,避免重复踩坑(每条都有实测根因)
## 一、硬规则(违反 = 静默失效,不报错)
### 1. `Config` 必须用宿主的 schemastery ⭐ 头号坑
```ts
import z from '@deepseek-ai/schemastery' // ✅ 宿主 fork,含 volatile 支持
import z from 'schemastery' // ❌ 公版 3.18.0,无 volatile
```
- 后果:`settings.update` **写进 `cordis.patch.yml` 成功**,但宿主 loader 静默 no-op —— 不生效、不报错、不重启、值冻结。
- 机制:宿主 `cordis.resolveConfig` 走 `runtime.Config['~standard'].validate()`,用的是**插件自己那份 schemastery** 的解析器;无 volatile 分支 ⇒ 没有 volatile 引用 ⇒ `cordis-plugin-loader._commitVolatile()` 首行 `if (!volatileEntries(fiber.config).length) return true` ⇒ 视作已提交。
- `package.json`:`peerDependencies` 加 `"@deepseek-ai/schemastery": "^3.18.1"`(宿主 mount 时提供),`devDependencies` 钉 `3.18.4`。
### 2. 用户偏好字段必须标 volatile,且并入 `Config`
```ts
tabsEnabled: z.dict(z.boolean()).default({}).extra('volatile', true)
```
- 0.2.0+ 移除了 `settings.register()`,**loader 只注册插件导出的 `Config`**;独立 `PrefsSchema` 是死代码。
- `settings.update` **只放行 volatile 字段**(非 volatile 路径抛 `Config field "<x>" is not volatile`)。
- **部署字段**(limit / shell 之类)保持**非 volatile**——它们不该被设置页改写。
### 3. settings ns = **profile 条目 id**,不是包名
`cordis.patch.yml` 里 `- id: <ns>` 的那个 id。用包名会在 `settings.describe()` 里找不到自己(报 `No configurable plugin entry "<包名>"`)。
### 4. tab id / map key **不要用冒号**
`apesTasks` ✅ / `apes:tasks` ❌。冒号 key 在设置投影链路上出现过丢失。
(i18n key 不受影响,本来就是 camelCase。)
### 5. volatile 字段**不能直读**
修复后 `config.tabsEnabled` 是 `createVolatile()` 产出的**不透明引用**:
```js
Object.freeze({ get: () => current, [Symbol.for('cosmokit.volatile.write')]: v => { current = v } })
```
⇒ 没有数据属性,`config.tabsEnabled.git` 是 `undefined`(不是旧值),`JSON.stringify` 出 `{}`。
**必须走 `settings.get()` / `settings.describe()`**(宿主对 ref 调 `plainConfig()` 解包)。
## 二、0.2.0-rc.2 消失的宿主 API 与官方替代
| 老调用(会静默 no-op) | 官方替代(宿主自己的 UI 就是这么调的) |
|---|---|
| `sessions.openSubagent(address)` | `sessions.open(childSessionId)` |
| `sessions.setSubagentCatalogOpen(id, open)` | `sessions.refreshProjections(id)` |
| `sessions.refreshSubagents(id)` | `sessions.refreshProjections(id)` |
判定方法(别猜):在 app 内全量 grep 宿主包——
```bash
A="/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai"
grep -rn "<apiName>" "$A"/*/lib/*.js | head
# 0 命中 = 宿主没实现;再看官方自己的插件怎么调的(如下):
sed -n '945,1005p' "$A/dsh-client-ui-subagent/lib/client.js"
```
`dsh-client-ui-subagent` 是**宿主自己的子代理 UI**,是找「官方正确姿势」的最权威样板。
## 二·补、把「另一个会话的聊天」开到侧栏(官方做法)
不想切走主面板、又想盯着某个子会话时,用官方资源式打开(宿主自己的子代理 UI 就是这么做的):
```ts
import { SUBAGENT_CHAT_KIND, subagentChatAddress } from './native/subagent-chat.ts' // 本插件提供的编解码
const column = (ctx as { get?: (n: string) => unknown }).get?.('sidebarRight') as
{ openResource?: (a: string, o?: { kind?: string; preferNewPane?: boolean }) => void } | undefined
column?.openResource?.(subagentChatAddress(address), { kind: SUBAGENT_CHAT_KIND, preferNewPane: true })
```
- 地址格式(必须逐字节一致,否则宿主解析不到):`dsh-resource://subagentchat/session/<encodeURIComponent(childSessionId)>?parent=<parentSessionId>&mode=<mode>`
- `kind: 'subagentchat'` + `preferNewPane: true` 是官方那两个参数,别省。
- 读取服务必须**可选探测**(`ctx.get?.()`):测试里的假 ctx 没有 `get`,直接 `ctx.get(...)` 会在渲染期崩掉整棵子树(我们踩过:9 个渲染测试全挂)。
- 交互:官方只在「该行不是当前会话」时显示按钮,并给 `title` + `aria-label` 同一份文案。
## 三、注册/生命周期
- 服务注册必须等服务、不能等槽声明:`ctx.inject(['sidebarRightTabs'], cb)` 驱动类型注册(槽声明早于服务 provide,按槽注册会静默什么都不注册且不重试);槽只用来挂 tab 体。
- 原生 tab 体宿主 `.paneBody` 是**有确定高度的块级滚动容器**,native 适配层要给每个 tab 体包 `height:100%` 的列 flex 宿主。
- 跨会话操作原生栏用 `openTabIn/openResourceIn/closeIn`(不在 `ISidebarRight` 接口里,需结构化探测)。
## 四、自检清单(发版前逐条过)
1. `pnpm typecheck && pnpm lint && pnpm build && pnpm test` 全绿(构建 ≠ typecheck,tsdown 跳过 tsc 严格性)。
2. `grep '^import z' lib/index.js` → 必须是 `@deepseek-ai/schemastery`。
3. `grep -c 'schemastery' package.json` → `dependencies` 里**没有**公版 schemastery。
4. 设置开关实测:点两次,值**不再弹回**,且 `cordis.patch.yml` 里的新值等于 UI 显示值。
5. 改完配置**不需要重启 DSH**(volatile 就地生效);若需要重启才生效 = 第 1 条没做对。
6. 产物 rsync 到 `~/.dsh/profiles/<profile>/node_modules/<pkg>/lib/` 后**逐文件 md5 比对**(tsdown 输出非确定性,字节数相同≠内容相同)。