eh-tools/dsh-plugin--plugins-file-git-explorer0

dsh-file-git-explorer

左右树浏览: 左侧文件树(可见/隐藏/忽略三区, 根 = cwd) + 右侧 git 树(当前分支/变更列表/悬浮 diff), 静态双半插件

包名
dsh-file-git-explorer
版本
0.1.0
许可证
MIT
最近更新
2026年8月23日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:eh-tools/dsh-plugin#3bbf1589066c718345d9c4be9f7c4c225bb4c748&path:plugins/file-git-explorer

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.jsontest / checkjustfile test

术语

「cwd」「可见组 / 隐藏组 / 忽略组」「悬浮面板」「细条」「图钉」「联动」「diff 范围」「分支」「查看分支」「文件搜索」「提交历史」「刷新」「cwd 缓存」「树面板」的定义见仓库根 CONTEXT.md