New global Side card setting `agentOpenTools` (default off, 常规 group): when enabled, one `sidebar_open` tool is injected into the model so it can actively open a local file, a local folder (tree rooted there), or an HTTP(S) page in the calling session's sidebar. - host: AgentOpenRegistry + registerOpenTool (src/agent-opens.ts); the tool classifies http(s) URLs / stat-driven file/folder paths, refuses other schemes, resolves relative paths against the session cwd, and reports target-tab-disabled instead of silently no-oping. Gated like agentTerminalTools (register/drain on settings watch). - delivery: /sidebar/ws/agent-opens push (consume-on-send queue, replay on attach); the tool result reports `delivered` so queued opens for non-active sessions are honest. - client: Sidebar subscribes and routes pushes to editor (file), folder window (meta.dir, tree rooted at the folder — EditorHost), or browser (URL) tabs; settings row in 常规 + 20 locale dictionaries. - tests: agent-opens registry/tool spec, smoke gating + WS route list, prefs/plugin-shape defaults, editor-host folder window, side-card rows. - docs: design doc, AGENTS.md key lists + reference, README feature bullets.
6.8 KiB
6.8 KiB
设计:模型主动在侧边栏打开(sidebar_open 工具 + agentOpenTools 设置)
日期:2026-08-23 · 状态:已实现 · 仓库:omdsh-dev/DSH-better-sidebar
需求
新增一个侧边栏全局设置(Side card 设置页「常规」分组):agentOpenTools,默认关闭。
开启后向模型注入一个工具 sidebar_open,允许模型主动在调用方会话的侧边栏中打开:
- 本地文件 → 内置 editor tab(按 path 去重,聚焦既有 tab)
- 本地文件夹 → 内置 editor tab 的文件夹窗口(文件树以该目录为根)
- HTTP(S) 网页 → 内置 browser tab(URL 预填)
关键决策
1. 作用域 = 调用方 agent 会话
与 terminal_* 工具一致:工具只绑定 exec.agent.session.id,模型不传 sessionId。
子代理(或未激活会话)调用时若无客户端连接,打开请求排队,该会话侧边栏下次可见时重放。
工具结果通过 delivered 如实报告「当前是否已推送到已连接的侧边栏视图」。
2. 投递通道:host→client WS 推送(无 ack 协议)
- 新增 WS 升级路由
/sidebar/ws/agent-opens?sessionId=...(与/sidebar/ws/agent-terminals同模式,过同一 trust fence)。 - host 侧
AgentOpenRegistry维护会话级队列 + 已连接视图集合:enqueue:有订阅者 → 即时推送并消费(delivered: true);无订阅者 → 保留队列(delivered: false)。attach:注册订阅者并重放队列(消费即出队),返回 disposer(close/error 时退订)。drainAll:特性关闭时丢弃未投递队列。
- 消费即出队是关键:browser tab 无 per-URL 去重,重连重放会铸造重复 tab;消费语义保证每个请求恰好推送一次。
- 不加 ack 回执、不加
/sidebar/api路由——结果的最佳努力语义(delivered)已足够,符合 KISS 与 terminal 先例。
3. sidebar_open 工具(唯一新增工具)
- 参数:
target(必填:绝对/相对路径,或 http(s) URL)、title(可选:tab 标题)。 - 执行流程:
throwIfAborted→ 分类 → 目标 tab 启用检查 → 入队 → 返回规范结果。 - 分类规则:
http:///https://(大小写不敏感)→url,默认标题 = hostname;- 其他 scheme(
file:/javascript:/vscode:等)→ 报错(Windows 盘符前缀C:\/C:/除外,它是路径不是 scheme); - 本地路径 → 相对路径以会话 cwd(
sessionCwdOf)解析为绝对路径,stat定file/folder;不存在/不可读 → 报错; - 目标 tab 类型被用户禁用(
tabsEnabled['editor'|'browser'] === false)→ 报错并提示启用(避免客户端静默 no-op)。
- 输出(canonical JSON,
render纯文本投影):{ kind, target, title, delivered }。 - 工具描述/错误消息为英文(与 8 个
terminal_*一致)。
4. 文件夹窗口:editor tab 的 meta.dir
- 打开文件夹 =
openTab({ type: 'editor', path: <dir>, id: 'editor:'+<dir>, title, meta: { dir: true } })。 - EditorHost 识别
meta.dir === true:不进入 viewer 加载流程,渲染全窗 TreePanel(cwd = tab.path); FileTree 根部即cwd(fs.tree接受绝对路径),搜索结果/上传仍受会话工作区约束(已知限制,见下)。 meta为可持久化的 tab 状态(v0.12.0+),sanitizeState原样保留——刷新后文件夹窗口恢复。- 折叠/展开、右键打开、拖拽上传等全部复用 TreePanel 既有能力。
5. 设置与门控
- 新 prefs 字段
agentOpenTools: boolean(SidebarPrefs+PrefsSchema+ 客户端parsePrefs),默认false。 - SideCardSection「常规」分组新增一行 Switch(标题/描述走新 locale key)。
- host 门控完全拷贝
agentTerminalTools的syncToolsGate模式:- 开启 →
registerOpenTool(...);关闭 → 注销工具 +agentOpenRegistry.drainAll(); - 一次
scope.watch同时驱动两个门控(各持有自己的 disposer,互不干扰); - 工具不依赖 node-pty,降级模式下也可用。
- 开启 →
- 客户端防御:Sidebar 的 agent-opens 推送订阅在
prefs.agentOpenTools !== true时忽略消息(host 已门控,此为双保险)。
6. 与既有拦截开关的关系
- URL 打开不受
browserInterceptLinks/browserInterceptHttp/browserInterceptHttps约束——那是用户点击外链的接管开关; 模型显式调用sidebar_open只受 browser tab 自身启用开关约束(openTab 既有 gating)。 - 文件打开不受
interceptOpenPath影响(那是聊天点击路径的接管开关)。
既有模式复用(零新公共 API)
| 新事物 | 复用/镜像的既有模式 |
|---|---|
agentOpenTools 门控 |
agentTerminalTools 门控(settings seam watch → 注册/注销) |
/sidebar/ws/agent-opens |
/sidebar/ws/agent-terminals 推送(升级注册、fence、每会话 attach) |
| 客户端推送订阅 | Sidebar 的 agent-terminals effect(重连上限 FAILURE_LIMIT、防抖/清理) |
| 文件/URL 打开 | service.openFile / openTab({ type:'browser', url })(link-intercept 同款) |
| 文件夹窗口 | path-less explorer 的全窗 TreePanel 分支(仅根目录改为 tab.path) |
不新增 BetterSidebarService 方法;不改变任何现有路由协议。
已知限制
- 多浏览器窗口:推送按「有订阅者即发送并消费」执行,多窗口同时连接时后连接的窗口不会收到已消费的请求 (其本体状态由 localStorage 恢复——与 agent-terminals 的全量列表 fan-out 不同,属尽力而为语义)。
- 文件夹窗口的全局搜索/上传:TreePanel 的搜索盒仍以会话 cwd 为根(
fs.search是有意的工作区限定), 目录内上传经 host 会话工作区校验(目录在 cwd 内可上传,cwd 外拒绝)。 - 无 ack:
delivered: true只表示「推送已发出」,客户端处理失败(罕见)不重试。 - 关闭特性不清已打开 tab:与 tab 启用开关语义一致,只注销工具并清空未投递队列。
测试覆盖
tests/agent-opens.spec.ts(新):注册表语义(排队/重放/消费/作用域隔离/drain/dispose)+ 工具行为 (分类、相对路径、缺路径、非 http(s) scheme、tab 禁用、schema 校验、无 agent 错误)。tests/smoke.spec.ts:WS 升级路由清单 +settings.get默认值 + 独立 gating 测试(默认 off → 0 工具;on → 1;off → 注销)。tests/plugin-shape.spec.ts/tests/prefs.spec.ts:schema/parse 默认值 + 全量 prefs 字面量。tests/editor-host.spec.tsx:meta.dir文件夹窗口(树以目录为根、无编辑器 chrome、不触发 viewer 加载)。tests/locales.spec.ts:zh/en/ja 词典 key 集相等(自动覆盖新键)。