# dsh-side-dir 设计文档 日期:2026-08-23 状态:已批准(方案 B:命令通道桥接) ## 概述 为 DeepSeek Harness Web 界面提供**项目目录预览**插件:右侧 details 面板内浏览当前会话工作目录的文件树,点击文件查看内容(只读)。侧栏底部提供开关按钮。 ## 架构 一个插件、两个半边(官方标准双半边结构): ``` ┌─ Host 半边 (lib/index.js) ──────────────┐ │ ctx.commands.register('/dirpreview') │ │ handler: ctx.fs.resolve + contains │ │ + readText(围栏在会话 cwd 内,只读) │ └──────────────┬──────────────────────────┘ │ command.execute RPC(响应直接携带结果文本) ┌──────────────▼──────────────────────────┐ │ Client 半边 (lib/client.js) │ │ · 侧栏 footer 开关按钮(toggle) │ │ · ON: 遮蔽 details 槽 → 目录树面板 │ │ - 树: workspaces.listDirectory RPC │ │ - 文件: command.execute 读内容 │ │ · OFF: 释放 details 槽(原生面板恢复) │ └─────────────────────────────────────────┘ ``` ## 关键决策与依据 | 决策 | 依据 | |---|---| | 目录数据用 `host.listDirectory` RPC | 目录选择器插件的 browse 能力已在 web profile 挂载,现成可用(单层列表+面包屑+hidden 标记,树由客户端逐层懒加载拼接) | | 文件内容用**自定义命令**桥接 | 核心 wire 契约封闭(RpcMethodMap 固定),插件不能新增 RPC;`command.execute` 的 RPC 响应直接携带 handler 结果文本,是官方允许的 host→client 数据通道 | | details 槽用条件遮蔽 | 该槽已被 ui-conversation 的 DetailsPanel 占用(single 槽)。开关 ON 时以 priority -10 注册占位,OFF 时释放——原生面板完整恢复(与品牌印章同一验证过的模式) | | 路径围栏 | handler 内 `ctx.fs.resolve(path,{cwd:会话cwd})` + `ctx.fs.contains(sessionRoot, resolved)`,越界拒绝;只读,无写入面 | | 内容上限 | `stat` 预检 size > 256KB 拒绝(提示过大);`readText` 自带二进制拒绝(FS_NOT_TEXT) | ## 组件 ### Host 半边(lib/index.js) - `name: 'side-dir'`,注册命令 `dirpreview`: - `dirpreview root` → `{ok, root}`(当前会话 cwd,作为面板树根) - `dirpreview read <相对路径>` → `{ok, path, content, truncated?, size}` 或 `{ok:false, error}` - handler 从 invocation 取 session cwd;无会话上下文时返回错误 - 无其它 host 行为 ### Client 半边(lib/client.js,闭包工厂格式) - `inject: ['theme', 'slots', 'workspaces', 'layout', 'sessions']`(实施时按实际服务键微调) - **侧栏开关**(`sidebar.footer.action` 槽):调色板图标按钮「目录」,ON 时高亮 - **DirPreviewPanel**(details 遮蔽占位): - 头部:当前目录面包屑 + 刷新按钮 + 关闭(恢复原生面板) - 树体:懒加载目录树(展开时调 listDirectory);文件条目显示名字+大小 - 内容视图:点击文件 → command.execute 读取 → `
` 渲染(等宽、自动换行关、256KB 上限内);返回按钮回到树
  - 空态/加载态/错误态(FS_NOT_TEXT → 「二进制文件,无法预览」)
- 开关逻辑:ON = 注册 details 占位(priority -10)+ `layout.openDetails()`;OFF = 释放占位(原生 DetailsPanel 自动恢复)

## 数据流

1. 用户点侧栏「目录」→ 注册 details 占位 + 打开右栏
2. 面板打开 → `command.execute(sessionId, '/dirpreview root')` 取树根 cwd → `listDirectory(root)` 渲染第一层
3. 展开目录 → `listDirectory(子路径)`
4. 点击文件 → `command.execute(sessionId, '/dirpreview read <路径>')` → 响应文本渲染
5. 关闭/切 OFF → 释放占位

## 错误处理

| 情形 | 表现 |
|---|---|
| 路径越界(workspace 外) | 命令返回 error:「路径超出项目目录」 |
| 二进制文件 | 「二进制文件,无法预览」(ctx.fs 原生 FS_NOT_TEXT) |
| 文件 >256KB | 「文件过大(>256KB),无法预览」 |
| 会话无 cwd / 无 agent | 面板显示「当前会话无关联目录」 |
| listDirectory 失败 | 节点显示错误态可重试 |

## 安全

- 只读:handler 无任何写入调用
- 路径围栏:contains 校验,符号链接逃逸由 ctx.fs.resolve 的规范化语义处理
- 命令结果不进模型历史(官方契约:command 结果由适配器直接渲染)

## 测试

- 手动:树浏览/展开/文件预览/二进制提示/越界拒绝/开关切换后原生面板恢复
- 边界:无 cwd 会话、256KB 边界、深层嵌套懒加载
- 回归:OFF 状态下原生 details 面板行为与安装前一致

## 范围外(v1 不做)

- 文件编辑/写入、搜索、自动刷新(fs 事件订阅)、多 workspace 切换、代码高亮