Baseline moves to DSH 0.1.5-rc.1 — the 0.1.5 line's first release candidate, and the npm `latest` dist-tag now: peer floor ^0.1.5-rc.1, devDependency pins, the CI mount-lane pin and dsh.plugin.json engines all follow. 0.1.5-alpha.2 users stay on v0.19.0-alpha.1. The upstream delta from alpha.2 is small: 373 changed files, mostly version bumps, sidebar preview polish and test snapshots. Exactly two changes touch this plugin, and only one needs work: - `SidebarRightGuideEntry.description` is back (optional). The native guide renders it only while it lists at most 4 entries; a longer list drops every description. The plugin restores `TabDescriptor.description` and the six built-in `guideDesc*` lines (20 dictionaries) verbatim from 0b6ac5d^, but deliberately does NOT restore the old generic `nativeGuideDesc` fallback: the host has no fallback of its own, so a descriptor that declares none now ships no `description` field at all and renders as a clean title-only capsule. Consequence worth knowing: the plugin contributes six guide entries, so the default composition is over the 4-entry limit and shows titles only. The mount lane proves the restored field end to end by shrinking the enabled set to three types through the plugin's own settings route (scratch profile only) and requiring the Files capsule to grow its line. - The built-in `files` type declares a guide description again and uses a coloured folder glyph; the plugin owns that kind, so nothing to do. Verified unchanged from alpha.2 (so nobody re-checks them): the global main-panel model, the file address grammar (util/workspace-path changed only its package.json), the native tab-body host contract (.paneBody is still a block scroller with a definite height), the core/agent/session/ subagent host APIs and the ui-primitives export surface. Restore caveat fixed in the lane: the settings route merges patches key-wise, so undoing a temporary `tabsEnabled` change by posting the original (empty) map leaves the disabled keys behind — the e2e restores every key it touched explicitly.
17 KiB
dsh-better-sidebar 仓库规则(AGENTS)
本文只含项目全局开发规则(面向贡献者与 agent)。 消费插件接入 API 全参考(
ctx.betterSidebar服务、TabDescriptor / FileViewerDescriptor 全字段、声明式设置、原生右侧栏承载面、皮肤契约等)→ docs/external-plugin-guide.md;逐特性设计史(含实施偏差记录)→ docs/plans/。
1. 仓库硬约束(必须遵守)
- 禁止修改 DSH 源码:对官方 checkout(
~/.dsh/source/current)零写入。 - 代码改动必须走 PR:非文档改动在
feat/*/fix/*分支开发,gh pr create发起,review 合并后进 main;仅纯文档改动(README / AGENTS.md / docs/)允许直推 main。 - 挂载只走
cordis.patch.yml+ profile 机制(~/.dsh/profiles/<profile>/),插件作为独立包被 profile 引用,不反向侵入 DSH。 - 市场受管安装约束:
dependencies/peerDependencies/optionalDependencies一律不得出现cordis(按名硬拒,optional 无效),scripts不得含preinstall/install/postinstall/prepare。由tests/market-manifest.spec.ts守护。 - 缺能力时用 DSH 现成只读/公开 API 或插件自有路由(如
jobs.output事件回放:读会话事件日志而非动注册表);做不到先向用户说明取舍,不改 DSH。
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 <tarball>)并启动真实 dsh web(keyless,--port 0)→ tests/e2e/mount.e2e.ts(Playwright)断言 [data-dsh-better-sidebar] 挂载、无错误条/pageerror/console 错误,展开 DSH 原生右侧栏后经其 guide 页逐个打开插件 tab 类型(含终端懒加载 chunk),再经插件文件树(原生 files kind 接管)打开 seed 文件强制加载 editor chunk(client-editor.js),并跑 mermaid / README 预览与 sidechat 宿主路由烟测。
本地:pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount。CI 钉 @deepseek-ai/dsh@0.1.5-rc.1(在 npm 上同时是 latest 与 next;peer 下限 ^0.1.5-rc.1)。e2e spec 命名 *.e2e.ts + vitest exclude 双保险;改 exclude 必须保留默认排除项(exclude 整体替换默认值)。
3. DSH 0.1.5 适配要点(0.1.5-rc.1+ 基线)
v0.19.0(正式版,npm latest)起仅支持 DSH 0.1.5-rc.1+(peer 下限 ^0.1.5-rc.1,CI 钉 @deepseek-ai/dsh@0.1.5-rc.1)。0.1.5-alpha.2 用户停留在 v0.19.0-alpha.1(npm alpha)——rc.1 的 delta 很小,插件不再对 alpha 线做运行时兼容;0.1.2-rc.1 稳定线用户继续用 v0.18.1——0.1.5 的会话事件模型与文件打开漏斗都变了,插件不再对 0.1.2 做运行时兼容。发版:release.yml 按版本号是否含 - 自动选 alpha/latest dist-tag(0.19.0 无后缀 → latest;npm alpha 标签仍指向 0.19.0-alpha.1)。
宿主契约(均经真机挂载冒烟 14/14 验证):
- 一次性 token 鉴权:就绪行
dsh web: http://127.0.0.1:<port>/?token=<43字符>(导航换签名 cookie,干净 URL 401)。e2e-mount.sh的 URL grep 必须延伸到空白([^ ]*,在/截断丢 token);e2e 统一走tests/e2e/host.ts(token 必选:parseLaunchUrl对裸 origin 直接抛错),带 stamps 导航走gotoPage()(先 addCookies 再直达——token 换 cookie 的 303 会丢弃同 URL 其它 query 参数)。插件/sidebar/*路由不受影响,同源 fetch 照旧。 - Remote gateway 斜杠 RPC(唯一方言):
POST /api/workspace/create,payload 恰为{args: {...}},args 按控制器 TS 参数名包装(workspace/create、session/create→{args:{request:{...}}};session/list参数名_request不可省略——{}也被拒args fields do not match the descriptor);envelopemethod与路径一致,点分路径 404。请求由tests/e2e/host-protocol.ts的rpcAttempt构造、tests/e2e-host-protocol.spec.ts锁定;要调新方法先在真机验参数名再进RPC_ARGS_KEY。 MarkdownTextlabels 嵌套契约:必填labels: { code: { copyLabel, copiedLabel }, footnotes }(漏传回退硬编码中文)。四个渲染点(mermaid.tsx / MarkdownHtml.tsx / TextEditor.tsx / SideChatView.tsx)统一走src/client/markdown-labels.tsx的markdownTextProps()。- 侧边对话转录走自有路由,不碰客户端宿主 RPC:
ctx.connection.api(含sessions.history)在 alpha.1 整体移除,继任session/follow|page又对origin:'subagent'会话强制 subagent 地址(普通{kind:'session'}被agent-busy拒)且分页throughSeq不得超当前游标。转录因此由sidechat.events插件路由供给(src/sidechat-routes.ts:live 读agent.session.snapshotEvents()、冷读sessionPersistence.inspect,服务端session/end-seed切割 +afterSeq增量);ctx.connection镜像与 inject 已删。0.1.5 起实时增量不在日志里:assistant/chunk事件被删除,进行中的模型增量改由agent/assistant-stream瞬时帧发布(start/chunk/end,不入会话日志),结算时才落assistant/message(内嵌stream)或assistant/attempt(失败尝试,内嵌stream)。插件在宿主侧src/assistant-live.ts折叠这些帧成有界缓冲,sidechat.events的live字段按「当前 attempt 全量、每次轮询替换」下发(不是增量),转录映射与继承快照都读它。设计见 docs/plans/2026-08-20-sidechat-tab-design.md §10。 dsh-settings无运行时settingsNamespace:命名空间合法性校验转为编译期模板字面量SettingsNamespaceInput(小写字母开头 +[a-z0-9-]尾部),'dsh-better-sidebar'字面量直接过——宿主侧直接传常量(src/index.ts的 settings inject)。dsh-subagent的SUBAGENT_DESCRIPTOR_VERSION2 → 3:sidechat 种子的subagent/descriptor版本由宿主包盖章,插件不硬编码;测试断言跟随常量(tests/sidechat-routes.spec.ts),勿钉字面量。@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四处同步)均已无该条目。- e2e scratch profile 的
minimumReleaseAgeExclude含'@deepseek-ai/*'(scripts/e2e-mount.sh/e2e-aggregate-mount.sh,与仓库根pnpm-workspace.yaml同策):alpha 版本常在发布后 24h 内跑 lane,pnpm 11 的minimumReleaseAge默认会拒装新鲜包。 - 插件开发树的 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 / code-runtime / scope / session-projection / system-prompt / user-approval / util-time 七个即为此提升,与直接依赖同款精确钉版);@deepseek-ai/cordispeer 自 alpha.3 起要求^4.0.2(上游全线 peer 已升)。适配新 alpha 版本时先跑pnpm peers check,把新失配的传递 peer 一并提升进 devDependencies。例外:dsh-client-locale的 peer/devDep 允许落后于基线(alpha.5 时上游停在 0.1.2-alpha.3 未发新版)——peer 下限^0.1.2-alpha.3天然容纳同 tuple 的 alpha.5 运行时,此时保持旧钉版并在pnpm-workspace.yaml注明,待上游发版再追平;rc.1 起上游恢复发版,该包回到与 DSH 同 tuple(0.1.2-rc.1 → 0.1.5-rc.1,peer 下限^0.1.5-rc.1),该例外消除。alpha.2 起还有一类:上游包自身的dependencies丢失——@deepseek-ai/dsh-client-ui-primitives@0.1.5-alpha.2的 manifest 不再声明任何dependencies(alpha.1 声明了 19 个),但lib/*.js仍裸 importanser/shiki/@shikijs/langs/*/mdast-util-*/micromark-*/katex。宿主由预构建前端 bundle 满足,独立安装的 dev/test 树不会——vitest 一碰 primitives 就Cannot find package 'anser'。此时把 alpha.1 声明的同一组版本提升进 devDependencies(clsx已在 dependencies 无需重复)。rc.1 复查:@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1仍不声明任何dependencies,bundle 仍裸 import 同一组包——这组提升不得回退,pnpm peers check/ 单测一旦报Cannot find package 'anser'即是有人回退了。 - 右侧栏是 DSH 原生栏,插件只提供 tab 类型:聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走
ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))(packages/client/ui-chat/src/client/apply.ts是唯一调用点),remote.session.openWorkspacePath在 0.1.5 客户端已无调用者,插件的 openpath 拦截随之删除。插件的每个TabDescriptor注册成原生 tab 类型(kind = descriptor.id,extension带)+ 原生 tab 体(sidebar.right.pane.tabkeyed 槽,key =dsh-better-sidebar:<id>);editor类型同时认领dsh-resource://file/**(压过内置text的fallback),并接管内置files页面 kind(openTab('files')打开插件文件树,注销即复位)。ctx.sidebarRight.openTab/openResource/close只对在屏会话写入;跨会话用具体类上的openTabIn/openResourceIn/closeIn(不在ISidebarRight接口里,需结构化探测),目标会话未挂载时排队到上屏重放(src/client/native/surface.ts)。原生 tab 的插件侧状态(合成SidebarTab、树展开集合、实例编号)在src/client/native/tab-adapter.tsx;原生布局只在内存,不做持久化。注册必须等服务、不能等槽声明:原生栏先声明sidebar.right.pane.tab再provide('sidebarRightTabs'),真机 profile(web,0.1.5-alpha.1)上槽声明回调里ctx.get('sidebarRightTabs')仍是 undefined(实测 3 秒后才出现),按槽触发注册会静默什么都不注册且永不重试——用ctx.inject(['sidebarRightTabs'], cb)驱动类型注册(tests/native-surface.spec.ts以「槽先触发、服务后到达」的顺序守护),槽声明只用来挂 tab 体。指南 §0 列了全部行为差异。alpha.2 起(v0.19.0-alpha.1):指南条目一度改成「图标+标题」胶囊、TabDescriptor.description随之下线,新建标签页的默认页改为从注册表选(恰好 1 个指南条目 → 直接开它,0 或 ≥2 → 开指南),revealIfOpened对「页面」在同一 pane 内强制去重。原生 tab 体宿主.paneBody是有确定高度的块级滚动容器(非 flex 容器),native 适配层因此给每个 tab 体包一层height:100%的列 flex 宿主(sidebar.module.css的.nativeTabHost),tab 组件根继续用flex:1/height:100%——否则根盒塌成内容高度,sidechat 的输入框就贴不到面板底。全局面板(根级mainkeyed 槽 +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前缀解析并去?/#)。rc.1 起(v0.19.0):SidebarRightGuideEntry.description回归(可选),插件恢复TabDescriptor.description(六个内置类型各声明一条说明,guideDesc*词条回到 20 份词典),但宿主的原生指南只在列出的条目 ≤ 4 条时渲染说明(上游MAX_DESCRIBED_ENTRIES = 4;更长的列表是整列丢弃,不是截断)——插件默认贡献 6 个 guide 条目(文件 / 文件变动 / 任务管理 / 侧边对话 / 终端 / 浏览器),所以默认组合下说明不渲染,只有读者在插件设置页关掉足够多 tab 类型、把 guide 压到 ≤ 4 条时才出现;插件不恢复旧的nativeGuideDesc通用兜底句(宿主自己没有兜底,通用句是噪音),没声明说明的 descriptor 就不发description字段;条目缺icon时由宿主补方块占位。 - 自定义种子必须带 fork 标记对(
meta.isSeeded: true+inheritedEventCount):dsh-session 契约「只给 seed 不标 isSeeded,种子算重放历史而非继承前缀」——缺标记时Session.ownEvents()含整个种子,子会话的 inbox 折(inboxProjectionDefinition,0.1.5 起@deepseek-ai/dsh-agent只导出Inbox接口,实现是该投影)重放种子里的agent/inbox/spliced,继承父会话切割时刻未领取的 inbox 输入(排队用户消息 next-turn、长回合中工具结果上下文/steering next-step——后者即「上下文很长时侧边对话先把之前的 User msg 发出去」的根因,幽灵消息排在 boundary 之前最先发给模型)。宿主session.fork(api-session-controller)的调用即规范形态;回归由tests/sidechat-seed-validation.spec.ts(真实Session.create+ 对ownEvents()跑 inbox 折)守护。0.1.5 复评:无更优雅的 sidechat 宿主 API(ContinuableStartSpec仍无 seed 字段),AgentRegistry.create接缝补齐标记即为规范用法;Session.create的 headerversion必须用SESSION_FORMAT_VERSION(0.1.5 是3字面量类型),勿钉0。
4. npm 发版(GitHub Release → npm publish)
.github/workflows/release.yml 在 GitHub Release(tag vX.Y.Z)发布时自动发 npm:
- 前置:
package.json版本 bump 到X.Y.Z,CI 全绿后打 tag;tag 与版本不匹配直接失败。 - 流程:
pnpm build/typecheck/test→ 校验 tag →pnpm publish --provenance --access public。 - 认证:npm Trusted Publishing(OIDC),不配
NPM_TOKEN。一次性配置(npmjs.com package → Settings → Trusted Publishers):ProviderGitHub Actions、Orgomdsh-dev、RepoDSH-better-sidebar、Workflow filenamerelease.yml、Environment 留空。 - 调试:
workflow_dispatch+dry_run=true只打包不发版。
5. 开发规则速查
- 构建纯度门:client bundle 禁止 value-import
@dsh-external/*或非白名单@deepseek-ai/*(tsdown.config.ts拦截);import type {}被擦除不触发——类型可共享,运行时符号不行;跨插件交互走ctx.betterSidebar方法调用。 - 懒加载 chunk:重依赖(xterm/CodeMirror/mermaid)在独立 bundle(
lib/client-<name>.js),经/sidebar/bundle按需下发、globalThis.__dshChunks__物化(src/client/chunk-loader.ts),核心 bundle 禁止静态 importsrc/client/chunks/*。 - i18n:词典在
betterSidebar命名空间,跟随 DSHctx.locale;新增 zh key 必须同步src/client/locales-ja.ts的 ja 翻译(否则 ja 下回退 en)。渲染MarkdownText必须经markdownTextProps()(§3 第 3 条)。 - 皮肤契约:视觉值只消费
--dsw-alias-*/--dsw-font-*/--ds-*令牌,无硬编码颜色;契约全文与 titleBar 四方案模型见指南 §12,改动必须同步该节与tests/theme.spec.ts。 - 契约反向引用:皮肤契约被
src/client/shell-presets.ts、tests/e2e/mount.e2e.ts的注释以「指南 §12」引用——调整指南章节结构时同步检查这两处。 - 接入 API 即文档:
src/client/service.ts与src/client/builtins/的任何行为变更,必须同步 docs/external-plugin-guide.md(唯一权威接入文档,不再双份维护)。
6. 文档与测试地图
- 接入 API 全参考:docs/external-plugin-guide.md(消费插件开发者向;§0 原生栏承载面 / §4 Tab API / §5 FileViewer API / §7 服务方法 / §10 平台陷阱 / §11 已移除的自由窗口 / §12 皮肤契约 / §15 真实案例)。
- 设计文档:docs/plans/(30+ 份逐特性设计,含实施偏差记录)。
- 关键测试守护:
tests/service.spec.ts/builtins.spec.ts(注册表与内置清单:7 tab + 6 viewer)/market-manifest.spec.ts(市场约束)/e2e-host-protocol.spec.ts(RPC 双协议)/native-surface.spec.ts(原生右侧栏承载面)/theme.spec.ts(皮肤契约)/plugin-list.spec.ts(推荐插件目录)/fs-search.spec.ts(host 文件名搜索)。