docs: record the 0.1.6-alpha.2 adaptation and resync the user docs
Adds the per-version adaptation record (docs/plans/2026-09-21-…-adaptation.md), rewrites AGENTS.md §3 for the new baseline (the two silent contract breaks, the terminal/browser handoff, the new official plugin-manager surface, the three ui-primitives breaks, the widened primitives bare-import set), and updates both READMEs and the integration guide for the v0.20.0 feature set. Also retires the support text the terminal deletion made false: the install flow is one step again (no build-script approval), the FAQ's two node-pty rows collapse into one pointing at DSH's own terminal, the platform note no longer promises prebuilt native binaries (there are no native deps left), and the installers drop the -Repair mode and their allowBuilds writes along with pnpm-workspace.yaml's now-empty allowBuilds block. Removes the dead browserNoSandboxWarning locale key (its setting went with the plugin's browser) and tightens theme.spec's capsule guard so a literal colour fails it. The e2e resolver now refuses a PATH that is not on the baseline line: this machine's PATH is 0.1.6-alpha.1, and running the mount lane against it fails at 'the bundle was not registered' with a zero exit from the CLI — a misleading dead end worth one explicit error instead.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
> 面向 **消费插件开发者**:如何让你的插件向 better-sidebar 注册新的侧边栏页面(tab)和文件类型预览器。
|
||||
>
|
||||
> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**;终端固定(pin)**v0.17.0+**。当前版本 **v0.19.1**(正式版,npm `latest`,仅支持 DSH **0.1.5-rc.1+**,已在 **0.1.5-rc.2** 上完成真机挂载验证;**0.1.5-alpha.2 及更早不再支持**——alpha.2 用户请用 v0.19.0-alpha.1,那是它的最后一版;0.1.2-rc.1 稳定线请用 v0.18.x)。**v0.19.0 移除了自绘右侧面板与自由窗口**(见 §0、§11)。
|
||||
> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**。当前版本 **v0.20.0**(正式版,npm `latest`,仅支持 DSH **0.1.6-alpha.2+**)。**v0.19.0 移除了自绘右侧面板与自由窗口**(见 §0、§11);**v0.20.0 移除了插件自带的终端与浏览器视图**(宿主 0.1.6 自带两者,见 §4.4)、**移除了终端固定(pin)**(终端本身不再由本插件提供),并**删除了轮尾产物行接管**(DSH 0.1.6 把 `conversation.chat.turnTail` 从 chain 改成只能追加的 list,替换语义不复存在)。
|
||||
> 权威代码:`src/client/service.ts`(服务实现)、`src/client/builtins/`(内置 8 tab + 6 viewer 参考实现)、`lib/types/client/service.d.ts`(类型声明)。
|
||||
> 仓库开发规则(硬约束 / CI / 发版)见 [AGENTS.md](../AGENTS.md)。
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
## 0. 承载面:DSH 原生右侧栏 + 插件底部工作台(v0.19.0-alpha.0 起)
|
||||
|
||||
从 v0.19.0-alpha.0 起,**右列完全属于 DSH**:你的 tab 渲染在 **DSH 自己的右侧栏**里(`ctx.sidebarRight` / `ctx.sidebarRightTabs`),插件把每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`)+ 一个原生 tab 体。插件自己只保留**底部工作台**(分栏树、终端、会话内持久化)。对你的接入代码**没有影响**——仍然只调用 `ctx.betterSidebar`:
|
||||
从 v0.19.0-alpha.0 起,**右列完全属于 DSH**:你的 tab 渲染在 **DSH 自己的右侧栏**里(`ctx.sidebarRight` / `ctx.sidebarRightTabs`),插件把每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`)+ 一个原生 tab 体。插件自己只保留**底部工作台**(分栏树、会话内持久化;**v0.20.0 起其中不再有终端**——宿主 0.1.6 的右侧栏终端取代了它)。对你的接入代码**没有影响**——仍然只调用 `ctx.betterSidebar`:
|
||||
|
||||
- `registerTab` / `registerFileViewer` 签名不变;
|
||||
- `openTab` / `openFile` 默认落到原生右侧栏;新增可选 `OpenTabSeed.target`(`'right'` 默认 / `'bottom'` 落插件的底部工作台);
|
||||
@@ -21,7 +21,7 @@
|
||||
| 事项 | 说明 |
|
||||
|---|---|
|
||||
| 生命周期回调 | 原生面只有「一次打开」,不区分新建/聚焦,因此只触发 `onOpen`(`onActivate` 仅在插件自己的底部工作台里触发) |
|
||||
| 去重 | 原生按 `(kind, 地址)` 去重:有 `createTab` 的类型每次新开一个 tab(terminal / browser / sidechat / diff),其余聚焦已有 tab;`dedupeKey` 的自定义语义不参与原生面 |
|
||||
| 去重 | 原生按 `(kind, 地址)` 去重:有 `createTab` 的类型每次新开一个 tab(sidechat / diff),其余聚焦已有 tab;`dedupeKey` 的自定义语义不参与原生面 |
|
||||
| 布局持久化 | 原生栏的布局**只在内存**(刷新后回到折叠默认),插件自己的底部工作台仍然持久化 |
|
||||
| 跨会话打开 | 目标会话的右侧栏 store 未挂载时,打开会排队到该会话上屏后重放 |
|
||||
| 内置类型接管 | 插件的 `editor` 类型以 `extension` 优先级认领 `dsh-resource://file/**`(压过内置 `ui-sidebar-documentpreview` 的 `text` 预览——即 `fallback` 带),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件的文件树);插件卸载/禁用时内置实现自动复位 |
|
||||
@@ -42,7 +42,7 @@ better-sidebar 从 v0.4.0 起把自己改造成一个**注册表服务**:
|
||||
- **新页面(tab)**:注册一种新的侧边栏 tab 类型,出现在侧边栏 `+` 菜单里,用户点击后在自己的分栏里打开你的 React 页面;
|
||||
- **文件预览器(file viewer)**:注册一种文件类型预览器,让用户在侧边栏打开文件时走你的渲染组件(覆盖或补充内置的 image/pdf/code 等)。
|
||||
|
||||
内置的 7 个 tab(editor / git——「文件变动」统一 tab(Git 视角 + 本轮文件视角)/ subagent / sidechat / terminal / browser / diff)和 6 个 viewer(image / pdf / markdown / html / code / binary-download)**自己也是通过同一套 API 注册的**(吃自己的狗粮),所以外部插件的能力与内置功能完全对等。
|
||||
内置的 5 个 tab(editor / git——「文件变动」统一 tab(Git 视角 + 本轮文件视角)/ subagent / sidechat / diff)和 6 个 viewer(image / pdf / markdown / html / code / binary-download)**自己也是通过同一套 API 注册的**(吃自己的狗粮),所以外部插件的能力与内置功能完全对等。**v0.20.0 起不再内置 terminal / browser 两个 tab 类型**:DSH 0.1.6 自带右列终端(`ui-sidebar-terminal`、kind `terminal`)与浏览器(`ui-sidebar-browser`、kind `browser`),插件的同名类型会让读者看到两份实现。
|
||||
|
||||
关键机制一句话:better-sidebar 的 client half 在 `apply()` 开头执行 `ctx.provide('betterSidebar', service)`(`src/client/index.tsx`),消费插件在 `inject` 里声明 `'betterSidebar'`,Cordis 保证服务就绪后才激活你的插件,然后你调用 `ctx.betterSidebar.registerTab(...)` / `registerFileViewer(...)` 完成注册,返回的 disposer 由 Cordis fiber 在卸载(HMR / 禁用)时自动调用。
|
||||
|
||||
@@ -192,11 +192,11 @@ interface TabDescriptor {
|
||||
description?: string | (() => string)
|
||||
/** 图标:ReactNode 或 (size: number) => ReactNode(不声明时宿主补一个方块占位) */
|
||||
icon?: ReactNode | ((size: number) => ReactNode)
|
||||
/** + 菜单排序(升序);默认 100。内置:editor=10, git=20, subagent=30, sidechat=35, terminal=40, browser=50 */
|
||||
/** + 菜单排序(升序);默认 100。内置:editor=10, git=20, subagent=30, sidechat=35 */
|
||||
order?: number
|
||||
/** 从 + 菜单隐藏(editor/diff 用:由其他流程触发打开,不在菜单里) */
|
||||
hidden?: boolean
|
||||
/** + 菜单禁用判定(如 terminal 配额满)。返回 false 只影响菜单 disabled,不拦截 openTab(只有设置页禁用开关会)。 */
|
||||
/** + 菜单禁用判定(用量配额一类)。返回 false 只影响菜单 disabled,不拦截 openTab(只有设置页禁用开关会)。 */
|
||||
available?: (ctx: Context, scope: SessionScope, state: SidebarState) => boolean
|
||||
/**
|
||||
* 单实例语法糖:`single: true` ≡ `dedupeKey: () => id`(打开时聚焦既有
|
||||
@@ -212,7 +212,7 @@ interface TabDescriptor {
|
||||
dedupeKey?: (tab: SidebarTab) => string | undefined
|
||||
/**
|
||||
* 自定义 tab 创建(minting SidebarTab + 状态 patch)。
|
||||
* 返回 null 拒绝创建。terminal 用它生成 terminal:<n> id 并递增 nextTerminal。
|
||||
* 返回 null 拒绝创建。sidechat 用它实现「一个线程一个 tab」。
|
||||
* 省略时用默认 { id, type, title } + seed 里的 path/diff。
|
||||
*/
|
||||
createTab?: (state: SidebarState) => { tab: SidebarTab; patch?: Partial<SidebarState> } | null
|
||||
@@ -220,7 +220,8 @@ interface TabDescriptor {
|
||||
* 外链认领(v0.13.0+,`features.includes('urlTarget')` gate):外链被接管
|
||||
* (`browserInterceptLinks` 总闸 + 协议开关均开)时,第一个 urlTarget 命中且未禁用的
|
||||
* 类型以 openTab({ type, url, title: hostname }) 打开,URL 预填 tab.path。先到先得;
|
||||
* 谓词抛错被吞。内置 browser 不声明、永远隐式兜底。
|
||||
* 谓词抛错被吞。v0.19.1 及更早内置 browser 隐式兜底;**v0.20.0 起本插件不再有
|
||||
* browser 类型**,无可认领的默认目标时接管直接放行给系统浏览器。
|
||||
* 多 URL 并存需 createTab 铸造 per-URL id,否则二次点击被 id 安全网聚焦、不覆写 path。
|
||||
*/
|
||||
urlTarget?: (url: URL) => boolean
|
||||
@@ -240,8 +241,8 @@ interface TabDescriptor {
|
||||
* - onOpen:openTab 真正**新建** tab 后(dedupe/id 安全网聚焦不算打开);
|
||||
* - onActivate:tab 被聚焦时(dedupe 聚焦、id 安全网聚焦、tab 栏点击激活);
|
||||
* - onClose:closeTab 关闭 tab 后。
|
||||
* 内置专属流程(diff 拆分放置、agent 终端 reconcile)直接改 state,不触发
|
||||
* 回调——但它们只作用于内置类型(diff/terminal),外部插件的 tab 永远走
|
||||
* 内置专属流程(diff 拆分放置、sidechat 线程重开)直接改 state,不触发
|
||||
* 回调——但它们只作用于内置类型(diff / sidechat),外部插件的 tab 永远走
|
||||
* service 路径。回调抛错只 console.error,绝不打断打开/关闭流程。
|
||||
* openTab 回调 scope 携带调用者传入的 { sessionId, cwd? };
|
||||
* closeTab/activateTab 仅显式传 scope 时带 cwd。
|
||||
@@ -356,7 +357,7 @@ ctx.effect(() =>
|
||||
)
|
||||
```
|
||||
|
||||
**自定义创建**(mint 自增 id,terminal 内置页同款):
|
||||
**自定义创建**(mint 自增 id):
|
||||
|
||||
```ts
|
||||
ctx.effect(() =>
|
||||
@@ -365,8 +366,8 @@ ctx.effect(() =>
|
||||
title: 'Console',
|
||||
order: 80,
|
||||
createTab: (state) => ({
|
||||
tab: { id: `console:${state.nextTerminal}`, type: 'my-plugin:console', title: `Console ${state.nextTerminal}` },
|
||||
patch: { nextTerminal: state.nextTerminal + 1 }, // 借内置计数器;也可自建 state 字段
|
||||
tab: { id: `console:${state.nextBrowser}`, type: 'my-plugin:console', title: `Console ${state.nextBrowser}` },
|
||||
patch: { nextBrowser: state.nextBrowser + 1 }, // 借内置计数器;也可自建 state 字段
|
||||
}),
|
||||
component: ({ tab, scope }) => <ConsoleView tabId={tab.id} sessionId={scope.sessionId} />,
|
||||
})
|
||||
@@ -400,12 +401,19 @@ ctx.effect(() => {
|
||||
| `git` | 20 | 是 | 是(本轮文件操作数) | 「文件变动」统一 tab(id 保留 `git` 以兼容持久化布局):**Git 视角**(原 Git 面板:staged/unstaged / 提交 / 历史 / worktree·子仓库选择)+ **本轮文件视角**(原 file-trace:模型读/写/编辑实时折叠,按文件分组、类型筛选);会话事件经插件自有宿主路由 `changes.ops` 供给(live 日志优先、冷会话回放持久化记录,`afterSeq` 增量),badge 读 tab 轮询写入的同步缓存。两视角共用底部可拖拽预览面板(`tab.meta.lens/previewH` 持久化),diff 渲染统一走 `src/client/diff/`(`DiffRows`/`DiffFiles`:mod 配对 + 行内高亮 + 语法着色 + 上下文折叠);Git 目标可展开为独立 diff tab(落进工作台的 diff 分栏) |
|
||||
| `subagent` | 30 | 是 | 否 | 子代理拓扑 |
|
||||
| `sidechat` | 35 | 否(`sidechat:<uuid>`,按 `meta.threadId` 去重) | 否 | 侧边对话(每对话一 Tab):打开即建空线程(首条消息赢得标签并同步标题);线程 = 插件自建子会话(种子继承父会话上下文,进行中回合以 `interrupted` 闭合;种子带合法 `subagent/descriptor`,SubagentView 按 `Side: ` 前缀过滤),`origin:'subagent'` 隐藏于主列表;走 `/sidebar/api/sidechat.*` 路由;头部菜单切换/重开(`parkSidechatReopen` + 确定性 id),关 Tab 释放 live agent;重开经 `collectOwnEvents` 回源到种子边界;「保存为新会话」= `session.fork`(`this` 敏感)。[设计文档](plans/2026-08-20-sidechat-tab-design.md) |
|
||||
| `terminal` | 40 | 否(`terminal:<n>`) | 否 | 终端。v0.17.0+ 右键「固定到工作区/全局」:跨会话不消失,TabBar 内联虚拟 Tab(`pinned:<homeSessionId>:<tabId>`),就地按 home scope 连 PTY;global 全会话可见、workspace 仅同 cwd;`tab.pin = { scope, homeCwd? }` 随会话持久化,渲染期解析(`collectPinnedTabs` → `createPinnedVirtualTab` → `injectPinnedIntoTree`) |
|
||||
| `browser` | 50 | 否(`browser:<n>`) | 否 | 内嵌浏览器(沙箱 iframe,可设置关沙箱) |
|
||||
| `diff` | -1 | 否(按 id 去重) | 是 | 差异查看(changes tab 的预览面板「展开为独立页签」触发,同一渲染栈) |
|
||||
|
||||
你的 `id` 不可与上述重复,否则 `registerTab` 抛 `"tab type \"X\" already registered"`。
|
||||
|
||||
**v0.20.0 删除的类型(迁移提示)**:
|
||||
|
||||
| 原 id | 现状 | 你该怎么做 |
|
||||
|---|---|---|
|
||||
| `terminal` | 宿主自带 `kind: 'terminal'`(`@deepseek-ai/dsh-client-ui-sidebar-terminal`) | 不要再注册同名 kind。插件侧 API(`api.ptyClose` / `agentPtyClose` / `agentSkipWait` / `terminalDeps` / `shellGet`)与 `SidebarTab.pin` / `SidebarState.nextTerminal` / `agentWaits` / `isAgentTabId` / `agentUuidOf` / `reconcileAgentTerminals` / `mirrorAgentWaits` / `setTabPin` 全部**已删除** |
|
||||
| `browser` | 宿主自带 `kind: 'browser'`(`@deepseek-ai/dsh-client-ui-sidebar-browser`) | 要打开网页请走宿主:`ctx.sidebarRight.openTab('browser', { params: { url } })`(宿主未装该包时 `openTab` 会抛,务必 try/catch 兜底)。插件侧的 `api.browserProbe` / `SidebarPrefs.browserNoSandbox` / `browserAllowedLoopback` 已删除;`browserInterceptLinks` / `browserInterceptHttp` / `browserInterceptHttps` **保留**(链接接管仍在,`urlTarget` 机制不变) |
|
||||
|
||||
**同时删除的还有**:轮尾产物行接管。DSH 0.1.6 把 `conversation.chat.turnTail` 从 `chain` 改成 `list`(list 槽要求 `options.id`,且只能**追加**,不能替换宿主的产物行),插件因此整体移除了 `registerTurnTailInterception` 与 `selectProducedFiles`。要往那个位置加东西,按 list 契约注册 `{ name: 'conversation.chat.turnTail', id: '<你的 id>', order }`;`openSidebarFile` 作为通用打开工具留在 `dsh-better-sidebar/src/client/sidebar-file.ts`(`resolveSidebarPath` 移到 `src/client/paths.ts`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 文件预览器(FileViewer)注册 API
|
||||
@@ -811,7 +819,7 @@ ctx.effect(() =>
|
||||
- 展示:小卡片网格(图标 + 标题 + 类型 id),**高亮 = 启用**,勾选徽标钉在卡片最右端;viewer 卡片额外显示扩展名。
|
||||
- 持久化:开关写入 `SidebarPrefs.tabsEnabled / viewersEnabled`(开放 map,**缺省 = 启用**,显式 `false` 才禁用)。
|
||||
- 关闭语义:tab 从 `+` 菜单消失、`openTab` 拒绝新开(`console.warn`)、派生流程(子代理自动展开、agent 终端自动补 tab)停止,**已打开的 tab 保留**;viewer 被 `matchFileViewer` 跳过,文件落到下一个匹配。
|
||||
- `settings.toggles`(可选):在卡片行下追加**嵌套设置行**(仅父级启用时显示),绑定 `SidebarPrefs` 字段;通过卡片底部「功能设置」条在原生弹窗中编辑。行控件形状见 §4.1 的 `SettingRow`:`type: 'switch' | 'text' | 'number'`(v0.11.0+;text/number 行 blur/Enter 提交,number 行按 min/max 钳制,unit 渲染单位后缀)与 `type: 'select'`(v0.13.0+;`options` 支持 value/title/desc/icon,`multi` 多选存数组并按 options 顺序提交;任一项带 icon 时渲染大图标选项卡)。内置示例:subagent tab 的 `autoOpenSubagent`、terminal tab 的 `agentTerminalTools` + 自定义字体行、editor tab 的 `editorExplorer` 图标化下拉与 `workspaceFence` 开关(工作区路径围栏,见 §8.1)。
|
||||
- `settings.toggles`(可选):在卡片行下追加**嵌套设置行**(仅父级启用时显示),绑定 `SidebarPrefs` 字段;通过卡片底部「功能设置」条在原生弹窗中编辑。行控件形状见 §4.1 的 `SettingRow`:`type: 'switch' | 'text' | 'number'`(v0.11.0+;text/number 行 blur/Enter 提交,number 行按 min/max 钳制,unit 渲染单位后缀)与 `type: 'select'`(v0.13.0+;`options` 支持 value/title/desc/icon,`multi` 多选存数组并按 options 顺序提交;任一项带 icon 时渲染大图标选项卡)。内置示例:subagent tab 的 `autoOpenSubagent`、editor tab 的 `editorExplorer` 图标化下拉与 `workspaceFence` 开关(工作区路径围栏,见 §8.1)。
|
||||
- `settings.pluginToggles`(可选,v0.12.0+):**插件自有设置行**,行控件与 toggles 相同,但 key 是插件局部的——持久化在 prefs 文档的 `pluginSettings[<descriptor id>]`(开放 map,无需宿主 schema 字段)。tab 与 viewer 都可用(v0.12.0 起 viewer 卡片也有设置条)。
|
||||
- `settings.render`(可选,v0.12.0+):**自定义设置面板**——追加渲染在行列表之后,可单独存在。props 含 store/service/prefs、本 descriptor 的 `pluginSettings` blob、`updatePluginSetting(key, value)` 与 `close()`;抛错会被吞掉并显示内联错误。
|
||||
|
||||
@@ -851,7 +859,7 @@ ctx.effect(() =>
|
||||
)
|
||||
```
|
||||
|
||||
> ⚠️ **`toggles` 的 key 必须是宿主 PrefsSchema 的字段**(内置键:`autoOpenSubagent` / `agentTerminalTools` / `agentOpenTools` / `terminalFontFamily` / `terminalFontSize` / `editorExplorer` / `workspaceFence` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserNoSandbox` / `browserInterceptLinks` / `browserInterceptHttp` / `browserInterceptHttps` / `changesDiffFloat`)。**v0.12.0 起设置 seam 已开放**:你自己的设置走 `pluginToggles`(声明式行)或 `render`(自定义面板),值持久化在 `pluginSettings[id]`——不再需要宿主 schema 字段,也不再被 seam 丢弃。值须 JSON 可序列化(行控件只产出 string/number/boolean;自定义面板自行负责)。
|
||||
> ⚠️ **`toggles` 的 key 必须是宿主 PrefsSchema 的字段**(v0.20.0 起的内置键:`autoOpenSubagent` / `autoOpenJobs` / `agentOpenTools` / `editorExplorer` / `workspaceFence` / `titleBarScheme` / `titleBarPresetId` / `customCss` / `titleBarCompat` / `titleBarStripPx` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `browserInterceptLinks` / `browserInterceptHttp` / `browserInterceptHttps` / `tabsEnabled` / `viewersEnabled`;**已删除**:`agentTerminalTools` / `terminalShell` / `terminalShellArgs` / `terminalFontFamily` / `terminalFontSize` / `bottomPanelAutoTerminal` / `browserNoSandbox` / `browserAllowedLoopback`)。**v0.12.0 起设置 seam 已开放**:你自己的设置走 `pluginToggles`(声明式行)或 `render`(自定义面板),值持久化在 `pluginSettings[id]`——不再需要宿主 schema 字段,也不再被 seam 丢弃。值须 JSON 可序列化(行控件只产出 string/number/boolean;自定义面板自行负责)。
|
||||
|
||||
### 8.1 内置键 `workspaceFence`(工作区路径围栏)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user