- SidebarPrefs 新增 tabsEnabled / viewersEnabled 开放 map(缺省 = 启用), PrefsSchema 用 z.dict 校验,旧文档零迁移 - TabDescriptor/FileViewerDescriptor 新增声明式设置面:viewer 补 title/icon, 两者补 settings.toggles(子代理声明 autoOpenSubagent、终端声明 agentTerminalTools) - 服务新增 isTabEnabled/isViewerEnabled;openTab 拒绝禁用类型、matchFileViewer 跳过禁用 viewer - SideCardSection 重写为注册表驱动清单:侧边栏内容(图标+类型id+开关+嵌套开关) + 文件预览(图标+标题+扩展名+开关),开关持久化 + 乐观提交回滚 - 消费点 gating:+ 菜单过滤、子代理自动展开、agent 终端 reconcile、 产出文件拦截(editor 关闭时回落默认行)、新会话 explorer 预置 - 内置 viewer 补 6 个手绘 outline 图标;store 快照携带 prefs 使开关变更即时生效 - 测试:service gating / builtins 声明 / parsePrefs map 校验 / schema 断言 / 新增 SideCardSection renderToString 规格 - 文档:README 设置小节、AGENTS.md 描述符与服务方法、新增设计文档;版本 0.5.0
8.1 KiB
8.1 KiB
侧边栏设置声明式化 + 内置支持"插头化"设计
日期:2026-08-11 状态:已实施(本文档含实施偏差记录) 目标版本:v0.5.0
1. 目标
- 把 DSH 设置页的「侧边卡片」分区从硬编码 4 行改为注册表驱动的声明式清单:
- 展示当前注册的所有侧边栏内容(tab 类型)与文件查看器(viewer),每项可直接打开/关闭;
- 每项声明式元数据:类型(id)、图标(SVG)、相关设置(如子代理的"检测到子代理就自动打开")。
- 内置支持(资源管理 / Git / 终端 / 子代理 / 编辑器 / 差异)与 8 个文件预览器继续以插头(descriptor)形式注册,并在描述符上携带上述声明。
2. 非目标
- 不改宿主路由 / pty / tools 的 gating 语义:
agentTerminalTools仍是独立布尔偏好(宿主syncToolsGate零改动),只是它的设置行渲染位置改由终端 tab 描述符声明。 - 不做"已打开 tab 随禁用即时关闭"(保留式语义:禁用只影响新开,已打开 tab 继续渲染)。
- 不实现外部插件自定义 settings 键的动态 schema(见 §6 限制)。
- 不迁移 bundle 形态、不加依赖、不动 peerDependencies。
3. 现状回顾
SideCardSection.tsx硬编码 4 行:openByDefault/defaultWidthPercent/autoOpenSubagent/agentTerminalTools。PrefsSchema(config.ts)为扁平对象,parsePrefs(client/prefs.ts)逐字段校验。- 内置 tab/viewer 已通过
registerBuiltins走同一注册表(v0.4.0),但描述符缺少设置面元数据:viewer 无title/icon,tab 无相关设置声明。 - 服务无启用态概念:
getTabs/matchFileViewer/openTab不过滤。
4. 设计
4.1 偏好扩展(prefs-shared.ts / config.ts / client/prefs.ts)
interface SidebarPrefs {
openByDefault: boolean
defaultWidthPercent: number
autoOpenSubagent: boolean
agentTerminalTools: boolean
tabsEnabled: Record<string, boolean> // 按 tab descriptor id;缺省 = 启用
viewersEnabled: Record<string, boolean> // 按 viewer descriptor id;缺省 = 启用
}
- schema:
z.dict(z.boolean()).default({})(schemasterydict);旧文档自动解析为{}(全开),零迁移。 parsePrefs:record-of-boolean 校验(非对象/非布尔值回退{}/ 丢弃该键)。- 设置页开关写入
tabsEnabled: { ...prefs.tabsEnabled, [id]: next }(viewers 同理)。
4.2 声明式描述符(client/service.ts)
interface SidebarSettingToggle {
key: string // SidebarPrefs 字段名('autoOpenSubagent')
title: string | (() => string)
desc?: string | (() => string)
}
interface SidebarSettingsDeclaration {
toggles?: readonly SidebarSettingToggle[]
}
// TabDescriptor 增加:
settings?: SidebarSettingsDeclaration
// FileViewerDescriptor 增加(设置清单展示用):
title?: string | (() => string) // 缺省回退 id
icon?: ReactNode | ((size) => ReactNode)
settings?: SidebarSettingsDeclaration
服务新增:
isTabEnabled(id: string): boolean // store.getPrefs().tabsEnabled[id] !== false
isViewerEnabled(id: string): boolean
行为变化:
openTab:类型被禁用 → no-op(console.warn提示);matchFileViewer:跳过禁用 viewer(文件落到下一个匹配或下载按钮)。getTabs()/getFileViewers()仍返回全部注册项(设置页清单必须看到已禁用的项)。
4.3 内置声明(builtins/tabs.tsx / builtins/viewers.tsx)
| tab | 相关设置声明 |
|---|---|
| subagent | toggles: [{ key: 'autoOpenSubagent', ... }] |
| terminal | toggles: [{ key: 'agentTerminalTools', ... }] |
| explorer/git/editor/diff | 无(仅开关) |
8 个 viewer 全部补 title(locales 新增 viewerImage…viewerBinary)+ icon(icons.tsx 新增 6 个手绘 outline SVG:image/pdf/docx/xlsx/pptx/markdown;code/binary-download 复用 IconCodeOutline16 / IconDownloadOutline16)。
4.4 Store 快照携带 prefs(client/state.ts)
SidebarSnapshot 增加 prefs 字段;setPrefs 更新快照并 notify()(setSession/update/reduce 构造快照时带上当前 prefs)。效果:+ 菜单等 useSyncExternalStore 消费者在设置变更后立即重渲染(禁用 git → + 菜单马上消失)。
makeDefaultState(width, panelOpen, seedExplorer = true) 增加第三参;loadState 在 tabsEnabled['explorer'] === false 时传 false——新会话不再预置 explorer tab(与"已存在会话保持布局"语义并列:禁用只影响新布局)。
4.5 消费点 gating
| 消费点 | 行为 |
|---|---|
+ 菜单(buildNewTabOptions) |
filter(!d.hidden && service.isTabEnabled(d.id)) |
| 子代理自动展开 effect | 追加 isTabEnabled('subagent') === false 早退(禁用时不展开面板) |
| agent 终端 reconcile(WS 推送) | isTabEnabled('terminal') === false 时忽略推送(不自动补 tab;重新启用后下一次推送自愈) |
产出文件拦截(intercept.tsx select) |
tabsEnabled['editor'] === false 时 select 返回 null(回落默认交付行) |
openTab / matchFileViewer |
见 §4.2 |
| 新会话默认布局 | explorer 禁用 → 空 pane |
4.6 设置页声明式渲染(SideCardSection.tsx)
inject: () => ({ store, service })(index.tsx闭包直接捕获 service)。- 注册表清单:本地 state +
service.subscribe同步(注册变化罕见,无需 external-store)。 - 行结构:
- 常规:
openByDefault、宽度(既有逻辑)。 - 侧边栏内容:按
order升序、hidden置后遍历 tabs——图标(16) + 标题 + desc=tab.id(类型)+ 开关;settings.toggles渲染为缩进子行(仅父级启用时显示,绑定prefs[toggle.key])。 - 文件预览:按 priority 降序遍历 viewers——图标 +
title ?? id+ desc=exts.join(' · ') || t('settingsViewerCatchAll')+ 开关。
- 常规:
- 提交机制不变:串行化 + revision 守卫 + 失败回滚 +
store.setPrefs同步。
5. 测试
service.spec.ts:启用态缺省 true;openTab拒绝禁用类型;matchFileViewer跳过禁用 viewer(含兜底 code 被关 → undefined)。builtins.spec.ts:subagent/terminal 的 toggles 声明;全部 tab 有 icon;全部 viewer 有 title+icon。unit.spec.ts:parsePrefs的 map 校验(非对象/非布尔回退)+ 新字段默认值;loadStateexplorer 禁用 → 空 pane。plugin-shape.spec.ts/smoke.spec.ts:schema 解析与 settings 路由断言补新字段。side-card-section.spec.tsx(新增):renderToString断言声明式行(图标/标题/id/exts/开关/嵌套开关隐藏规则)。
6. 限制与失败模式
- 外部插件的自定义 settings 键:
toggles[].key必须是宿主PrefsSchema的字段(内置:autoOpenSubagent/agentTerminalTools);未知键会被 settings seam 丢弃(schemastery object 非严格模式放行未知键但 schema 不含其校验)。文档注明。 - 设置路由失败 / 文档缺失:沿用既有降级(schema 默认 + 本地 state,开关默认全开)。
- 已打开 tab 的类型被禁用:tab 保留可继续使用;关闭后无法重开。
- 父级(子代理/终端)关闭时嵌套开关隐藏,其值保留在 prefs 不销毁。
7. 实施偏差记录
- 嵌套开关的"父级关闭即隐藏":设计初稿曾考虑始终显示嵌套开关;实施决定仅在父级启用时渲染(避免"autoOpenSubagent 已开但子代理页面被关"的无效组合),值保留。
- 宿主
syncToolsGate不改:agentTerminalTools仅随自身开关走,不与tabsEnabled['terminal']联动(模型终端工具是模型面向能力,与侧边栏 UI 开关解耦)。 renderRow统一行渲染:tab/viewer/嵌套开关共用一行渲染器(sub参数区分缩进),CSS 新增.sectionHeading/.rowIcon/.titleLine/.subRow。- 清单排序:tabs 按
hidden置后 +order升序(editor/diff 殿后);viewers 按 priority 降序(code 兜底最后)。