diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..e740899
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,447 @@
+# dsh-better-sidebar 插件接入文档
+
+> 面向**消费插件开发者**:如何让你的插件向 better-sidebar 注册新的侧边栏页面(tab)和文件类型预览器。
+
+better-sidebar 从 v0.4.0 起暴露 `ctx.betterSidebar` 服务(Cordis context 属性),其他插件通过 `registerTab` / `registerFileViewer` 注册扩展点,返回 disposer 由 Cordis fiber 自动管理生命周期(HMR-safe)。
+
+---
+
+## 1. 服务定位
+
+- **服务名**:`betterSidebar`(即 `ctx.betterSidebar`)
+- **发布侧**:better-sidebar 的 client half(`src/client/index.tsx`,通过 `ctx.provide('betterSidebar', service)` 发布)
+- **消费侧**:你的插件的 client half(`inject = ['betterSidebar', ...]`,然后 `ctx.betterSidebar.registerTab(...)`)
+- **类型合并**:`declare module 'cordis' { interface Context { betterSidebar: BetterSidebarService } }` 由 `dsh-better-sidebar` 包导出;消费插件 `import type {} from 'dsh-better-sidebar'` 即触发类型合并
+
+> ⚠️ **host 半不发布此服务**:`ctx.betterSidebar` 只在 client 侧存在。如果你的插件 host 半需要读 better-sidebar 状态,走 better-sidebar 自己的 HTTP/WS 路由(`/sidebar/api/*`),不走服务。
+
+---
+
+## 2. 消费插件的最小骨架
+
+### 2.1 `package.json`
+
+```jsonc
+{
+ "name": "my-plugin",
+ "peerDependencies": {
+ "cordis": "^4.0.0-rc.7",
+ "dsh-better-sidebar": "workspace:*"
+ },
+ "peerDependenciesMeta": {
+ "dsh-better-sidebar": { "optional": true }
+ }
+}
+```
+
+- `dsh-better-sidebar` 必须声明为 **peerDependency**(不是 dependency,避免重复实例化)
+- 标记 `optional: true` 让你的插件在 better-sidebar 未安装时也能加载(注册代码会因为 `ctx.betterSidebar` 为 undefined 而跳过)
+
+### 2.2 client half 入口
+
+```ts
+// my-plugin/src/client/index.ts
+import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并
+
+export const inject = ['betterSidebar', 'slots'] // 声明服务依赖
+
+export function apply(ctx: Context): void {
+ // 注册一个 sidebar tab
+ ctx.effect(() =>
+ ctx.betterSidebar.registerTab({
+ id: 'my-plugin:db',
+ title: () => 'Database',
+ icon: ,
+ order: 50,
+ component: ({ ctx, scope, tab }) => ,
+ })
+ )
+
+ // 注册一个文件预览器
+ ctx.effect(() =>
+ ctx.betterSidebar.registerFileViewer({
+ id: 'my-plugin:csv',
+ exts: ['csv'],
+ fetchStrategy: 'custom',
+ load: async (path, scope) => parseCsv(await fetchCsvBytes(scope, path)),
+ component: ({ customData, path }) => ,
+ })
+ )
+}
+```
+
+> ⚠️ **构建期纯度门**:client bundle 禁止 value-import 别的插件代码(`tsdown.config.ts` 的纯度门会挡)。`import type {}` 会被擦除,**不触发门禁**——所以类型可以自由共享,运行时符号不行。所有运行时交互必须走 `ctx.betterSidebar` 的方法调用。
+
+### 2.3 类型导入
+
+```ts
+import type { TabDescriptor, FileViewerDescriptor, BetterSidebarService } from 'dsh-better-sidebar'
+```
+
+类型定义在 `lib/types/client/service.d.ts`,通过 `package.json` 的 `./client/service` exports 子路径暴露。
+
+---
+
+## 3. Tab 注册 API
+
+### 3.1 `TabDescriptor` 完整字段
+
+```ts
+interface TabDescriptor {
+ /** 唯一 id;也是 SidebarTab.type 的值。建议带包前缀:'my-plugin:db'。 */
+ id: string
+ /** 标题(i18n 友好:传字符串或返回字符串的函数) */
+ title: string | (() => string)
+ /** 图标:ReactNode 或 (size: number) => ReactNode */
+ icon?: ReactNode | ((size: number) => ReactNode)
+ /** + 菜单排序(升序);默认 100。内置:explorer=10, git=20, subagent=30, terminal=40 */
+ order?: number
+ /** 从 + 菜单隐藏(editor/diff 用:由其他流程触发打开,不在菜单里) */
+ hidden?: boolean
+ /** + 菜单禁用判定(如 terminal 配额满) */
+ available?: (state: SidebarState) => boolean
+ /**
+ * 去重键:openTab 时若已存在 dedupeKey 相同的 tab,则聚焦而非新开。
+ * 返回 undefined 表示不去重(每次都新开)。
+ * 内置策略:explorer/git/subagent 用 () => id(单实例);editor 用 tab => tab.path;diff 用 tab => tab.id。
+ */
+ dedupeKey?: (tab: SidebarTab) => string | undefined
+ /**
+ * 自定义 tab 创建(minting SidebarTab + 状态 patch)。
+ * 返回 null 拒绝创建。terminal 用它生成 terminal: id 并递增 nextTerminal。
+ * 省略时用默认 { id, type, title } + seed 里的 path/diff。
+ */
+ createTab?: (state: SidebarState) => { tab: SidebarTab; patch?: Partial } | null
+ /** 渲染函数 */
+ component: (props: TabComponentProps) => ReactNode
+}
+```
+
+### 3.2 `TabComponentProps`
+
+```ts
+interface TabComponentProps {
+ ctx: Context // client cordis context
+ store: SidebarStore // better-sidebar 的状态 store(可调 reduce/openTab 等)
+ scope: SessionScope // { sessionId, cwd? }
+ tab: SidebarTab // 当前 tab 实例(含 id/type/title/path?/diff?)
+ visible: boolean // 是否是当前激活 tab 且面板打开(不可见时暂停轮询等)
+ // 以下由内置 tab 使用,外部 tab 可忽略:
+ expanded?: string[] // explorer 的展开目录集
+ onToggleDir?: (path: string) => void
+ onReferenceFile?: (path: string) => void
+ onOpenFile?: (path: string) => void
+ onOpenDiff?: (tab: SidebarTab) => void
+ onSubagentJump?: (childSessionId: string) => void
+}
+```
+
+### 3.3 注册示例
+
+**最简 tab**(单实例、+ 菜单可见):
+```ts
+ctx.effect(() =>
+ ctx.betterSidebar.registerTab({
+ id: 'my-plugin:notes',
+ title: 'Notes',
+ icon: ,
+ order: 50,
+ dedupeKey: () => 'my-plugin:notes', // 单实例
+ component: ({ scope }) => ,
+ })
+)
+```
+
+**多实例 tab**(每次新开、带自定义 id):
+```ts
+ctx.effect(() =>
+ ctx.betterSidebar.registerTab({
+ id: 'my-plugin:doc',
+ title: 'Doc',
+ icon: ,
+ order: 60,
+ // 不设 dedupeKey:每次 openTab 都新开
+ component: ({ tab, scope }) => ,
+ })
+)
+// 外部触发打开:
+ctx.betterSidebar.openTab({ type: 'my-plugin:doc', title: 'Spec.md', id: 'doc:spec' })
+```
+
+**条件可见**(仅 git 仓库时显示):
+```ts
+ctx.effect(() =>
+ ctx.betterSidebar.registerTab({
+ id: 'my-plugin:commits',
+ title: 'Commits',
+ icon: ,
+ order: 70,
+ available: (state) => hasGitRepo(state), // 返回 false 时 + 菜单显示为 disabled
+ dedupeKey: () => 'my-plugin:commits',
+ component: ({ scope }) => ,
+ })
+)
+```
+
+### 3.4 内置 tab(不可重复注册)
+
+| id | order | single | hidden | 用途 |
+|---|---|---|---|---|
+| `editor` | -1 | 否(按 path 去重) | 是 | 文件编辑/预览(由 openSidebarFile 触发) |
+| `explorer` | 10 | 是 | 否 | 文件资源管理器 |
+| `git` | 20 | 是 | 否 | Git 面板 |
+| `subagent` | 30 | 是 | 否 | 子代理拓扑 |
+| `terminal` | 40 | 否 | 否 | 终端(nextTerminal 自增) |
+| `diff` | -1 | 否(按 id 去重) | 是 | 差异查看(由 GitView 触发) |
+
+你的 `id` 不可与上述重复,否则 `registerTab` 抛 `"tab type \"X\" already registered"`。
+
+---
+
+## 4. FileViewer 注册 API
+
+### 4.1 `FileViewerDescriptor` 完整字段
+
+```ts
+interface FileViewerDescriptor {
+ /** 唯一 id:'image' / 'pdf' / 'my-plugin:csv' */
+ id: string
+ /** 小写无点的扩展名数组:['png','jpg']。[] = catch-all(仅最低优先级有效) */
+ exts: readonly string[]
+ /** 优先级(高优先);默认 0。内置默认 0;catch-all code 用 -100;binary-download 用 -50 */
+ priority?: number
+ /** 字节获取策略 */
+ fetchStrategy: 'none' | 'fsRead' | 'mediaUrl' | 'custom' | 'binary-download'
+ /** 内容嗅探(覆盖 exts):head 字节可用时,第一个 detect 返回 true 的 viewer 命中 */
+ detect?: (path: string, head: Uint8Array) => boolean
+ /** fetchStrategy='custom' 时的加载函数 */
+ load?: (path: string, scope: SessionScope) => Promise
+ /** 渲染函数 */
+ component: (props: FileViewerProps) => ReactNode
+}
+```
+
+### 4.2 `FileViewerProps`
+
+```ts
+interface FileViewerProps {
+ ctx: Context
+ store: SidebarStore
+ scope: SessionScope
+ path: string
+ title: string
+ content?: string // fetchStrategy='fsRead' 时
+ truncated?: boolean // fetchStrategy='fsRead' 时
+ mediaUrl?: string // fetchStrategy='mediaUrl' 时
+ customData?: unknown // fetchStrategy='custom' 时(load() 的返回值)
+}
+```
+
+### 4.3 `fetchStrategy` 对照
+
+| 策略 | 字节来源 | 传给 component 的字段 | 适用 |
+|---|---|---|---|
+| `none` | 不需要字节 | (无) | 自渲染(如纯 UI) |
+| `fsRead` | `/sidebar/api` 的 `fs.read` | `content`, `truncated` | 文本类(CSV/JSON/XML) |
+| `mediaUrl` | `/sidebar/file` 媒体路由 URL | `mediaUrl` | 图片/PDF/Office(viewer 自己 fetch 字节) |
+| `custom` | viewer 的 `load()` 函数 | `customData` | 自定义协议(如远程拉取) |
+| `binary-download` | 不预览,显示下载按钮 | (无) | 无客户端渲染器的二进制格式 |
+
+### 4.4 匹配算法
+
+`matchFileViewer(path, head?)` 按以下顺序解析:
+
+1. **priority 降序**(稳定排序,相同 priority 按注册顺序)
+2. **detect 嗅探**:若 `head` 字节可用,遍历 priority 降序的 viewer,第一个 `detect(path, head)` 返回 true 的命中(**覆盖 exts**)
+3. **exts 匹配**:第一个 `exts` 含目标扩展名的 viewer 命中(`exts: []` 跳过此轮)
+4. **catch-all**:第一个 `exts: []` 的 viewer 命中
+5. **fallback**:返回 `undefined`,EditorView 走 CodeMirror 兜底(按扩展名选语法高亮)
+
+> **内置 viewer**(不可重复注册):image(0) / pdf(0) / docx(0) / xlsx(0) / pptx(0) / binary-download(-50)。
+> code/markdown **没有**注册为 viewer,是 EditorView 的兜底逻辑(matchFileViewer 返回 undefined 时走 fsRead + CodeMirror/MarkdownText)。外部 viewer 注册同扩展名 + 更高 priority 即可覆盖。
+
+### 4.5 注册示例
+
+**CSV 预览器**(自定义加载 + 渲染):
+```ts
+ctx.effect(() =>
+ ctx.betterSidebar.registerFileViewer({
+ id: 'my-plugin:csv',
+ exts: ['csv'],
+ fetchStrategy: 'custom',
+ load: async (path, scope) => {
+ const text = await fetchText(scope, path)
+ return parseCsv(text)
+ },
+ component: ({ customData, path }) => ,
+ })
+)
+```
+
+**覆盖内置 image viewer**(如想用自定义的 SVG 优化渲染):
+```ts
+ctx.effect(() =>
+ ctx.betterSidebar.registerFileViewer({
+ id: 'my-plugin:svg-pro',
+ exts: ['svg'],
+ priority: 10, // 高于内置 image 的 0
+ fetchStrategy: 'mediaUrl',
+ component: ({ mediaUrl }) => ,
+ })
+)
+```
+
+**内容嗅探**(按 magic bytes 路由,忽略扩展名):
+```ts
+ctx.effect(() =>
+ ctx.betterSidebar.registerFileViewer({
+ id: 'my-plugin:magic-parquet',
+ exts: [], // catch-all,但 priority 高 + detect 精确命中
+ priority: 100,
+ fetchStrategy: 'custom',
+ detect: (_path, head) => head.length >= 4
+ && head[0] === 0x50 && head[1] === 0x41
+ && head[2] === 0x52 && head[3] === 0x31, // 'PAR1'
+ load: async (path, scope) => parseParquet(await fetchBytes(scope, path)),
+ component: ({ customData }) => ,
+ })
+)
+```
+
+---
+
+## 5. 服务方法完整清单
+
+```ts
+interface BetterSidebarService {
+ /** 注册 tab 类型;返回 disposer */
+ registerTab(descriptor: TabDescriptor): () => void
+ /** 注册文件预览器;返回 disposer */
+ registerFileViewer(descriptor: FileViewerDescriptor): () => void
+ /** 当前已注册的 tab 描述符快照(同步,供 useSyncExternalStore 用) */
+ getTabs(): readonly TabDescriptor[]
+ /** 当前已注册的 file viewer 描述符快照 */
+ getFileViewers(): readonly FileViewerDescriptor[]
+ /** 按 id 查 tab 描述符 */
+ getTab(id: string): TabDescriptor | undefined
+ /** 按 path 匹配 file viewer(priority desc → detect → exts) */
+ matchFileViewer(path: string, head?: Uint8Array): FileViewerDescriptor | undefined
+ /** 打开一个 tab(+ 菜单和外部触发都用它;走 descriptor.dedupeKey 去重) */
+ openTab(seed: { type: string; title: string; path?: string; diff?: SidebarTab['diff']; id?: string }): void
+ /** 关闭一个 tab */
+ closeTab(tabId: string): void
+ /** 订阅注册表变化(register/dispose 时触发) */
+ subscribe(listener: () => void): () => void
+}
+```
+
+---
+
+## 6. 生命周期与 HMR
+
+- **disposer 必须返回**:`registerTab` / `registerFileViewer` 返回 `() => void`,Cordis fiber 卸载时自动调用。**务必**用 `ctx.effect(() => register(...))` 包裹,否则 fiber 卸载(HMR / 插件禁用)时不会撤销注册,导致下次激活时 `"already registered"` 错误。
+- **注册时机**:better-sidebar 在 `apply()` 开头 `ctx.provide('betterSidebar', service)`,所以你的插件 `inject = ['betterSidebar']` 时,better-sidebar 已经就绪。
+- **顺序无关**:Cordis 的 `inject` 保证服务就绪后才激活你的插件;你的插件可在 `apply` 内任意时刻注册。
+- **持久化降级**:localStorage 里持久化的 tab 若其 type 未注册(你的插件未加载),渲染为 `` 占位卡(显示 "插件未加载" + 关闭按钮);你的插件加载后下次渲染自动恢复。
+
+---
+
+## 7. 平台约束与陷阱
+
+| 陷阱 | 说明 |
+|---|---|
+| **构建纯度门** | client bundle 禁止 value-import `@dsh-external/*` 或非白名单的 `@deepseek-ai/*`;类型 `import type {}` 会被擦除,不触发门禁 |
+| **双 cordis 实例** | 外部插件解析不到 DSH monorepo 的 cordis augmentation;better-sidebar 自己重述了 `interface Context { betterSidebar: ... }`,你 `import type {}` 即拿到类型 |
+| **ModuleLoader 不跨插件** | 运行时 `require()` 虽支持跨 bundle,但被构建门挡;所有交互走 `ctx.betterSidebar` 方法调用 |
+| **host 半无此服务** | `ctx.betterSidebar` 只在 client 侧存在;host 半需要 better-sidebar 数据走 `/sidebar/api/*` HTTP 路由 |
+| **portal 限制** | 整面板 slot 由 ui-layout 独占,外部 tab 只能进入 better-sidebar 的 portal 内部,无法全屏替换 |
+| **id 冲突** | `registerTab` / `registerFileViewer` 对重复 id 抛错;建议用包前缀(`my-plugin:xxx`) |
+
+---
+
+## 8. 完整最小示例
+
+> 假设插件 `my-plugin` 要加一个"Database 浏览器" tab + `.csv` 文件预览器。
+
+**`my-plugin/package.json`**:
+```jsonc
+{
+ "name": "my-plugin",
+ "version": "0.1.0",
+ "main": "lib/index.js",
+ "exports": {
+ ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
+ "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" }
+ },
+ "peerDependencies": {
+ "cordis": "^4.0.0-rc.7",
+ "dsh-better-sidebar": "workspace:*",
+ "@deepseek-ai/dsh-client-runtime": "^0.0.1",
+ "react": "^18.2.0"
+ },
+ "peerDependenciesMeta": {
+ "dsh-better-sidebar": { "optional": true }
+ }
+}
+```
+
+**`my-plugin/src/client/index.tsx`**:
+```tsx
+import { createElement } from 'react'
+import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并
+import type { Context } from 'cordis'
+
+export const inject = ['betterSidebar']
+
+export function apply(ctx: Context): void {
+ // Database tab
+ ctx.effect(() =>
+ ctx.betterSidebar.registerTab({
+ id: 'my-plugin:db',
+ title: () => 'Database',
+ order: 50,
+ dedupeKey: () => 'my-plugin:db',
+ component: ({ scope }) => createElement(DbView, { sessionId: scope.sessionId }),
+ })
+ )
+
+ // CSV viewer
+ ctx.effect(() =>
+ ctx.betterSidebar.registerFileViewer({
+ id: 'my-plugin:csv',
+ exts: ['csv'],
+ fetchStrategy: 'custom',
+ load: async (path, scope) => {
+ const res = await fetch('/sidebar/api/fs.read', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ sessionId: scope.sessionId, path }),
+ })
+ const { value } = await res.json()
+ return parseCsv(value.content)
+ },
+ component: ({ customData, path }) =>
+ createElement(CsvGrid, { rows: customData as string[][], path }),
+ })
+ )
+}
+
+function DbView(props: { sessionId: string }): React.ReactNode { /* ... */ }
+function CsvGrid(props: { rows: string[][]; path: string }): React.ReactNode { /* ... */ }
+function parseCsv(text: string): string[][] { /* ... */ }
+```
+
+**注册到 profile**:在 `~/.dsh/profiles/web/package.json` 的 `dependencies` 加 `"my-plugin": "link:<你的插件路径>"`,在 `cordis.patch.yml` 加挂载行,`pnpm install`,重启 `dsh web` + 浏览器硬刷新。
+
+---
+
+## 9. 参考实现
+
+better-sidebar 自己的内置 tab 和 viewer 就是参考实现("吃狗粮"):
+
+- **`src/client/builtins.tsx`**:6 个内置 tab(explorer/git/subagent/terminal/editor/diff)+ 6 个内置 viewer(image/pdf/docx/xlsx/pptx/binary-download)的注册代码
+- **`src/client/service.ts`**:`BetterSidebarService` 接口 + `createBetterSidebarService` 工厂实现
+- **`tests/service.spec.ts`**:12 个测试覆盖 register/dispose/matchFileViewer/dedupe/createTab
+- **`docs/plans/2026-08-11-service-registry-design.md`**:设计文档
+
+调试时直接读这些文件即可看到所有 API 的真实用法。
diff --git a/README.md b/README.md
index f86edc7..af21455 100644
--- a/README.md
+++ b/README.md
@@ -286,6 +286,30 @@ pnpm watch # tsdown --watch(client bundle 热重建)
- client 状态按会话持久化到 `localStorage`(`dsh-sidebar:v1:`),读取时结构校验 + 宽度钳制视口 + 重复 id 重新编号
- client bundle externals 仅模块表词条(react/cordis/ui-primitives 等),xterm/CodeMirror 等全部内联
+## 🔌 服务化(v0.4.0+):让其他插件注册 tab 和文件预览器
+
+better-sidebar 从 v0.4.0 起暴露 `ctx.betterSidebar` 服务(Cordis context 属性),其他插件可通过它注册:
+
+- **侧边栏页面(tab)**:`ctx.betterSidebar.registerTab({ id, title, icon, component, ... })`
+- **文件类型预览器**:`ctx.betterSidebar.registerFileViewer({ id, exts, fetchStrategy, component, ... })`
+
+两个方法都返回 disposer,用 `ctx.effect(() => register(...))` 包裹即可让 Cordis fiber 卸载时自动撤销(HMR-safe)。内置 6 个 tab(explorer/git/subagent/terminal/editor/diff)和 6 个 viewer(image/pdf/docx/xlsx/pptx/binary-download)也通过同一服务注册("吃狗粮"),外部插件与内置一视同仁。
+
+**消费插件最小骨架**:
+```ts
+import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并
+export const inject = ['betterSidebar']
+export function apply(ctx: Context) {
+ ctx.effect(() => ctx.betterSidebar.registerTab({
+ id: 'my-plugin:db',
+ title: 'Database',
+ component: ({ scope }) => ,
+ }))
+}
+```
+
+**完整接入文档**:见 [`AGENTS.md`](./AGENTS.md) ——含 `TabDescriptor` / `FileViewerDescriptor` 全字段、`fetchStrategy` 对照、匹配算法、HMR 陷阱、完整最小示例。
+
## 🔐 安全边界
- 路由受 Host 头信任围栏保护(与 `/api` 一致;`0.0.0.0` 部署时由 `dsh web` 启动器动态派生的 LAN IP 列表生效)