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

5.8 KiB
Raw Blame History

侧边栏插件操作规范(给其他插件的 Agent)

版本基线:DSH Desktop 2.0.17 / 宿主 @deepseek-ai/* 0.2.0-rc.2 用途:任何往 DSH 右侧栏挂 tab 的插件都按这份规范做,避免重复踩坑(每条都有实测根因)

一、硬规则(违反 = 静默失效,不报错)

1. Config 必须用宿主的 schemastery ⭐ 头号坑

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

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() 产出的不透明引用:

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 宿主包——

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 就是这么做的):

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 输出非确定性,字节数相同≠内容相同)。