eh-tools/dsh-plugin--plugins-file-git-explorer ↗★ 0
dsh-file-git-explorer
左右树浏览: 左侧文件树(可见/隐藏/忽略三区, 根 = cwd) + 右侧 git 树(当前分支/变更列表/悬浮 diff), 静态双半插件
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:eh-tools/dsh-plugin#3bbf1589066c718345d9c4be9f7c4c225bb4c748&path:plugins/file-git-explorer说明文档
阅读完整 README ↗dsh-file-git-explorer
左右树浏览插件 —— 左侧文件树(可见 / 隐藏 / 忽略三区, 根 = 当前会话工作区, 支持按名搜索) + 右侧 git 树(当前分支只读下拉、工作区变更列表、悬浮 diff、查看分支的提交历史)。两个面板夹在会话 header 与 composer card 之间, 可左右拉伸、可收起为细条、图钉锁定, 不覆盖主对话区。
静态双半插件(host + client bundle), 随 web profile 启动自动加载。
安装
dsh plugin --profile web add link:/plugins/file-git-explorer
安装后 host 半随 DSH 启动自动挂载; 浏览器 bundle 由 profile 注入, 刷新 GUI 页面生效。卸载用 dsh plugin --profile web remove file-git-explorer(或对应 CLI 命令)。
布局与交互
┌────────────────────────────────────────────────────────────┐
│ header / tabs ───────────── 边框线(面板顶边不得越过) │
│ ┌─文件树──┐ 主对话区(748px 居中) ┌──git 树──┐ │
│ │⎇ 可显示 │ (悬浮面板可越过) │⎇ 当前分支▾│ │
│ │ 隐藏 │ │ M a.txt │ │
│ │ 忽略 │ │ A b.js │ │
│ └────────┘ └──────────┘ │
│ composer card ───────────────── 下边框(面板底边不得越过) │
└────────────────────────────────────────────────────────────┘
- 几何: 两面板
position:fixed,top/bottom=[data-conversation-scroll]滚动容器(对话列)的顶部 / 底边 —— 统一以对话列为基准, 面板以对称边距垂直居中(顶部 4px、底部 4px, 不因 composer 卡片自带 8px 底部留白而偏上)。左树左缘跟随应用侧边栏右缘(折叠成 rail 时自动跟随), 右树右缘在 details 列打开(360px)时自动让位。滚动 / resize / 侧边栏折叠 / 会话切换都通过事件重测锚点(无定时轮询)。 - 拉伸: 面板内侧边缘有 6px 拖柄, 水平拖动改宽度; 默认宽度 400px, 最小宽度 250px; 最大宽度 = 可留白的 2/3(较之前减少 1/3), 面板不会拖到对话区边缘; 钳制上限 = 对话区与对应侧之间的留白, 永不覆盖主对话区。拖柄悬停只显示一条细线, 淡化存在。
- 收起 / 细条: 面板头部「» / «」按钮(SVG 双箭头)收起为同侧 26px 细条(只剩一个圆角箭头, 无边框底色)。悬停细条(或箭头)即整侧展开; 鼠标移出面板后延迟约 360ms 才收起(张顿一下, 不因快速掠过而闪断), 期间移回即取消; 头部的 »/« 按钮也可点击收起。
- 头部路径: 左树头部显示当前根路径, 中间省略(保头保尾,
…), 点击路径复制完整路径到剪贴板(复制后短暂显示「✓ 已复制」)。 - 图钉(无色线条版 📌): 默认不固定(两侧收起为细条, 悬停即展开); 点任意一侧的图钉, 两个面板都展开为卡片并固定(不会出现「已固定但另一侧仍是细条」的状态); 钉住后两个面板都不能收起(按钮置灰, 鼠标移出也不会收起); 再点图钉解除固定。
- 悬浮栏联动: 点文件/diff 弹出的悬浮栏与源侧栏联动 —— 鼠标移到悬浮栏时侧栏保持展开(悬浮栏豁免收起); 移出整块区域(侧栏+悬浮栏)延迟后侧栏收起并一并关闭该悬浮栏, 不留下「侧栏已收、悬浮栏还在」的孤儿状态。
- 刷新 ⟳: 重读
info(根 / 仓库根 / 当前分支) + 重跑 git status, 并作废三棵树已加载的缓存。 - 外观: 面板背景 = 对话消息列(
.Md3f7G_column的--dsw-alias-bg-base), 与聊天区域同底色; 头部图标(图钉 / 刷新 / 收起 / 关闭 / 分支)全部用单色线稿 SVG 对齐; 面板内滚动条细且半透明(悬停才加深), 拖拽柄只在悬停时显示一条细线。
左侧文件树(三区, 独立滚动)
| 分区 | 内容 | 展开语义 |
|---|---|---|
| 可显示文件 | 非点开头 且 未被 .gitignore 忽略 | 每级只展示本区成员 |
| 隐藏文件 | . 开头(排除 .git 内部结构) | 普通目录只展示其 dot 子项; 点开的 dot 目录展示其全部子项 |
| 忽略文件 | .gitignore 忽略项(含被忽略的点文件) | 普通目录只展示其忽略子项; 点开的 被忽略目录(如 node_modules)展示其全部子项 |
- 逐级懒加载: 点目录才拉取子节点(
POST /fge/api/tree), 无定时扫描。 - 点文件 → 行高亮 + 内容悬浮面板向右浮出(可越过对话区, 文本 + 行号 + 逐行语法高亮: 关键字 / 类型·类名 / 函数调用 / 字符串 / 注释 / 数字, 覆盖 JS/TS/Python/Rust/Go/Java/C 等常见语言; >1 MiB 或二进制只显示提示、不预览)。
- 点文件同时触发联动: 右侧 git 树若存在该文件 diff, 滚动定位并闪现高亮; 不自动打开 diff; 无 diff 则无操作。
- 目录单击 = 展开 / 折叠切换。
文件搜索(name search)
- 头部放大镜展开搜索框, 输入防抖 ~150ms 即时按名检索——大小写不敏感子串匹配 相对路径, 不读取、不检索文件内容; 三区树被平铺结果替换, Esc / 清空即恢复。
- 每条结果带分区徽标(显 / 隐 / 忽); 排序 = 名字命中 > 仅路径命中 → 短路径优先; 扫描上限 20 000 条、返回上限 300 条, 超出时列表尾提示「已截断」。
- 点击文件命中 → 打开内容悬浮面板并联动右树(与点树内文件完全一致);
点击目录命中 → 关闭搜索并在对应分区树内逐级 reveal 展开到目标并高亮
(混合链如
src/.env在该区不可达时, 退化为高亮可达的最深祖先)。 - 非 git 工作区回退 fs 递归扫描(不跟符号链接, 无忽略区); 会话切根自动清空搜索态。
右侧 git 树
- 顶部: 当前分支(前有竖着 git 分支 SVG 图标; 实时读
git branch --show-current), 点击从面板左侧弹出所有分支下拉(本地 / 远程分组, 只读, 不支持切换; 点非当前分支仅标记「上次查看」)。 - 下方: 工作区相对 HEAD 的变更列表(已暂存 + 未暂存 + 未跟踪), 平铺 + 状态徽标(
M/A/D/R/U), 按路径排序。 - 点变更文件 → diff 悬浮面板向左浮出(unified, 行级 +/− 着色, 增删行内容同样做代码语法高亮; 未跟踪文件显示内容; rename 用
-M双路径 diff; 二进制显示提示)。再点同一项或点 ✕ 关闭。 - 非 git 目录: 右侧树显示「(工作区干净)」占位, 分支区为空, 历史按钮置灰。
提交历史(commit history)
- 头部时钟按钮向左浮出历史面板, 与 diff 浮层互斥共享锚位(开一关一)。
- 跟随「查看分支」= 分支下拉里最后点击的分支(默认当前分支; 分支被删时回退当前分支)——下拉里的「上次查看」标记由此获得实际用途。
- 列表每页 50 条, 滚动到底自动追加(
--skip分页); 条目 = subject + 作者 · 相对时间 · 短 hash。 - 点条目进入详情: 完整提交说明 + 按文件 ±行数列表(numstat), 点文件懒加载该提交内此文件的 diff; merge 提交只显示说明、不展示 diff(combined diff 无阅读价值)。
- agent turn 结束的自动刷新同样覆盖历史: 面板可见且 HEAD 变了才整页重拉已加载页数, 尽量保留滚动位置; Esc / 收起右栏 / 切换工作区都会关闭历史浮层。
cwd 缓存
- 树根 = 当前会话工作区(跟随工作区切换, 经
useSessions的会话 cwd 感知), 会话无 cwd 时回退 DSH 进程process.cwd(); 无路径切换框(设计决策, 见 CONTEXT.md「cwd」)。 - 按仓库根(
repoRoot, 无仓库时按 cwd)在localStorage(fge-cache-v1)记忆: 面板宽度、展开 / 收起状态、下拉里「上次查看」的分支; 切回同一仓库自动恢复。当前分支始终实时读取, 不缓存。
HTTP API(host 半, 仅本机)
信任栅栏与 dsh-ds-balance 同款: 仅回环地址 + x-dsh-plugin: 1 头 + POST。所有路径做防穿越校验(文件树/file 只能落在请求 root 之下, root/repoRoot 必须是绝对路径)。
| 路由 | 请求体 | 返回 |
|---|---|---|
POST /fge/api/info | {root?} | {cwd(=root), repoRoot, branch, head} |
POST /fge/api/tree | {root?, path, mode, reveal} | 目录三区条目 [{name, rel, type, dot, ignored}] |
POST /fge/api/status | {root?, repoRoot} | {current, head, branches[], changes[]} |
POST /fge/api/diff | {root?, repoRoot, path, status, from} | {kind: 'diff'|'untracked', text, ...} |
POST /fge/api/file | {root?, path} | {text, binary, truncated, size} |
POST /fge/api/search | {root?, query} | {matches[{rel, type, zone, nameHit}], truncated} |
POST /fge/api/log | {root?, repoRoot, ref?, skip?, limit?} | {ref, head, commits[{hash, short, author, at, subject}]} |
POST /fge/api/show | {root?, repoRoot, hash, path?} | {kind: 'commit'|'merge'|'diff', message, files[], text} |
git 一律经 subprocess 服务执行(argv 数组, 无 shell)。
实现事实(已用真实仓库实测钉死)
git status --porcelain=v1 -z: 条目 NUL 分隔; rename 是两条 ——R \0\0(状态 token 带新路径, 裸 token 是旧路径)。git check-ignore必须--stdin -z(argv 模式不允许-z), 只输出命中的路径(exit 0 = 有命中, 1 = 无)。git diff HEAD --对 rename 只会显示 new file, 必须-M --才能出 rename diff; 未跟踪文件 diff 为空, 回退读内容。- 非 ASCII 路径在 diff 里默认 octal 转义, 统一加
-c core.quotepath=false。 git ls-files -c -o --exclude-standard -z与-o -i --exclude-standard -z分别给出「可见+隐藏」「忽略」的全量文件清单; 搜索的目录命中项由文件路径派生(dirsFromPaths), 与懒加载树语义解耦。git log --format=%H%x00%h%x00%an%x00%at%x00%s: 字段 NUL 分隔、条目换行分隔, 作者名/主题含空格安全; 分页用--skip+-n。- merge 提交识别:
git rev-list --parents -n 1数父提交(>1 即 merge); 单文件 diff 用git show --format= --输出纯 diff, 首个提交需diff-tree --root才有 numstat。 - numstat 取文件清单必须
-z: 默认输出把 rename 打成old =>{new}箭头串(pathspec 无法命中);-z下为 hash\0 +A\tD\t\0, rename 是A\tD\t\0\0\0(计数 token 路径位为空, 后跟旧、新两个裸 token), 解析见parseNumStatZ。 - ref/hash 一律 argv 直传且先过白校验(safeRef 拒
-开头 /../ 空白 /@{; safeHash 只收十六进制串), 无 shell 可注入面。 - 面板锚点全部用稳定 data 属性:
[data-conversation-scroll]、[data-composer-card="true"]; 对话列宽读--dsh-chat-content-width; 不依赖任何哈希类名(uV2eYG_*/wSkVaW_*等跨构建不稳定)。
测试与静态检查
node tests/git.test.mjs # 纯函数层单测(status 解析 / 三区划分 / 防穿越 / diff 参数)
node tests/verify.mjs # host 集成冒烟(真实 git, 需在仓库内运行)
eslint . # 仓库统一 lint(client bundle 按惯例忽略)
两者均已接入根
package.json的test/check与justfile test。
术语
「cwd」「可见组 / 隐藏组 / 忽略组」「悬浮面板」「细条」「图钉」「联动」「diff 范围」「分支」「查看分支」「文件搜索」「提交历史」「刷新」「cwd 缓存」「树面板」的定义见仓库根 CONTEXT.md。