feat: adapt to DSH 0.1.7-alpha.1 and yield the read-only previews
DSH 0.1.7-alpha.1 is 1299 commits past 0.1.6-alpha.2 and rewrites four
contracts this plugin was built on. The peer floor moves to
^0.1.7-alpha.1 (semver's prerelease rule means ^0.1.6-alpha.2 can never
match a 0.1.7 prerelease); 0.1.6-alpha.2 and earlier stay on v0.19.1.
Released as 0.21.0-alpha.1 (dist-tag alpha).
Hard breaks:
- dsh-settings was rewritten: register(ns, schema) is gone, a form is
addressed by the plugin row's Loader entry id and read from the
module's exported Config. The preferences are merged into Config and
marked volatile — which requires DSH's own schemastery, because only
that build wraps a volatile field in the cosmokit reference the
loader's volatile commit walks. With the public package a settings
write reports success and revision 1 while the effective value never
changes.
- ui-primitives renamed its whole icon family (IconXxx14/16 ->
IconXxxRegular/Medium): 26 imported symbols, 17 files. The failing
mode is render-time, not load-time. Five call sites that relied on the
old 14px default now pass size={14} explicitly.
- Session format v4 rejects source.kind === 'plugin'; the side-chat
boundary injection now writes plugin:dsh-better-sidebar, the same kind
DSH's own v3->v4 migration produces, so historical threads survive.
Tool results moved to role 'tool' with top-level content/isError; all
four parsers accept both shapes.
- The client session snapshot dropped subagentsByParent/jobsBySession
and the on-demand subagent-catalog observation API. The Tasks page
reads projectionsBySession and a new jobs.list route.
Yields and convergence:
- The read-only previews go to DSH's documentpreview (Office->PDF, a
spreadsheet worker, zoom, per-directory refresh); editor.canOpen
refuses those 26 extensions. Markdown, HTML (with its sandbox
switches) and the editable code buffer stay.
- The host browser kind is desktop-profile-only now, so the web e2e
lanes expect no browser entry and assert its absence.
- Link takeover claims only urlTarget links and passes the rest to the
host, fixing a regression from the 0.1.6 round: the fallback pointed
at a 'browser' type the plugin no longer registers, so http links in
its own markdown did nothing.
Also fixed, found along the way:
- The file tree refreshes live (new directory watch over the plugin's
own socket), since the plugin owns the `files` kind.
- ctx.sessions.list has no `current` field at either tag; the three
readers were dead. They now use ctx.sidebarRight.mounted.
- package.json.files was missing lib/client-locale.js.
- Two genuinely dead call sites: sessions.openSubagent/open/
refreshSubagents do not exist on the current baseline, so the
topology click, the jump-back and the retry were all no-ops.
- The test harness window shim lacked addEventListener, which the 0.1.7
primitives call at module-evaluation time; and the e2e token exchange
needs Playwright's request stack, not Node's fetch.
This commit is contained in:
+100
-38
@@ -2,15 +2,15 @@
|
||||
|
||||
> 面向 **消费插件开发者**:如何让你的插件向 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+**。当前版本 **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`(类型声明)。
|
||||
> 适用版本:**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.21.0-alpha.1**(npm dist-tag `alpha`;`latest` 仍是 **v0.19.1**,peer 下限 `^0.1.7-alpha.1`,仅支持 DSH **0.1.7-alpha.1+**)。**v0.19.0 移除了自绘右侧面板与自由窗口**(见 §0、§11);**自 v0.20.0 开发线起**(**注意:0.20.0 从未发布到 npm,这些变更全部落在 v0.21.0-alpha.1**)插件**移除了自带的终端**(宿主 0.1.6 的 `ui-sidebar-terminal` 取代,见 §4.4)、**移除了终端固定(pin)**,并在 0.1.7 上**把浏览器视图与只读文件预览整体让给宿主**(`ui-sidebar-browser` / `ui-sidebar-documentpreview`,见 §4.4、§5.4)、**收敛了外链接管**(见 §4.1)、**重写了设置接入面**(`SettingsForms`,见 §8.2)、**给文件树加了实时刷新**(见 §10);同时**删除了轮尾产物行接管**(DSH 0.1.6 把 `conversation.chat.turnTail` 从 chain 改成只能追加的 list,替换语义不复存在)。
|
||||
> 权威代码:`src/client/service.ts`(服务实现)、`src/client/builtins/`(内置 5 tab + 3 viewer 参考实现)、`lib/types/client/service.d.ts`(类型声明)。
|
||||
> 仓库开发规则(硬约束 / CI / 发版)见 [AGENTS.md](../AGENTS.md)。
|
||||
|
||||
---
|
||||
|
||||
## 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 体。插件自己只保留**底部工作台**(分栏树、会话内持久化;**v0.20.0 起其中不再有终端**——宿主 0.1.6 的右侧栏终端取代了它)。对你的接入代码**没有影响**——仍然只调用 `ctx.betterSidebar`:
|
||||
从 v0.19.0-alpha.0 起,**右列完全属于 DSH**:你的 tab 渲染在 **DSH 自己的右侧栏**里(`ctx.sidebarRight` / `ctx.sidebarRightTabs`),插件把每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`)+ 一个原生 tab 体。插件自己只保留**底部工作台**(分栏树、会话内持久化;**其中不再有终端**——宿主 0.1.6 的右侧栏终端取代了它)。对你的接入代码**没有影响**——仍然只调用 `ctx.betterSidebar`:
|
||||
|
||||
- `registerTab` / `registerFileViewer` 签名不变;
|
||||
- `openTab` / `openFile` 默认落到原生右侧栏;新增可选 `OpenTabSeed.target`(`'right'` 默认 / `'bottom'` 落插件的底部工作台);
|
||||
@@ -24,14 +24,16 @@
|
||||
| 去重 | 原生按 `(kind, 地址)` 去重:有 `createTab` 的类型每次新开一个 tab(sidechat / diff),其余聚焦已有 tab;`dedupeKey` 的自定义语义不参与原生面 |
|
||||
| 布局持久化 | 原生栏的布局**只在内存**(刷新后回到折叠默认),插件自己的底部工作台仍然持久化 |
|
||||
| 跨会话打开 | 目标会话的右侧栏 store 未挂载时,打开会排队到该会话上屏后重放 |
|
||||
| 内置类型接管 | 插件的 `editor` 类型以 `extension` 优先级认领 `dsh-resource://file/**`(压过内置 `ui-sidebar-documentpreview` 的 `text` 预览——即 `fallback` 带),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件的文件树);插件卸载/禁用时内置实现自动复位 |
|
||||
| 内置类型接管 | 插件的 `editor` 类型以 `extension` 优先级认领 `dsh-resource://file/**`(压过内置 `ui-sidebar-documentpreview` 的 `text` 预览——即 `fallback` 带),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件的文件树);插件卸载/禁用时内置实现自动复位。**但认领是有选择的**:宿主自己的文档预览已经覆盖的格式(表格 / PDF / 图片 / Office,清单见 §5.4)由 `editor.canOpen` 主动**拒绝**,地址交回内置 `text` 档 |
|
||||
| 文件树实时刷新(v0.21.0-alpha.1+) | 插件接管了内置 `files` 页,所以宿主自己的按目录 watch 覆盖不到这棵树——插件自带一条 `/sidebar/ws/fs-watch` socket:客户端上报**已展开**的目录集,宿主侧按目录 `fs.watch`(150ms 去抖、每连接上限 64 个句柄),变动后只让那一层缓存失效并重列;目录折叠即退订。路径仍走 `fs.tree` 同一道 workspace fence |
|
||||
| 链接接管(v0.21.0-alpha.1+) | DOM 层只接管**有类型通过 `urlTarget` 声明认领**的外链(Ctrl/Cmd/Shift/Alt 点击一律放行);一个都没认领到时**不阻止默认行为**,交回宿主。见 §4.1 的 `urlTarget` |
|
||||
| path 种子的去向(v0.19.2+) | `path` seed 的含义**跟随类型**:只有 `editor`(唯一认领 `dsh-resource://file/**` 的类型)把 path 转成资源地址打开(文件落在编辑器);**其余类型保留页面型打开**,path 随导航 params 落到合成记录的 `tab.path` 供组件消费——组件型 tab 的 path seed 不会被改道到文件编辑器(v0.19.0/0.19.1 上一切 path seed 都被改道,组件从未挂载,#632) |
|
||||
| 终端上限 | 终端 tab 的数量上限只统计插件自己底部工作台里的终端;原生栏里的终端不计入 |
|
||||
| 终端(已交还宿主) | 插件**不再提供任何终端**:宿主 0.1.6 起自带 `ui-sidebar-terminal`(kind `terminal`),插件侧 PTY 栈与 `terminal_*` 工具整体删除。这里不再有「插件终端数量上限」这类语义 |
|
||||
| 底部工作台的开合 | 落到底部工作台的打开一律展开它(新建与聚焦都算),因此 `openTab` 的落点永远可见;开合按钮注册在 DSH 会话头的 utilities 槽(`conversation.session.header.utilities`),不在插件自己的宿主里 |
|
||||
| 新建标签页列表 | 每个 tab 类型在原生 guide 里占一行:标题取 `title` + 图标取 `icon`(缺图标时宿主补一个方块占位),说明取可选的 `description`——**宿主只在 guide 列出的条目 ≤ 4 条时渲染说明**(上游 `MAX_DESCRIBED_ENTRIES = 4`),更长的列表整列丢掉所有说明;未声明 `description` 的条目渲染成单行「图标 + 标题」(rc.1 起 `description` 回到宿主契约,但**宿主与插件都没有兜底句**,所以插件恢复字段而不恢复旧的通用句);`hidden: true` 的类型不占行。插件的 `editor` 类型不再单独占行(它认领的文件资源由 `files` 接管页承载同一视图)。**注意默认组合看不到说明**:插件贡献 6 个 guide 条目(文件 / 文件变动 / 任务管理 / 侧边对话 / 终端 / 浏览器),已超过 4 条上限——要让说明出现,需在插件设置页关掉足够多的 tab 类型把 guide 压到 ≤ 4 条 |
|
||||
| 新建标签页列表 | 每个 tab 类型在原生 guide 里占一行:标题取 `title` + 图标取 `icon`(缺图标时宿主补一个方块占位),说明取可选的 `description`——**宿主只在 guide 列出的条目 ≤ 4 条时渲染说明**(上游 `MAX_DESCRIBED_ENTRIES = 4`),更长的列表整列丢掉所有说明;未声明 `description` 的条目渲染成单行「图标 + 标题」(rc.1 起 `description` 回到宿主契约,但**宿主与插件都没有兜底句**,所以插件恢复字段而不恢复旧的通用句);`hidden: true` 的类型不占行。插件的 `editor` 类型不再单独占行(它认领的文件资源由 `files` 接管页承载同一视图)。**本插件默认贡献 4 个 guide 条目**(文件 / 文件变动 / 任务管理 / 侧边对话,恰好在上限内),**但宿主的终端条目也占一行**——装了宿主终端即是 5 条,说明整列不渲染;要让说明回来,需在插件设置页关掉足够多的 tab 类型把总数压到 ≤ 4 条 |
|
||||
| 新建面板的种子(alpha.2) | 在新会话打开原生新面板时,宿主从已注册的 guide 条目里播种:恰好 1 个条目 → 直接打开那一页;0 或 ≥2 个条目 → 打开指南。`revealIfOpened` 打开的「页面」在**同一 pane 内**强制去重(已在该 pane 就不再新建);由已有 tab 地址驱动的打开不受该去重影响 |
|
||||
| alpha.2 全局面板(不接入) | 插件**不采用** alpha.2 引入的全局主面板模型——根级 keyed `main` 槽(预留 key `conversation`,由 ui-conversation 注册为 `main.conversation`)、根级 `sidebar.panellist` 列表槽(`SidebarPanelMetadata` / `SidebarPanelIconOwnerProps`)、`ctx.layout.selectPanel(MainPanelId|null)` / `beginNavigation()` / `dispose()`、全局标准 prop `usePanelInfo`,以及改根级并新增会话级 `rightbar.session` 子槽的 `rightbar`——这些只作兼容保留,不向其迁移 |
|
||||
| 已移除 | 插件自绘右侧面板(含宽度拖拽 / 新会话默认宽度)与**自由窗口**(`features` 里的 `'floatWindows'` 已删除,v0.18.x 及更早版本的消费者请勿再 gate 该能力);`openByDefault` / `defaultWidthPercent` / `changesDiffFloat` 三个设置项同步删除(旧文档里的键会被忽略) |
|
||||
| 已移除 | 插件自绘右侧面板(含宽度拖拽 / 新会话默认宽度)与**自由窗口**(`features` 里的 `'floatWindows'` 已删除,v0.18.x 及更早版本的消费者请勿再 gate 该能力);`openByDefault` / `defaultWidthPercent` / `changesDiffFloat` 三个设置项同步删除(旧文档里的键会被忽略);**插件自带的终端与浏览器 tab 类型**(宿主 0.1.6/0.1.7 自带两者)与**只读文件预览**(image / pdf / Office / 表格,宿主 0.1.7 的 `ui-sidebar-documentpreview` 接手)同样不再内置,详见 §4.4 与 §5.4 |
|
||||
|
||||
---
|
||||
|
||||
@@ -40,9 +42,9 @@
|
||||
better-sidebar 从 v0.4.0 起把自己改造成一个**注册表服务**:
|
||||
|
||||
- **新页面(tab)**:注册一种新的侧边栏 tab 类型,出现在侧边栏 `+` 菜单里,用户点击后在自己的分栏里打开你的 React 页面;
|
||||
- **文件预览器(file viewer)**:注册一种文件类型预览器,让用户在侧边栏打开文件时走你的渲染组件(覆盖或补充内置的 image/pdf/code 等)。
|
||||
- **文件预览器(file viewer)**:注册一种文件类型预览器,让用户在侧边栏打开文件时走你的渲染组件(覆盖或补充内置的 markdown/html/code)。**宿主自己也有文档预览**——DSH 0.1.7 的 `ui-sidebar-documentpreview` 拿走了表格 / PDF / 图片 / Office 等只读格式,本插件因此**拒绝**认领那些扩展名,你的 viewer 也接不到它们(清单与原因见 §5.4)。
|
||||
|
||||
内置的 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`),插件的同名类型会让读者看到两份实现。
|
||||
内置的 5 个 tab(editor / git——「文件变动」统一 tab(Git 视角 + 本轮文件视角)/ subagent / sidechat / diff)和 3 个 viewer(markdown / html / code)**自己也是通过同一套 API 注册的**(吃自己的狗粮),所以外部插件的能力与内置功能完全对等。**不再内置 terminal / browser 两个 tab 类型**:DSH 0.1.6 自带右列终端(`ui-sidebar-terminal`、kind `terminal`),浏览器是 0.1.6 的 `ui-sidebar-browser`(kind `browser`,**0.1.7 起只在 desktop profile 挂载**,Web profile 没有这个 kind);插件的同名类型会让读者看到两份实现。同理,0.1.7 的 `ui-sidebar-documentpreview` 让插件的 image / pdf / Office / 表格预览失去意义,那三个 viewer 描述符(image / pdf / binary-download)已删除。
|
||||
|
||||
关键机制一句话:better-sidebar 的 client half 在 `apply()` 开头执行 `ctx.provide('betterSidebar', service)`(`src/client/index.tsx`),消费插件在 `inject` 里声明 `'betterSidebar'`,Cordis 保证服务就绪后才激活你的插件,然后你调用 `ctx.betterSidebar.registerTab(...)` / `registerFileViewer(...)` 完成注册,返回的 disposer 由 Cordis fiber 在卸载(HMR / 禁用)时自动调用。
|
||||
|
||||
@@ -217,11 +219,19 @@ interface TabDescriptor {
|
||||
*/
|
||||
createTab?: (state: SidebarState) => { tab: SidebarTab; patch?: Partial<SidebarState> } | null
|
||||
/**
|
||||
* 外链认领(v0.13.0+,`features.includes('urlTarget')` gate):外链被接管
|
||||
* (`browserInterceptLinks` 总闸 + 协议开关均开)时,第一个 urlTarget 命中且未禁用的
|
||||
* 类型以 openTab({ type, url, title: hostname }) 打开,URL 预填 tab.path。先到先得;
|
||||
* 谓词抛错被吞。v0.19.1 及更早内置 browser 隐式兜底;**v0.20.0 起本插件不再有
|
||||
* browser 类型**,无可认领的默认目标时接管直接放行给系统浏览器。
|
||||
* 外链认领(v0.13.0+,`features.includes('urlTarget')` gate):声明后,DOM 层
|
||||
* 捕获到的外链点击里**只有被某个启用类型认领的那些**会被接管——第一个
|
||||
* urlTarget 返回 true 的类型以 openTab({ type, url, title: hostname }) 打开,
|
||||
* URL 预填 tab.path。先到先得;谓词抛错被吞。
|
||||
*
|
||||
* **没被认领的链接一律放行**(插件不 preventDefault):DSH 0.1.7 起正文链接的
|
||||
* 去向由宿主的用户设置 `linkOpening` 决定(进侧栏还是新标签页),插件自绘
|
||||
* markdown 里则走 `<a>` 自己的默认行为。**v0.21.0-alpha.1 起那三个「按协议分流」
|
||||
* 的旧设置项已删除**(旧文档里的键会被忽略),本插件不再有任何接管总闸——
|
||||
* 认领与否完全由你的 urlTarget 决定;Ctrl/Cmd/Shift/Alt 点击永远绕过接管。
|
||||
* 认领成功但目标类型在打开那一刻已不可用(插件卸载 / 被设置关闭)时,点击
|
||||
* 兜底为 `window.open(url, '_blank', 'noopener,noreferrer')`,不会变成静默无反应。
|
||||
*
|
||||
* 多 URL 并存需 createTab 铸造 per-URL id,否则二次点击被 id 安全网聚焦、不覆写 path。
|
||||
*/
|
||||
urlTarget?: (url: URL) => boolean
|
||||
@@ -256,7 +266,7 @@ interface TabDescriptor {
|
||||
|
||||
/** 声明式设置声明(行控件形状见上方 SettingRow)。 */
|
||||
interface SidebarSettingsDeclaration {
|
||||
/** 宿主 prefs 字段行(key 必须是宿主 PrefsSchema 的字段,内置键清单见 §8)。 */
|
||||
/** 偏好字段行(key 必须落在本插件的 `PrefsSchema` 里——它已并入本插件 Loader 行的 `Config`,内置键清单见 §8)。 */
|
||||
toggles?: readonly SettingRow[]
|
||||
/** 插件自有设置行(v0.12.0+):key 插件局部,
|
||||
* 持久化在 pluginSettings[<descriptor id>],无需宿主 schema 字段。 */
|
||||
@@ -405,12 +415,12 @@ ctx.effect(() => {
|
||||
|
||||
你的 `id` 不可与上述重复,否则 `registerTab` 抛 `"tab type \"X\" already registered"`。
|
||||
|
||||
**v0.20.0 删除的类型(迁移提示)**:
|
||||
**自 v0.20.0 开发线起删除的类型(迁移提示;0.20.0 从未发布,变更落在 v0.21.0-alpha.1)**:
|
||||
|
||||
| 原 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` 机制不变) |
|
||||
| `browser` | 宿主自带 `kind: 'browser'`(`@deepseek-ai/dsh-client-ui-sidebar-browser`),**但 DSH 0.1.7 起只在 desktop profile 挂载**(`web-app` 的 patch 里 `disabled: ctx.get('profileContext')?.name !== 'desktop'`)——Web profile 上**没有这个 kind**,`openTab('browser', …)` 必然失败 | 不要再注册同名 kind。要打开网页:desktop profile 走 `ctx.sidebarRight.openTab('browser', { params: { url } })`(务必 try/catch 兜底,宿主未装该包时 `openTab` 会抛);Web profile 只能自己提供页面,或把链接交回宿主(正文链接由宿主的 `linkOpening` 设置决定去向)。插件侧的 `api.browserProbe` / `SidebarPrefs.browserNoSandbox` / `browserAllowedLoopback` **以及三个「按协议分流」的外链接管偏好键全部已删除**;`urlTarget` 机制保留,但不再有接管总闸(见 §4.1) |
|
||||
|
||||
**同时删除的还有**:轮尾产物行接管。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`)。
|
||||
|
||||
@@ -430,7 +440,7 @@ interface FileViewerDescriptor {
|
||||
icon?: ReactNode | ((size: number) => ReactNode)
|
||||
/** 小写无点的扩展名数组:['png','jpg']。[] = catch-all(仅最低优先级盲命中有效) */
|
||||
exts: readonly string[]
|
||||
/** 优先级(高优先);默认 0。内置:image/pdf/markdown/html=0,binary-download=-50,code=-100 */
|
||||
/** 优先级(高优先);默认 0。内置:markdown/html=0,code=-100 */
|
||||
priority?: number
|
||||
/** 字节获取策略 */
|
||||
fetchStrategy: 'none' | 'fsRead' | 'mediaUrl' | 'custom' | 'binary-download'
|
||||
@@ -476,7 +486,7 @@ interface FileViewerProps {
|
||||
| `fsRead` | `/sidebar/api` 的 `fs.read` | `content`, `truncated` | 文本类(CSV/JSON/XML) |
|
||||
| `mediaUrl` | `/sidebar/file` 媒体路由 URL | `mediaUrl` | 图片/PDF(viewer 自己 fetch 字节) |
|
||||
| `custom` | viewer 的 `load()` 函数 | `customData` | 自定义协议(如远程拉取) |
|
||||
| `binary-download` | 不预览,显示下载按钮 | (无) | 无客户端渲染器的二进制格式 |
|
||||
| `binary-download` | 不预览,显示下载按钮 | (无) | 无客户端渲染器的二进制格式。**本插件现在没有任何内置 viewer 用这条策略**(`binary-download` viewer 已让给宿主);策略本身仍在契约里,你的 viewer 可以照用 |
|
||||
|
||||
### 5.4 匹配算法(`matchFileViewer`)
|
||||
|
||||
@@ -489,8 +499,14 @@ interface FileViewerProps {
|
||||
|
||||
> **head 字节从哪来**:第一次匹配(纯扩展名)没有 head。`fsRead` 策略读取后若文件为二进制,host 的 `fs.read` 响应会带 `head` 字段(base64,前 4KB,`src/index.ts` 的 `READ_HEAD_LIMIT`),编辑器会用它对 `detect` viewer **重匹配一次**——所以 detect 型 viewer 的实际触发场景是"扩展名匹配落空/二进制文件"。文本文件的 detect 嗅探不在内置流程内(用 `exts` 或 `custom` 策略替代)。
|
||||
|
||||
> **内置 viewer**(不可重复注册,全部 6 个):image(0) / pdf(0) / markdown(0, fsRead;内嵌 HTML 支持:DOMPurify 白名单消毒、`<details>` 跨段嵌套、本地媒体 src 重写走 `/sidebar/file`;≥3 标题时浮动目录大纲。实现 `markdown-html.ts` / `MarkdownHtml.tsx` / `md-toc.tsx`,[设计文档](plans/2026-08-24-markdown-html-toc-design.md)) / html(0, fsRead, 沙箱 iframe 预览) / code(-100, catch-all, fsRead) / binary-download(-50, exts doc/xls/ppt + NUL detect)。Office 三件套预览(.docx/.xlsx/.pptx)**不再内置**——已迁至推荐插件(设置页「添加插件」→ 文件预览弹窗里的 Office 预览插件),以相同 id 注册。
|
||||
> code 是兜底 viewer:任何其他 viewer 未认领的文件都会落到 code(CodeMirror 文本编辑);二进制文件经 head 重匹配被 binary-download 的 NUL detect 认领(下载按钮)。外部 viewer 注册同扩展名 + 更高 priority 即可覆盖。
|
||||
> **内置 viewer**(不可重复注册,全部 3 个):markdown(0, fsRead;内嵌 HTML 支持:DOMPurify 白名单消毒、`<details>` 跨段嵌套、本地媒体 src 重写走 `/sidebar/file`;≥3 标题时浮动目录大纲。实现 `markdown-html.ts` / `MarkdownHtml.tsx` / `md-toc.tsx`,[设计文档](plans/2026-08-24-markdown-html-toc-design.md)) / html(0, fsRead, 沙箱 iframe 预览 + `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` 两个宿主没有的逃生门) / code(-100, catch-all, fsRead, **可编辑** CodeMirror + 保存)。
|
||||
> code 是兜底 viewer:任何其他 viewer 未认领的文件都会落到 code(CodeMirror 文本编辑);二进制文件经 head 重匹配找不到 sniffer,落到编辑器的下载面板。外部 viewer 注册同扩展名 + 更高 priority 即可覆盖内置的那三个。
|
||||
>
|
||||
> ⚠️ **让给宿主的格式(重要,影响你的 viewer 能不能被调用)**:DSH 0.1.7 的 `ui-sidebar-documentpreview` 自带 code / excel(xlsx,xls,csv,tsv) / office(宿主侧转 PDF)/ pdf / image / html / markdown / text 预览,并带缩放与**按目录自动刷新**——本插件原有的 image / pdf / binary-download 三个 viewer 描述符因此删除。更重要的是**路由**:插件的 `editor` 类型在 `canOpen` 里对下列扩展名一律返回 false(`src/client/native/index.ts` 的 `HOST_OWNED_EXTS`),把地址让给内置 `text` 档,**根本不会进入本插件的 `matchFileViewer`**:
|
||||
>
|
||||
> `xlsx xls xlsb xlt xltx xltm ods ots fods csv tsv pdf png jpg jpeg gif webp svg bmp ico avif doc docx dot dotx ppt pptx`
|
||||
>
|
||||
> 后果:**你注册这些扩展名的 viewer 仍然合法(注册表不冻结 id,`matchFileViewer` 也会命中),但通过聊天 / 文件树 / 编辑器的正常文件打开路径拿不到它们**——那是宿主的文档预览。要在这些格式上做文章,只能自己在页面里渲染(或像 [Office 预览插件](https://github.com/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office) 那样在宿主接管前的旧版本上生效)。**可用的是 md/markdown / html/htm 与 code catch-all 三类**(外加任何不在上表里的扩展名)。未知二进制(`.zip` / `.wasm`)仍走 code 认领 → `fs.read` 判 binary → head 重匹配无 sniffer → 编辑器渲染下载面板,功能不回归。
|
||||
|
||||
### 5.5 注册示例
|
||||
|
||||
@@ -511,20 +527,22 @@ ctx.effect(() =>
|
||||
)
|
||||
```
|
||||
|
||||
**覆盖内置 image viewer**(如自定义 SVG 优化渲染):
|
||||
**覆盖内置 markdown viewer**(如自家渲染管线的只读预览):
|
||||
|
||||
```ts
|
||||
ctx.effect(() =>
|
||||
ctx.betterSidebar.registerFileViewer({
|
||||
id: 'my-plugin:svg-pro',
|
||||
exts: ['svg'],
|
||||
priority: 10, // 高于内置 image 的 0
|
||||
fetchStrategy: 'mediaUrl',
|
||||
component: ({ mediaUrl }) => <OptimizedSvg src={mediaUrl} />,
|
||||
id: 'my-plugin:md-pro',
|
||||
exts: ['md', 'markdown'],
|
||||
priority: 10, // 高于内置 markdown 的 0
|
||||
fetchStrategy: 'fsRead',
|
||||
component: ({ content }) => <MyMarkdown source={content ?? ''} />,
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
> ⚠️ 只有内置仍认领的格式能被这样覆盖(md/markdown、html/htm,以及 code catch-all 接的其它扩展名)。`.svg` / `.png` / `.pdf` / `.docx` 之类**不要**照这个例子注册——地址在 `canOpen` 就被让给宿主的文档预览了,你的 viewer 永远不会被调用(原因与完整清单见 §5.4)。
|
||||
|
||||
**内容嗅探**(按 magic bytes 路由,忽略扩展名):
|
||||
|
||||
```ts
|
||||
@@ -568,8 +586,8 @@ const { value } = await res.json() // 错误时 { ok: false, error: { code, me
|
||||
| `fs.read` | 读文件:文本返回 `{ kind: 'text', content, truncated }`;二进制返回 `{ kind: 'binary', size, truncated, head }`(head = base64 前 4KB) |
|
||||
| `fs.write` | 原子写文件 |
|
||||
| `git.status` / `git.diff` / `git.log` 等 | 全套 Git 只读 + 写操作 |
|
||||
| `pty.close` / `agent-pty.close` | 释放终端(外部 tab 一般用不到) |
|
||||
| `settings.get` / `settings.update` | 侧边栏偏好读写(revision 守卫) |
|
||||
| `pty.close` / `agent-pty.close` | **已删除**(插件自带的 PTY 栈随终端一起移除;宿主 `ui-sidebar-terminal` 不通过本插件的路由暴露控制面) |
|
||||
| `settings.get` / `settings.update` | 侧边栏偏好读写(revision 守卫,冲突回 wire 错误码 `settings-conflict`);后端在 0.1.7 上就是宿主的 `SettingsForms`,见 §8.2 |
|
||||
|
||||
> **文件路径安全边界**:`fs.tree`、`fs.read`、`fs.write`、`/sidebar/file`、`/sidebar/html` 和 `/sidebar/upload` 都以请求对应 session 的权威 `cwd` 作为 workspace 根目录。路径会按真实文件系统路径检查,越界绝对路径、`..` 解析结果和指向 workspace 外部的符号链接都会被拒绝;消费插件不应把 `cwd` 当作可由用户扩大权限范围的参数。
|
||||
|
||||
@@ -634,10 +652,10 @@ interface BetterSidebarService {
|
||||
* 合成 tab),createTab 铸造的 title/meta 作缺省、seed 字段优先(v0.19.2+ 起
|
||||
* path 也随导航 params 下发,见下)。
|
||||
* path 可选:含义跟随类型——editor(唯一认领 dsh-resource://file/** 的
|
||||
* 类型)把 path 转成资源地址打开(文件落在编辑器);其余类型 path 是
|
||||
* 组件种子,随导航 params 落到 tab.path(v0.19.2+;0.19.0/0.19.1 把一切
|
||||
* path seed 都改道文件资源打开,组件型 tab 的组件不会挂载,#632)。
|
||||
* url 可选:把**新建** tab 的 path 预填为 URL(侧边栏浏览器导航种子);
|
||||
* 类型,但会拒绝宿主的文档预览格式,见 §5.4)把 path 转成资源地址打开(文件落在
|
||||
* 编辑器);其余类型 path 是组件种子,随导航 params 落到 tab.path(v0.19.2+;
|
||||
* 0.19.0/0.19.1 把一切 path seed 都改道文件资源打开,组件型 tab 的组件不会挂载,#632)。
|
||||
* url 可选:把**新建** tab 的 path 预填为 URL(网页类 tab 的导航种子);
|
||||
* 聚焦既有 tab 时 url 不会覆写其 path。
|
||||
* 被设置禁用的类型是 no-op(console.warn 提示)。注意:available 不拦截 openTab。
|
||||
* scope(v0.12.0+)定向到指定 session:给出且非当前 session 时,打开落在
|
||||
@@ -859,7 +877,49 @@ ctx.effect(() =>
|
||||
)
|
||||
```
|
||||
|
||||
> ⚠️ **`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;自定义面板自行负责)。
|
||||
> ⚠️ **`toggles` 的 key 必须是本插件 `PrefsSchema` 的字段**(`PrefsSchema` 已并入本插件 Loader 行的 `Config`;内置键:`autoOpenSubagent` / `autoOpenJobs` / `agentOpenTools` / `editorExplorer` / `workspaceFence` / `titleBarScheme` / `titleBarPresetId` / `customCss` / `titleBarCompat` / `titleBarStripPx` / `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` / `tabsEnabled` / `viewersEnabled` / `pluginSettings`;**已删除**:`agentTerminalTools` / `terminalShell` / `terminalShellArgs` / `terminalFontFamily` / `terminalFontSize` / `bottomPanelAutoTerminal` / `browserNoSandbox` / `browserAllowedLoopback` / 三个按协议分流的旧外链接管键)。**v0.12.0 起设置 seam 已开放**:你自己的设置走 `pluginToggles`(声明式行)或 `render`(自定义面板),值持久化在 `pluginSettings[id]`——不再需要本插件的 schema 字段,也不再被 seam 丢弃。值须 JSON 可序列化(行控件只产出 string/number/boolean;自定义面板自行负责)。
|
||||
|
||||
### 8.2 宿主设置表单(DSH 0.1.7+:`SettingsForms`)
|
||||
|
||||
**0.1.7 把可注册的设置命名空间整体删除了。** `ctx.settings.register(ns, schema)` 与它返回的 `get` / `watch` / `update` / `replace` 都已不存在,取而代之的是 `SettingsForms`——一套**按 profile 条目寻址**的表单服务:
|
||||
|
||||
```ts
|
||||
interface SettingsForms {
|
||||
/** 只列出「导出了 Config 且 fiber 处于 ACTIVE」的条目;ns 就是 Loader 行的 entry id。 */
|
||||
describe(options?: { redactSecrets?: boolean }): SettingsDescriptor[]
|
||||
/** 把 patch 合并进该条目的 config(revision 不符时抛 SettingsConflictError)。 */
|
||||
update(ns: string, patch: object, expectedRevision?: number): Promise<void>
|
||||
/** 重置全部 live 字段后再写入给定字段(普通 config 保留)。 */
|
||||
replace(ns: string, section: object, expectedRevision?: number): Promise<void>
|
||||
/** 按路径逐字段改(持不完整视图——例如被脱敏的秘密字段——时用它,别用 replace 重述整节)。 */
|
||||
mutate(ns: string, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise<void>
|
||||
/** 声明本插件实例的页面策略(自带设置页的插件写 auto: false);返回 disposer。 */
|
||||
configure(presentation: { auto?: boolean }, owner?: Fiber): () => void
|
||||
get writable(): boolean
|
||||
get documentPath(): string
|
||||
prepareDocument(): Promise<string>
|
||||
}
|
||||
|
||||
interface SettingsDescriptor {
|
||||
ns: string // profile 条目 id —— 不是插件自选的命名空间
|
||||
autoGenerate: boolean
|
||||
schema: unknown
|
||||
value: unknown // 已解析(defaults → composition base → 用户层)
|
||||
revision: number
|
||||
base?: unknown
|
||||
user?: unknown // 只读的 profile 覆盖层;为空 = 该条目还没被用户写过
|
||||
applies: 'live'
|
||||
secrets?: RedactedSecret[]
|
||||
}
|
||||
```
|
||||
|
||||
四条必须记住的新规则:
|
||||
|
||||
1. **命名空间 = 插件 Loader 行的 `entry.options.id`**,不是插件自选的字符串。聚合包会用别的 id 挂同一个包,所以**运行时自发现**(本插件 `src/index.ts` 的 `ownEntryId(ctx)`:按 `entry.options.name === '<包名>'` + `entry.fiber === ctx.fiber` 匹配,回退到第一个启用的同名行),**不要硬编码** id。
|
||||
2. **schema 来自插件模块导出的 `Config`**(`entry.fiber.runtime.Config`)。用户可见的字段必须**并进这个 `Config`**——本插件把 `PrefsSchema` 合并进 `Config`(`src/config.ts`),并把每个偏好字段标 `meta.volatile = true`;**这一个标记就是「改设置实时生效、不重挂插件」的全部机制**(Loader 的 `equalExceptVolatile` 跳过 volatile 字段,纯 volatile 变更提交进运行中的引用并发 `loader/volatile-update`,普通字段仍是重挂语义)。
|
||||
3. **自带设置页的插件要主动退出自动表单**:`ctx.effect(() => ctx.settings.configure({ auto: false }, ctx.fiber))`,否则 Settings 会把同一批字段渲染两遍(策略不影响读写)。
|
||||
4. **`settings/document-updated` 你监听不到,别写**:事件签名是 `'settings/document-updated'(ns, revision)`(语义是「该条目的表单值 / 可用性 / 页面策略变了,表单客户端重新读取 schema、解析值与 revision」),但它在 **settings 服务自己的 context 上 `emit`**,而 cordis 事件只向该 ctx 的**祖先**冒泡——插件 fiber 是兄弟,`ctx.on(...)` 永远不会触发。**替代做法:每次重新读取表单时重新求值**(本插件的 `settingsFace.get()` 在返回当前值前跑一遍门控同步;客户端侧另经 `remote` 服务的 `$on('settings/document-updated')` 镜像触发重新拉取)。**旧文档里的 `settings/updated` 事件在 0.1.7 已不存在**。
|
||||
|
||||
|
||||
### 8.1 内置键 `workspaceFence`(工作区路径围栏)
|
||||
|
||||
@@ -891,10 +951,11 @@ ctx.effect(() =>
|
||||
| **id 冲突** | `registerTab` / `registerFileViewer` 对重复 id 抛错;建议用包前缀(`my-plugin:xxx`) |
|
||||
| **不要 value-import `dsh-better-sidebar`** | 即使是 `./client/service` 子路径,运行时落点也是 client bundle;只做 type-only import |
|
||||
| **client 声明图零 Node 依赖(v0.12.0+)** | `dsh-better-sidebar/client/service` 的可达声明面(含 `Context`)不引用 `node:*` / `Buffer`——纯浏览器侧插件无需 `@types/node`,`skipLibCheck: false` 也能编译(`scripts/check-consumer-types.sh` 守护;主入口含宿主声明,宿主消费者本就处于 Node 环境) |
|
||||
| **家族右面板互斥(v0.13.0+)** | `aionui-panel` 设置的 `rightPanel` 解析为 `'aionui-panel'` 时整个侧边栏不挂载(`settings.get` 返回 `externalDisable: true`,挂载门 + 接管停用;`settings/document-updated` 推送实时生效,无 `remote` 服务回退启动时判定)。未安装 aionui 不受影响 |
|
||||
| **家族右面板互斥(v0.13.0+)** | `aionui-panel` 设置的 `rightPanel` 解析为 `'aionui-panel'` 时整个侧边栏不挂载(`settings.get` 返回 `externalDisable: true`,挂载门 + 接管停用)。**DSH 0.1.7 起 `settings/updated` 事件已删除**,保留的 `settings/document-updated` 是在 **settings 服务自己的 context 上 emit 的、插件 fiber 监听不到**(见 §8.2 第 4 条)——所以实时性由「每次重新读取时重新求值 + 客户端经 `remote` 镜像重新拉取」提供,不要写成 `ctx.on('settings/document-updated')` |
|
||||
| **设置持久化位置(0.1.7 起)** | 偏好不再写 `~/.dsh/settings.yaml`,而是落在 **profile 的 cordis patch 文档**里,按插件 Loader 行的 entry id 组织(本插件默认 `better-sidebar`)。0.1.6 线的 `dsh-better-sidebar` 段由插件在**首次启动时一次性回迁**(只在该行用户层仍为空时执行,且只迁移当前 `Config` 仍声明的字段),所以用户设置不会丢。排障时先看 profile patch,而不是旧的 `settings.yaml`(服务已把它改名为 `settings.yaml.imported`) |
|
||||
| **i18n 跟随** | 文案跟随 DSH `ctx.locale`(词典在 `betterSidebar` 命名空间;Host-backed `locale.preference` 优先于浏览器语言并实时切换;缺失回退浏览器)。消费插件**不要**依赖内部 `t()`——标题传字符串或 `() => string` |
|
||||
| **第三语言覆盖(ja 等)** | 可选 peer `@huanlin/dsh-plugin-better-locale`(optional)提供 ja/ko 覆盖,**借用 DSH 英文槽位**(仅 DSH=en 时生效,zh 下惰性)。经 `ctx.get('betterLocale')` 注入 `t()`;未安装整段 no-op |
|
||||
| **懒加载 chunk** | 重依赖(xterm/CodeMirror)在独立 bundle(`lib/client-<name>.js`),经 `/sidebar/bundle` 按需下发;factory 赋到 `globalThis.__dshChunks__[<name>]`,由 `src/client/chunk-loader.ts` 物化,**不经** `__ModuleLoader__`。对消费插件透明 |
|
||||
| **懒加载 chunk** | 重依赖(CodeMirror / mermaid / 19 份第三语言词典)在独立 bundle(`lib/client-<name>.js`,现存 `editor` / `mermaid` / `locale` 三个),经 `/sidebar/bundle` 按需下发;factory 赋到 `globalThis.__dshChunks__[<name>]`,由 `src/client/chunk-loader.ts` 物化,**不经** `__ModuleLoader__`。对消费插件透明 |
|
||||
| **聊天文件打开漏斗(0.1.5 宿主)** | 聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 DSH 原生右侧栏的 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`(`packages/client/ui-chat/src/client/apply.ts` 是唯一调用点);`remote.session.openWorkspacePath` 在 0.1.5 客户端**已无调用者**,better-sidebar 的 openpath 拦截随之删除。你的插件若要观测/旁路聊天文件打开,注册原生 tab 类型认领 `dsh-resource://file/**`(见 [AGENTS.md §3](AGENTS.md) 第 10 条);不要假设 `ctx.workspaces.openPath`(已删除)或旧 `remote.session` 漏斗存在 |
|
||||
| **原生 tab 体的高度契约** | 你的 tab 组件被挂在**全高列 flex 宿主**里:native 适配层给每个 tab 体(含 orphaned 回退)包一层 `height:100%` 的 `sidebar.module.css` `.nativeTabHost`(带 `data-dsh-native-tab-host`)——根用 `flex: 1` / `height: 100%` 并加 `min-height: 0` 填满面板。宿主的面板体 `.paneBody`(`dsh-client-ui-dockkit`)是**有确定高度的块级滚动容器,不是 flex 容器**,因此只靠外层 flex 容器支撑的根会塌成内容高度(sidechat 的输入框就贴不到面板底);DSH 自己的 tab 体(`ui-sidebar-documentpreview` 的文档预览、`ui-sidebar-files` 的 FilesBody)正是因此在根上声明 `height: 100%` |
|
||||
|
||||
@@ -1029,7 +1090,7 @@ function parseCsv(text: string): string[][] { /* ... */ }
|
||||
|
||||
better-sidebar 的内置 tab 和 viewer 就是参考实现("吃狗粮"),调试时直接读:
|
||||
|
||||
- **`src/client/builtins/`**:7 个内置 tab(tabs.tsx)+ 6 个内置 viewer(viewers.tsx)的注册代码 + 聚合与 disposer 生命周期(index.ts);Office 预览见 plugins-viewers.ts
|
||||
- **`src/client/builtins/`**:5 个内置 tab(tabs.tsx)+ 3 个内置 viewer(viewers.tsx)的注册代码 + 聚合与 disposer 生命周期(index.ts);Office / 表格 / 图片 / PDF 预览**不在插件里**(宿主的 `ui-sidebar-documentpreview` 负责,见 §5.4)
|
||||
- **`src/client/service.ts`**:`BetterSidebarService` 接口 + `createBetterSidebarService` 工厂实现(含匹配算法、dedupe、createTab、启用态 gating)
|
||||
- **`src/client/Sidebar.tsx`**:底部工作台外壳 + `TabContent` 分发(查 `getTab` → 调 descriptor.component;未注册 → `<OrphanedTab/>`)、`+` 菜单构建(order 排序 + available disabled + 禁用过滤)
|
||||
- **`src/client/native/`**:原生右侧栏接入(tab 类型注册、合成 `SidebarTab` 适配、资源地址、跨会话打开排队)
|
||||
@@ -1040,7 +1101,8 @@ better-sidebar 的内置 tab 和 viewer 就是参考实现("吃狗粮"),
|
||||
- **`src/client/FileTree.tsx`** / **`TreePanel.tsx`** / **`src/fs-search.ts`**:文件树 / 树面板 / host 文件名搜索(`fs.search`;`tests/fs-search.spec.ts`)
|
||||
- **`src/client/markdown-html.ts`** / **`MarkdownHtml.tsx`** / **`md-toc.tsx`**:markdown 内嵌 HTML 管线与目录大纲(注意 `md-toc.tsx` 头注释的「子组件读父 ref 为 null」时序陷阱)
|
||||
- **`src/agent-opens.ts`** / **`/sidebar/ws/agent-opens`**:模型主动打开(`sidebar_open` 工具 + `agentOpenTools` 设置,默认关闭);文件夹窗口 = `meta.dir: true` 的 editor tab([设计文档](plans/2026-08-23-agent-open-tools-design.md))
|
||||
- **`tests/service.spec.ts`** / **`tests/builtins.spec.ts`**:注册表生命周期 / 匹配算法 / dedupe / createTab / 启用态 gating;内置清单断言(7 tab + 6 viewer + 声明式元数据)
|
||||
- **`tests/service.spec.ts`** / **`tests/builtins.spec.ts`**:注册表生命周期 / 匹配算法 / dedupe / createTab / 启用态 gating;内置清单断言(5 tab + 3 viewer + 声明式元数据,并显式断言 image / pdf / binary-download **不在**内置清单里、但作为公开契约仍可被第三方注册)
|
||||
- **`src/fs-watch.ts`** / **`src/client/use-dir-watch.ts`**:文件树实时刷新(按展开目录 watch,150ms 去抖、每连接 64 个句柄上限;`tests/fs-watch.spec.ts` 守护)
|
||||
- **`docs/plans/`**:逐特性设计文档(含实施偏差记录,以现状为准);入口如 `2026-08-11-service-registry-design.md` / `2026-08-11-declarative-sidebar-settings-design.md`
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user