diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index 8eb22e2..f4486b3 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -16,7 +16,7 @@ body: options: - label: 我已搜索现有 issue,确认没有**完全相同**的条目(只有完全一致才算重复)| I searched existing issues and confirmed there is no **exact duplicate** (only exactly identical issues count as duplicates) required: true - - label: 我已确认使用最新版本 dsh-better-sidebar / I confirmed I am on the latest dsh-better-sidebar version + - label: 我已确认使用最新版本 dsh-apes-sidebar / I confirmed I am on the latest dsh-apes-sidebar version required: false - type: dropdown @@ -58,8 +58,8 @@ body: id: plugin-version attributes: label: 插件版本 / Plugin version - description: dsh-better-sidebar 的版本号 | The dsh-better-sidebar version - placeholder: "如 0.13.1(`dsh plugin --profile web add dsh-better-sidebar@latest` 安装的版本)" + description: dsh-apes-sidebar 的版本号 | The dsh-apes-sidebar version + placeholder: "如 0.13.1(`dsh plugin --profile web add dsh-apes-sidebar@latest` 安装的版本)" validations: required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 007eec1..fc46003 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: # config; the runner has none by default. - name: Configure git identity for tests run: | - git config --global user.name "dsh-better-sidebar-ci" + git config --global user.name "dsh-apes-sidebar-ci" git config --global user.email "ci@dsh.invalid" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -84,7 +84,7 @@ jobs: # identity and rely on ambient config, which the runner lacks. - name: Configure git identity for tests run: | - git config --global user.name "dsh-better-sidebar-ci" + git config --global user.name "dsh-apes-sidebar-ci" git config --global user.email "ci@dsh.invalid" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -192,7 +192,7 @@ jobs: run: pnpm test:mount # Aggregate double-mount regression: a fixture aggregate bundle mounts - # dsh-better-sidebar under its own entry id BEFORE the plugin's bundle + # dsh-apes-sidebar under its own entry id BEFORE the plugin's bundle # row; the plugin must back off (no duplicate /sidebar/api, boot OK). # Uses the pinned DSH CLI installed above. - name: Aggregate double-mount regression diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be1389c..36f1498 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ name: Release -# Publishes `dsh-better-sidebar` to npm via npm Trusted Publishing (OIDC) — +# Publishes `dsh-apes-sidebar` to npm via npm Trusted Publishing (OIDC) — # no NPM_TOKEN secret required, provenance is attached to the tarball. # # Trigger: a GitHub Release is published (tag vX.Y.Z). The tag MUST match @@ -12,7 +12,7 @@ name: Release # supports the stable DSH line); stable versions publish as `latest`. # # One-time npm-side setup (manual, on npmjs.com): -# 1. package `dsh-better-sidebar` -> Settings -> Publishing access +# 1. package `dsh-apes-sidebar` -> Settings -> Publishing access # -> Trusted Publishers -> "Add Trusted Publisher". # 2. Provider: GitHub Actions # Organization or user: omdsh-dev @@ -66,7 +66,7 @@ jobs: # (see tests/smoke.spec.ts); the runner has none by default. - name: Configure git identity for tests run: | - git config --global user.name "dsh-better-sidebar-ci" + git config --global user.name "dsh-apes-sidebar-ci" git config --global user.email "ci@dsh.invalid" - name: Install dependencies diff --git a/AGENTS.md b/AGENTS.md index 49b4f25..9e242d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# dsh-better-sidebar 仓库规则(AGENTS) +# dsh-apes-sidebar 仓库规则(AGENTS) > 本文只含**项目全局开发规则**(面向贡献者与 agent)。 > 消费插件接入 API 全参考(`ctx.betterSidebar` 服务、TabDescriptor / FileViewerDescriptor 全字段、声明式设置、原生右侧栏承载面、皮肤契约等)→ [docs/external-plugin-guide.md](docs/external-plugin-guide.md);逐特性设计史(含实施偏差记录)→ [docs/plans/](docs/plans/)。 @@ -17,7 +17,7 @@ ## 2. CI 挂载冒烟(`plugin-mount` job / `pnpm test:mount`) -「npm 打包 → 真实挂载 → 无头渲染」门禁(证明打包产物在真实 DSH 挂载后不 crash):`pnpm build && pnpm pack` 产 tarball → `scripts/e2e-mount.sh` 装进全新 scratch profile(`dsh plugin --profile web add `)并启动真实 `dsh web`(keyless,`--port 0`)→ `tests/e2e/mount.e2e.ts`(Playwright)断言 `[data-dsh-better-sidebar]` 挂载、无错误条/pageerror/console 错误,展开 DSH 原生右侧栏后经其 guide 页逐个打开本插件贡献的 tab 类型**以及宿主自己的终端条目**(必须恰好 1 条——多于 1 条即意味着插件又在遮蔽宿主),再经插件文件树(原生 `files` kind 接管)打开 seed 文件强制加载 editor chunk(`client-editor.js`),并跑 mermaid / README 预览与 sidechat 宿主路由烟测。**`tests/e2e/native-reload.e2e.ts` 是同一 lane 里的第二条门**:它 `utimesSync` 已安装的 `lib/client.js`(**在钉住的宿主 0.1.7-rc.1 上**:HMR 每 500ms stat-poll,rev = `mtime/ctime/size` 的 sha1;DSH 源码树的新版本改成只比 `mtime+size` 并对**内容**取 sha1——pin 上调时这个触发器必须同步复核,否则该用例会红在替换断言上而不是静默失效)触发**页面内条目替换**,断言替换前后都无 `native register … error`、无诊断条、且 `files` 接管重新可用——这是唯一能在部署层复现「teardown 期间注册把 id 孤儿化」(见 §3.4 第 9 条)的用例,v0.22.0 上连续 3 次运行全红、修复后连续 3 次全绿。**`browser` 不在这个清单里,而是显式断言缺席**:DSH 0.1.7 的 `ui-sidebar-browser` 只在 desktop profile 挂载(`web-app` 的 patch 里 `disabled: ctx.get('profileContext')?.name !== 'desktop'`),lane 全走 web profile,钉住「没有」比默默放过更能暴露宿主立场的变化。 +「npm 打包 → 真实挂载 → 无头渲染」门禁(证明打包产物在真实 DSH 挂载后不 crash):`pnpm build && pnpm pack` 产 tarball → `scripts/e2e-mount.sh` 装进全新 scratch profile(`dsh plugin --profile web add `)并启动真实 `dsh web`(keyless,`--port 0`)→ `tests/e2e/mount.e2e.ts`(Playwright)断言 `[data-dsh-apes-sidebar]` 挂载、无错误条/pageerror/console 错误,展开 DSH 原生右侧栏后经其 guide 页逐个打开本插件贡献的 tab 类型**以及宿主自己的终端条目**(必须恰好 1 条——多于 1 条即意味着插件又在遮蔽宿主),再经插件文件树(原生 `files` kind 接管)打开 seed 文件强制加载 editor chunk(`client-editor.js`),并跑 mermaid / README 预览与 sidechat 宿主路由烟测。**`tests/e2e/native-reload.e2e.ts` 是同一 lane 里的第二条门**:它 `utimesSync` 已安装的 `lib/client.js`(**在钉住的宿主 0.1.7-rc.1 上**:HMR 每 500ms stat-poll,rev = `mtime/ctime/size` 的 sha1;DSH 源码树的新版本改成只比 `mtime+size` 并对**内容**取 sha1——pin 上调时这个触发器必须同步复核,否则该用例会红在替换断言上而不是静默失效)触发**页面内条目替换**,断言替换前后都无 `native register … error`、无诊断条、且 `files` 接管重新可用——这是唯一能在部署层复现「teardown 期间注册把 id 孤儿化」(见 §3.4 第 9 条)的用例,v0.22.0 上连续 3 次运行全红、修复后连续 3 次全绿。**`browser` 不在这个清单里,而是显式断言缺席**:DSH 0.1.7 的 `ui-sidebar-browser` 只在 desktop profile 挂载(`web-app` 的 patch 里 `disabled: ctx.get('profileContext')?.name !== 'desktop'`),lane 全走 web profile,钉住「没有」比默默放过更能暴露宿主立场的变化。 本地:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`(`DSH_CMD` 未设时 `scripts/e2e-common.sh` 回退的 npx 钉版是 `DSH_NPX_SPEC`,默认 `@deepseek-ai/dsh@0.2.0-rc.1`——**不钉就会解析 `latest`,而 `latest` 仍是 0.1.7-rc.2**;本机 pnpm 的 registry 是 npmmirror,新预发布常滞后,装不到时用 `pnpm install --registry https://registry.npmjs.org` 一次性覆盖)。CI 钉 `@deepseek-ai/dsh@0.2.0-rc.1`(**pin 一律写精确版本号,不要写 dist-tag**:0.1.7-rc.1 上线时 `alpha` 指的是 **0.1.7-alpha.2**,而 0.2.0-rc.1 只挂在 `next` 上;peer 下限 `^0.2.0-rc.1`)。该步骤用 `NODE_OPTIONS=--max-old-space-size=4096`:钉版自身的传递依赖是浮动 `^` 范围,上游分阶段发布预发布版时(rc.2 于 2026-09-10 的 14:43–14:57 逐包上线)npm 会组出混合 peer 图,3062 条 ERESOLVE 后 OOM(exit 134);另一个失败模式是对未发布兄弟包 ETARGET,靠「钉一个已完整发布的版本」修掉。**不要加 `--legacy-peer-deps`**:它跳过的正是全局安装必须提供的 peer,`@deepseek-ai/dsh-app-boot` 在 boot 时 require 的 `@deepseek-ai/cordis-plugin-group` 是 peer 而非 dependency,加了它 CLI 直接 `ERR_MODULE_NOT_FOUND`(实测过一次,见 rc.2 计划 C 节)。`ci-windows` 的 `Test` 跑 `pnpm test:windows`(`--maxWorkers=1`)而非 `pnpm test`——多个 spec 真起进程(`smoke` / `install-powershell` / `git`),2 核 runner 上并行起 ConPTY / 冷启 `powershell.exe` 是超时与 worker 静默死亡的放大器;`vitest.config.ts` 的全局 `testTimeout: 15_000`(默认 5000 在 Windows 上对真起进程的用例太低,三个不同文件先后翻车)是配套的一半。e2e spec 命名 `*.e2e.ts` + vitest `exclude` 双保险;**改 `exclude` 必须保留默认排除项**(exclude 整体替换默认值)。 @@ -48,13 +48,13 @@ 1. **`dsh-settings` 整体重写**(`packages/settings/settings`):`SettingsProvider.register(ns, schema)` 被删除,只剩 `SettingsForms` 的 `describe/update/replace/mutate/configure`;**命名空间就是插件 Loader 行的 `entry.options.id`**(不再是插件自选的字符串),schema 来自 `entry.fiber.runtime.Config`,且 `describe()` 只列出「有 `Config` 且 fiber 处于 ACTIVE」的条目。本插件的适配: - `src/config.ts` 把用户偏好并进导出的 `Config`(原先只是 4 个部署限额),并把每个偏好字段收集为 volatile schema。**这一个标记就是「改设置不重挂插件」的全部机制**:Loader 的 `equalExceptVolatile` 跳过 volatile 字段,纯 volatile 变更走 `_commitVolatile` 提交进运行中的引用并发 `loader/volatile-update`,而普通字段(4 个限额)保持原来的重挂语义。**必须用 DSH 自己的 schemastery(`@deepseek-ai/schemastery`),这是本版最容易踩空的一处**。`schemastery` 的 `meta.volatile` 只是标记;DSH 的构建额外在**解析期把 volatile 字段包成 cosmokit `Volatile` 引用**(`createVolatile`),而 Loader 的 `_commitVolatile` 正是走 `volatileEntries`/`updateVolatile` 去提交那些引用。用公开的 `schemastery` 包:写入**报告成功、revision 也涨**,但有效值永远不变——静默丢弃每一次改设置(真机实测:`settings.update` 200 + `revision: 1`,而 `describe().value` 仍是旧值)。另一个同等致命的坑:`.volatile()` 走 `extra()`,**返回的是副本而不是原地修改**,`for (const f of ...) f.volatile()` 会把标记丢掉,随后宿主报 `Plugin entry "better-sidebar" has no volatile fields`;必须收集返回值。 - - `src/index.ts` 的 `ownEntryId(ctx)` **运行时自发现**自己那一行的 id(按 `options.name === 'dsh-better-sidebar'` + `entry.fiber === ctx.fiber` 匹配,回退到第一个启用的同名行),**不允许硬编码 `better-sidebar`**:聚合包会用 `web-ui-better-sidebar` 之类的 id 挂同一个包。 + - `src/index.ts` 的 `ownEntryId(ctx)` **运行时自发现**自己那一行的 id(按 `options.name === 'dsh-apes-sidebar'` + `entry.fiber === ctx.fiber` 匹配,回退到第一个启用的同名行),**不允许硬编码 `better-sidebar`**:聚合包会用 `web-ui-better-sidebar` 之类的 id 挂同一个包。 - `viewOf()` = `describe({redactSecrets:true}).find(d => d.ns === ns)`;写入 = `update(ns, patch, expectedRevision)`(`SettingsConflictError` 仍导出、构造签名未变,409 分支保留)。 - `ctx.effect(() => settings.configure({ auto: false }, ctx.fiber))` —— 官方 README 给「自带设置页的插件」的写法;不这么做 Settings 会把同一批字段渲染两遍。 - **`settings/document-updated` 监听不到,不要写**:它在 settings 服务自己的 context 上 `emit`,而 cordis 的事件只向上冒泡到该 ctx 的祖先,插件的 fiber 是兄弟。本插件改用「客户端每次重新读取表单时重新求值」覆盖同样的触发点(`settingsFace.get()` 里跑门控同步)。 2. **`ui-primitives` 图标整族改名**:`Icon<14|16>` → `IconRegular`(1px 描边)/ `IconMedium`(1.3px)。0.1.6 的 81 个 `export const Icon*` 在 0.1.7 变成 184 个,**旧的尺寸后缀名一个不剩**(`git grep -c Outline16 -- packages/client/ui-primitives/src` = 0)。插件 25 个 icon 具名导入消失、17 个文件受影响。**失败模式是 render 期而不是 load 期**:插件 client bundle 是 CJS + external,产物是裸属性读,缺导出 = `undefined` → React 抛 `Element type must be invalid`,不是 ESM link error。**改名必须同时钉回尺寸**:0.1.6 的 `IconXxx14` 默认 `size = 14`,0.1.7 的 `XxxRegular` 一律默认 16,所以原先依赖 14px 默认值的 5 处(`TabBar.tsx` / `Sidebar.tsx` / `FileTree.tsx` 的 `IconCloseFillRegular`、`SubagentView.tsx` 两处 `IconRefreshOutlineRegular`)必须显式传 `size={14}`,否则**静默变大 2px**。另有 `StateDot` 的 ongoing 态被整图重画(10px 像素矩阵 → 14px 圆环 spinner,默认尺寸 `10 → 14`),签名兼容故无编译错误;插件选择跟随宿主观感。**rc.1 复查:`icons/index.tsx` 只新增了 `IconUsersOutline{Artwork,Regular,Medium}`,无删除、无改名,也没有任何既有 icon 的默认尺寸变化**(`export const` 187 → 189;`icons/props.ts` 与 `shared-artwork.tsx` 逐字未变)——上面这轮改名是 alpha.1 的历史账,不是 rc.1 的。**计数更正**:插件实际是 **25 个 icon 具名导入 + 1 个 type-only `IconProps`**(`tests/primitives-exports.spec.ts` 会实时提取导入面并与装好的包对照,数字写错它会先红)。 3. **会话格式 v3 → v4**(`packages/session/session-format-v3-to-v4`):两处变化。 - - **`source.kind === 'plugin'` 被硬拒**(`message-sources.ts` 抛 `format v4 message requires a producer-owned source kind`),而插件的 sidechat 边界注入正是那个形状。改用 **`plugin:dsh-better-sidebar`**:与 DSH 自己的 v3→v4 迁移对历史行产出的 kind **逐字一致**(`producerKind()` 对未知插件名返回 `` `plugin:${plugin}` ``),所以新旧数据形状统一,**历史 sidechat 会话不会丢**。类型层面还需一条 `declare module '@deepseek-ai/dsh-llm'` 的 `MessageSourceMap` 增强(0.1.7 把它改成合并可扩展的封闭联合,没有共享 catch-all)。 + - **`source.kind === 'plugin'` 被硬拒**(`message-sources.ts` 抛 `format v4 message requires a producer-owned source kind`),而插件的 sidechat 边界注入正是那个形状。改用 **`plugin:dsh-apes-sidebar`**:与 DSH 自己的 v3→v4 迁移对历史行产出的 kind **逐字一致**(`producerKind()` 对未知插件名返回 `` `plugin:${plugin}` ``),所以新旧数据形状统一,**历史 sidechat 会话不会丢**。类型层面还需一条 `declare module '@deepseek-ai/dsh-llm'` 的 `MessageSourceMap` 增强(0.1.7 把它改成合并可扩展的封闭联合,没有共享 catch-all)。 - **tool 结果消息重写**:`role: 'user'` + 嵌套 `type: 'tool-result'` 包装块 → `role: 'tool'` + **顶层** `content` / `isError` / `toolCallId`。插件的 4 个解析器原先靠 `candidate.type === 'tool-result'` 命中,在新形状下恒返回空串(**静默空白**)。现在四处都**同时接受新旧形状**(旧分支必须保留:历史日志里就是旧形状)。 4. **客户端会话快照换字段**:`SessionListState.subagentsByParent` / `jobsBySession` 被 `projectionsBySession` 取代,且 **`setSubagentCatalogOpen` 这个按需观察 API 被整体删除**(0.1.7 在连接建立时一次性加载所有 Session 的 projection,所以那套 observe/unobserve 机制应当删除而不是改名)。子代理目录改读 `projectionsBySession[sid]?.values.subagentCatalog`(`{id, createdAt, mode: 'one-shot'|'continuable'|'unknown', label?}`,**注意 `unknown` 是新值**);后台任务列表**没有替代字段**,但 0.1.7 的 web profile 会挂载**客户端** `ctx.jobs` 服务(`@deepseek-ai/dsh-api-job-controller/client`,`web-app/cordis.patch.yml`),任务页直接读它——`state.getSnapshot()/subscribe()` 是整份快照(`{rows: 按被观察会话的 roster, observed: 按任务 id 的保留输出}`),`watchRows(sessionId)` 是按会话引用计数的推送 roster(空集合即该会话无任务),`observe(sessionId|undefined, jobId)` 是**非消费**的输出流(返回释放函数),`kill(sessionId, jobId)` 是注册表准入。插件因此**删掉了自建的 `jobs.list` / `jobs.output` / `jobs.kill` 三条路由**(`src/jobs-routes.ts` 已不存在),也不再用事件回放读模型已读的输出:那个 pane 从诞生起就受「不能碰模型 `job_output` 游标」的约束,宿主服务把这条约束变成了能力。同一版里 `ctx.jobs.list/get/read/kill` 的 caller 从 `Agent` 变成 `SessionId`(插件因此可以彻底不碰 `ctx.agents` 来调注册表)。 @@ -86,7 +86,7 @@ 6. **`@deepseek-ai/dsh-client-runtime` 包已消亡**(继任 seed 是裸名 `dsh-client-store`,无 `/client` 子路径):peerDependencies、devDependencies、`dsh.client.inject`、chunk externals 白名单(`src/client/chunk-loader.ts` / `tsdown.config.ts` / `tests/chunk-loader.spec.ts` / `tests/manifest-consistency.spec.ts` 四处同步)均已无该条目。 7. **e2e scratch profile 的 `minimumReleaseAgeExclude` 含 `'@deepseek-ai/*'`**(`scripts/e2e-mount.sh` / `e2e-aggregate-mount.sh`,与仓库根 `pnpm-workspace.yaml` 同策):alpha 版本常在发布后 24h 内跑 lane,pnpm 11 的 `minimumReleaseAge` 默认会拒装新鲜包。 8. **插件开发树的 dsh-* 传递 peer 需提升为 devDependencies**(alpha.3 首见):`dsh-subagent` 等 npm 包把 `dsh-attachment` 等 dsh-* 姊妹包声明为 peerDependencies(由宿主 bundle 树统一提供,宿主侧无此问题),插件仓库若只直接依赖其中一部分,其余 peer 在 pnpm 下会解析到树上残留的旧版——如 `dsh-attachment@0.1.1-rc.1` 缺 `admitPromptContent` 导出、`dsh-subagent` 产物 import 它时测试加载即崩。因此 devDependencies 需涵盖 dev 树实际触达的全部 peer(attachment / scope / session-projection / system-prompt / user-approval / util-time 等,与直接依赖同款精确钉版);**`@deepseek-ai/cordis` peer 自 alpha.3 起要求 `^4.0.2`,0.1.7-alpha.1 要求 `^4.0.3`,rc.1 起要求 `~4.0.4`**(rc.1 顺手把 vendor 一起抬了:cordis 4.0.4 / schemastery 3.18.4 / cordis-plugin-group 1.0.4 / cordis-plugin-include 1.0.9;loader 保持 1.0.5)。**rc.1 实测的教训**:只改 `dependencies` 里的 schemastery 和 peer 里的 cordis **不够**——`cordis-plugin-group` / `-include` 是 `dsh-app-boot` / `dsh-config-editor` 的**传递 peer**,pnpm 会继续沿用 lockfile 里的旧副本,`pnpm peers check` 一次报出四类 unmet peer;按本条把四个 vendor 包**显式钉进 devDependencies** 才收敛(这也是 rc.1 那条「DSH 内部引用一律精确版本」的 workspace-release-ranges 政策的直接后果)。适配新版本时先跑 `pnpm peers check`,把新失配的传递 peer 一并提升进 devDependencies。**0.1.7 新增的同类问题**见 §3.3 第 12 条(`ui-primitives` 的第二层裸 import)。例外:**`dsh-client-locale` 的 peer/devDep 允许落后于基线**的历史已消除,rc.1 起上游恢复发版,0.1.7 起与 DSH 同 tuple。 -9. **右侧栏是 DSH 原生栏,插件只提供 tab 类型**:聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`。插件的每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`,`extension` 带)+ 原生 tab 体(`sidebar.right.pane.tab` keyed 槽,key = `dsh-better-sidebar:`);`editor` 类型同时认领 `dsh-resource://file/**`(**但按 §3.2 第 5 条的清单让出一部分**),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件文件树,注销即复位)。`ctx.sidebarRight.openTab/openResource/close` 只对**在屏会话**写入;跨会话用具体类上的 `openTabIn/openResourceIn/closeIn`。原生 tab 的插件侧状态在 `src/client/native/tab-adapter.tsx`。**注册必须等服务、不能等槽声明**:原生栏先声明 `sidebar.right.pane.tab` 再 `provide('sidebarRightTabs')`,真机 profile 上槽声明回调里 `ctx.get('sidebarRightTabs')` 仍是 undefined(实测 3 秒后才出现),按槽触发注册会静默什么都不注册且永不重试——用 `ctx.inject(['sidebarRightTabs'], cb)` 驱动类型注册(`tests/native-surface.spec.ts` 以「槽先触发、服务后到达」的顺序守护)。**0.1.6-alpha.2 起**:`SidebarRightGuideEntry.id` 必填且同 provider 内唯一(缺它会静默清空整个承载面);同 provider 的 guide 条目是**追加**关系,`chain` 那种「替换宿主条目」的语义只存在于 `conversation.chat.turnTail` 且已被上游改成 `list`。**0.1.7 起**:`sidebar-right` 本体的契约**零破坏**——槽名无改名、`SidebarRightTabDefinition` 没有新的必填字段、只有新增的可选 `keepMounted` 与 `ISidebarRight.mounted`,`openTabIn/openResourceIn/closeIn` 在具体 class 上字节不变;`tab.visible` 语义收紧为 `active && (...)`(全局面板/后台会话时 body 不挂载);持久化校验收紧(非根 / 列轴 / 嵌套 / 3+ docked pane 的 split 一律判非法**整份丢弃**,0.1.6 的 sidebar 自己产不出这种布局);`RightbarRoot` 现在为每个保留会话渲染一个隐藏 seat(`SidebarSessionViews`,未导出、插件拿不到)。原生 tab 体宿主 `.paneBody` 是**有确定高度的块级滚动容器**(非 flex 容器),native 适配层因此给每个 tab 体包一层 `height:100%` 的列 flex 宿主(`sidebar.module.css` 的 `.nativeTabHost`),tab 组件根继续用 `flex:1`/`height:100%`。全局面板(根级 `main` keyed 槽 + `sidebar.panellist` + `ctx.layout.selectPanel/beginNavigation`、`rightbar` 改根级并新增 `rightbar.session`)插件**不接入**,只做兼容。底部工作台的中心列锚点认 `[data-slot="main.conversation"]`(并跳过 `display: contents` 祖先,同时保留 alpha.1 的 `[data-slot="conversation"]`);文件地址语法跟随 alpha.2(`fileAddressFor` 一律 session 作用域、绝对路径保留前导 `/`、`parseFileAddress` 前缀解析并去 `?`/`#`)。**原生指南说明的 4 条上限(归属更正:这是 0.1.7-alpha.1 起就存在的既成事实,不是 rc.1 的变更)**:`ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx` 的 `MAX_DESCRIBED_ENTRIES = 4` 及其判定行在 alpha.1 / alpha.2 / rc.1 三个 tag 上**逐字相同**,宿主终端的 guide 条目(`ui-sidebar-terminal/src/client/TerminalGuide.tsx`)也是 alpha.1 就有了,`ui-sidebar-terminal/src` 在 rc.1 **零改动**。含义不变:条目 ≤ 4 条才渲染说明,更长的列表是整列丢弃而不是截断——插件默认贡献 4 个 guide 条目(文件 / 文件变动 / 任务管理 / 侧边对话),与宿主的终端条目合计后可能超过 4 条,此时说明不渲染;插件**不恢复**旧的 `nativeGuideDesc` 通用兜底句。**原生注册账本的两条不变量(v0.22.1 起,由 `tests/native-registration.spec.ts` 守护)**:① `live` 里同时装着描述符类型(键 = descriptor id)与 `files` 接管(键 = `FILES_KIND`,**永远不在 `wanted` 里**),清理循环必须**跳过 `FILES_KIND`**——它只由编辑器类型的 `wantsFiles/hasFiles` 开关分支与 seat disposer 管理;否则每次通知都会把它拆掉再重建,而 teardown 期间的那次重建会把 id 孤儿化:`tabs.register` 建在**宿主** ctx 上(宿主 ctx 此时仍活着)所以照样成功,紧随的 `ctx.slots.inject` 建在**插件** ctx 上却抛 `cannot create effect on inactive context`,`live.set` 永不执行 → 该 id 在整页生命周期内不可再注册(真实表现:`native register files error: … already registered` 刷屏 + 文件树落到宿主空态,只有刷新页面才恢复;官方桌面壳日志实证见 [docs/plans/2026-09-28-native-files-takeover-reload-leak.md](docs/plans/2026-09-28-native-files-takeover-reload-leak.md))。② 因此「注册成功一半」是**可达状态**:任何在宿主取走 id **之后**失败的注册(槽位注册抛错等)必须**回滚释放该 type**、并释放已经建好的槽位——插件侧只留一个下次通知可以重试的失败,绝不留下无处释放的 id。相关断言必须写在注册表的**事件日志**上而不是「活着的 id 集合」上:孤儿 id 会让键集合看起来完全正常,键集合断言在未修复代码上同样通过。 +9. **右侧栏是 DSH 原生栏,插件只提供 tab 类型**:聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`。插件的每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`,`extension` 带)+ 原生 tab 体(`sidebar.right.pane.tab` keyed 槽,key = `dsh-apes-sidebar:`);`editor` 类型同时认领 `dsh-resource://file/**`(**但按 §3.2 第 5 条的清单让出一部分**),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件文件树,注销即复位)。`ctx.sidebarRight.openTab/openResource/close` 只对**在屏会话**写入;跨会话用具体类上的 `openTabIn/openResourceIn/closeIn`。原生 tab 的插件侧状态在 `src/client/native/tab-adapter.tsx`。**注册必须等服务、不能等槽声明**:原生栏先声明 `sidebar.right.pane.tab` 再 `provide('sidebarRightTabs')`,真机 profile 上槽声明回调里 `ctx.get('sidebarRightTabs')` 仍是 undefined(实测 3 秒后才出现),按槽触发注册会静默什么都不注册且永不重试——用 `ctx.inject(['sidebarRightTabs'], cb)` 驱动类型注册(`tests/native-surface.spec.ts` 以「槽先触发、服务后到达」的顺序守护)。**0.1.6-alpha.2 起**:`SidebarRightGuideEntry.id` 必填且同 provider 内唯一(缺它会静默清空整个承载面);同 provider 的 guide 条目是**追加**关系,`chain` 那种「替换宿主条目」的语义只存在于 `conversation.chat.turnTail` 且已被上游改成 `list`。**0.1.7 起**:`sidebar-right` 本体的契约**零破坏**——槽名无改名、`SidebarRightTabDefinition` 没有新的必填字段、只有新增的可选 `keepMounted` 与 `ISidebarRight.mounted`,`openTabIn/openResourceIn/closeIn` 在具体 class 上字节不变;`tab.visible` 语义收紧为 `active && (...)`(全局面板/后台会话时 body 不挂载);持久化校验收紧(非根 / 列轴 / 嵌套 / 3+ docked pane 的 split 一律判非法**整份丢弃**,0.1.6 的 sidebar 自己产不出这种布局);`RightbarRoot` 现在为每个保留会话渲染一个隐藏 seat(`SidebarSessionViews`,未导出、插件拿不到)。原生 tab 体宿主 `.paneBody` 是**有确定高度的块级滚动容器**(非 flex 容器),native 适配层因此给每个 tab 体包一层 `height:100%` 的列 flex 宿主(`sidebar.module.css` 的 `.nativeTabHost`),tab 组件根继续用 `flex:1`/`height:100%`。全局面板(根级 `main` keyed 槽 + `sidebar.panellist` + `ctx.layout.selectPanel/beginNavigation`、`rightbar` 改根级并新增 `rightbar.session`)插件**不接入**,只做兼容。底部工作台的中心列锚点认 `[data-slot="main.conversation"]`(并跳过 `display: contents` 祖先,同时保留 alpha.1 的 `[data-slot="conversation"]`);文件地址语法跟随 alpha.2(`fileAddressFor` 一律 session 作用域、绝对路径保留前导 `/`、`parseFileAddress` 前缀解析并去 `?`/`#`)。**原生指南说明的 4 条上限(归属更正:这是 0.1.7-alpha.1 起就存在的既成事实,不是 rc.1 的变更)**:`ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx` 的 `MAX_DESCRIBED_ENTRIES = 4` 及其判定行在 alpha.1 / alpha.2 / rc.1 三个 tag 上**逐字相同**,宿主终端的 guide 条目(`ui-sidebar-terminal/src/client/TerminalGuide.tsx`)也是 alpha.1 就有了,`ui-sidebar-terminal/src` 在 rc.1 **零改动**。含义不变:条目 ≤ 4 条才渲染说明,更长的列表是整列丢弃而不是截断——插件默认贡献 4 个 guide 条目(文件 / 文件变动 / 任务管理 / 侧边对话),与宿主的终端条目合计后可能超过 4 条,此时说明不渲染;插件**不恢复**旧的 `nativeGuideDesc` 通用兜底句。**原生注册账本的两条不变量(v0.22.1 起,由 `tests/native-registration.spec.ts` 守护)**:① `live` 里同时装着描述符类型(键 = descriptor id)与 `files` 接管(键 = `FILES_KIND`,**永远不在 `wanted` 里**),清理循环必须**跳过 `FILES_KIND`**——它只由编辑器类型的 `wantsFiles/hasFiles` 开关分支与 seat disposer 管理;否则每次通知都会把它拆掉再重建,而 teardown 期间的那次重建会把 id 孤儿化:`tabs.register` 建在**宿主** ctx 上(宿主 ctx 此时仍活着)所以照样成功,紧随的 `ctx.slots.inject` 建在**插件** ctx 上却抛 `cannot create effect on inactive context`,`live.set` 永不执行 → 该 id 在整页生命周期内不可再注册(真实表现:`native register files error: … already registered` 刷屏 + 文件树落到宿主空态,只有刷新页面才恢复;官方桌面壳日志实证见 [docs/plans/2026-09-28-native-files-takeover-reload-leak.md](docs/plans/2026-09-28-native-files-takeover-reload-leak.md))。② 因此「注册成功一半」是**可达状态**:任何在宿主取走 id **之后**失败的注册(槽位注册抛错等)必须**回滚释放该 type**、并释放已经建好的槽位——插件侧只留一个下次通知可以重试的失败,绝不留下无处释放的 id。相关断言必须写在注册表的**事件日志**上而不是「活着的 id 集合」上:孤儿 id 会让键集合看起来完全正常,键集合断言在未修复代码上同样通过。 10. **自定义种子必须带 fork 标记对(`meta.isSeeded: true` + `inheritedEventCount`)**:dsh-session 契约「只给 seed 不标 isSeeded,种子算重放历史而非继承前缀」——缺标记时 `Session.ownEvents()` 含整个种子,子会话的 inbox 折重放种子里的 `agent/inbox/spliced`,**继承父会话切割时刻未领取的 inbox 输入**。宿主 `session.fork`(api-session-controller)的调用即规范形态;回归由 `tests/sidechat-seed-validation.spec.ts`(真实 `Session.create` + 对 `ownEvents()` 跑 inbox 折)守护。**0.1.7 复查:`Session.create` 的五个参数与函数体逐字相同**;上游新增了 `buildForkSeed(events, boundary)` 导出,与本插件手工的 `buildSidechatInheritance` 职责重叠但**收益不明确**(插件还有 dangling-tool-call 退回结构化快照的自有需求),暂不替换。 11. **`dsh.plugin.json` 从来不是 DSH 契约**:0.1.7 全树 grep 仍 **0 命中**。DSH 真正读的是 `package.json` 的 `dsh.bundle.patch` + `dsh.client`(`platform`/`inject`/`immediately`/`external`)+ `exports["./client"]`。`dsh.plugin.json` 只服务第三方 registry(社区市场)通道,`lib/client-registry.js` 同理;`tests/manifest-consistency.spec.ts` 守护的是这条通道的形状,不是 DSH 契约。**`dsh.manifestVersion` 也仍是「只声明、零读」**(0.1.7 的 `package-manifest` README 自己写明「Current installers and loaders do not enforce `dsh.manifestVersion` or `engines.dsh`」)。0.1.7 的官方管控面新增:`plugin-manager` 的**多 registry 回退**(`registryPlan()`:配置源 → `fallbackRegistries`(默认含 `registry.npmmirror.com`),`network`/`timeout`/`not-found`/`no-matching-version` 会自动问下一个源;`registry` 入参只存在于 Web 对话框与 `plugin_manager` 工具,**CLI 没有 registry flag**)、`waitForInstall`、以及**插件展示元数据**:`package.json#icon`(36×36、纯填充无描边、**双色渐变**——与 DSH 自带的 `PluginArtwork*` 同风格:它们「carry their own brand colors instead of riding currentColor」,且每个都是多色,单色版在 30px 行内尺寸下会读成一块纯色板)+ `locale/.json` 的 `meta.title`/`description`。本插件**已采纳**(v0.21.1):`icon.svg` + `locale/en.json` + `locale/zh.json`,并由 `tests/plugin-meta.spec.ts` 守护「声明了就必须发布出去」。**rc.1 复查:`packages/boot/app-boot/src/package-meta.ts` 与 `scripts/verify-package-meta.ts` 在两个 tag 上逐字相同**(`ui-plugin-manager/src/client/presentation.ts` 只多了一段 `incompatible-version` 文案),所以上面三条硬规则与「**没有任何尺寸/渐变校验**」都仍然成立。三条硬规则:icon 必须是**包内相对路径**且写进 `files`;`locale/en.json` 是发现基线(缺它整个目录都不读);`exports` 必须暴露 `./locale/*.json` 与 `./package.json`。**所有失败都是静默的**——元数据有问题只会退化成一行纯文字,没有任何报错。`dsh.bundle.patch` 现在多了形状校验(非法值会 `throw`,引导期跳过该 bundle),本插件是合法的单字符串。 12. **peer 声明是启动期硬约束(rc.1 起;本版最容易静默失效的一处)**:`packages/boot/app-boot/src/plugin-compatibility.ts`(rc.1 **新文件**)只做一件事——遍历 profile 行的 `peerDependencies`,对**名字为 `@deepseek-ai/dsh` 或以 `@deepseek-ai/dsh-` 开头**的条目跑 `semver.satisfies(宿主版本, range, { includePrerelease: true })`,不满足就把整行 `row.disabled = true`(group 行连 `group` 一起关掉),只在 stderr 留一行 `disabling profile plugin