# dsh-web-ui

> Add a workspace branch selector capsule and Git graph panel to dsh web GUI blank sessions: search branches, switch branches, create new branches, view lane-style commit graphs; git operations execute on the host.

## Metadata

- Author: [@zhu1090093659](https://github.com/zhu1090093659)
- Repo: <https://github.com/zhu1090093659/dsh-web-ui.git>
- GitHub: [zhu1090093659/dsh-web-ui](https://github.com/zhu1090093659/dsh-web-ui)
- Stars: 5,126
- Language: TypeScript
- License: [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
- Homepage: <https://gallery.dsh-market.com>
- Topics: `deepseek-harness`, `dsh`, `dsh-plugin`, `web-ui`
- Forks: 311
- Open Issues: 49
- Last push: 2026-08-20T14:37:38.000Z
- Added: 2026-08-20T00:00:00.000Z

## Install

```bash
dsh plugin --profile web add github:zhu1090093659/dsh-web-ui/packages/dsh-git-graph
```

## Wiki

## 一句话定位
为 dsh web GUI 空白会话加上一个工作区分支选择胶囊与 Git 图谱面板：搜索本地分支、查看当前 HEAD、创建并切换、查看泳道式提交历史，所有 git 操作在 host 进程的真实工作树上执行。

## 核心能力
- 在空白会话输入区右侧提供一个分支胶囊，点开是可搜索弹层：当前分支打勾、按名搜索、底部「创建并检出新分支…」与「Git 图谱」两个动作
- 切换分支作用于工作区级真实 `git switch --no-guess`：影响该工作区所有会话；守卫覆盖未解决冲突、进行中的 git 操作、目标分支已被其他 worktree 检出三类情况
- 新建分支走 `git switch --no-guess -c`，支持创建对话框：客户端先做即时校验（不允许以 `-` 开头、不含 `..` / `~^:?*[\` 等），host 再用 `git check-ref-format --branch` 做权威校验
- Git 图谱面板按泳道渲染提交历史（topo-order + parents），每行带作者、时间、相对时间（刚刚 / N 分钟前 / N 小时前 / N 天前），支持加载更多
- 状态刷新三路叠加：胶囊挂载 / 弹层打开 / 切换成功各拉一次；订阅期间 host 每 30 秒轮询 workspace（仅推送变化、15 秒超时兜底）；窗口聚焦额外拉一次（5 秒节流）
- SSE 长连接经跨标签页选主中继（Web Locks + BroadcastChannel），同一 URL 全浏览器只保留一条 EventSource，避免多开标签页挤占 HTTP/1.1 同源连接池

## 技术实现
- **语言**: TypeScript（host 半区）+ TypeScript + React 18 + CSS Modules（browser 半区）
- **关键依赖**: `react` ^18.2.0（peer，宿主运行时注入）、`@deepseek-ai/dsh-client-ui-conversation`（提供 SlotMap 与输入区槽位）、`@deepseek-ai/dsh-client-ui-slots`（slot 注册）、`@deepseek-ai/dsh-host-webserver`（host 路由与 SSE 框架）
- **架构模式**: 双半区 cordis bundle。`src/index.ts` 是 host 半区（mountOnce 单实例 guard + 注册 `/git/*` 路由 + 挂载 GitService）；`src/client/index.ts` 是 browser 半区（声明感知挂载：先等 2 秒看 shell 是否声明 `conversation.input.selector.context` 槽位，迟到则永久回退到 `conversation.input.dock` 的 hero 相位；active 会话不挂）。git 调用全部走 host 的 subprocess 服务（生产用 `/git/*` HTTP + SSE，测试用 plain child_process）
- **入口文件**: `packages/dsh-git-graph/src/index.ts`（host 入口，cordis `apply`）、`packages/dsh-git-graph/src/client/index.ts`（browser 入口，cordis client `apply`）；cordis bundle 声明在 `packages/dsh-git-graph/cordis.patch.yml:1-12`（插入 id `ui-git-graph`），浏览器依赖在 `packages/dsh-git-graph/package.json:34-46`（注入 3 个官方 `@deepseek-ai/dsh-client-*` 模块 + `platform: web`）

## 适用场景
- 在 dsh web 上做多分支并行开发的用户：开新会话前先在空白面板点开分支胶囊，搜/切到当前想用的分支，避免每个会话都手动 `git switch` 一遍；
- 想可视化看自己工作区提交历史的用户：点胶囊底部「Git 图谱」打开泳道图谱面板，看一眼哪个分支走在哪条线、有没有分叉，比 `git log --graph` 直观；
- 习惯用对话驱动开发、希望工具栏风格与官方工作区胶囊对齐的用户：本插件的胶囊与官方 WorkspaceChip / AgentPresetSeat 走同一套 28px 透明胶囊配方与 `--dsw-*` 主题 token，安装后视觉风格不跳。

## 前置依赖与兼容性
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| DSH 宿主 | `0.1.0-rc.6+` 推荐 | package.json 未在 dsh.engines 声明；devDependencies 统一锁到 `@deepseek-ai/dsh-* ^0.1.0-rc.8` |
| Node.js | `^22.19.0` 或 `>=24.0.0` | package.json `engines.node` |
| 平台 | 跨平台（host 端 PATH 需可解析 `git`；Windows 显式走 `git.exe`） | host 半区跑在 Node.js、client 半区跑在浏览器，无 OS 限制 |
| 原生模块 | 无（运行时） | 通过 host 的 subprocess 服务 spawn git 二进制，不引入 native 绑定 |
| Git 二进制 | 系统 PATH 可达 | host 端 `git --version` 需可用；Windows 推荐 Git for Windows |
| React | `^18.2.0` | peerDependency，由宿主注入 |

## 安装方式
```bash
dsh plugin --profile web add github:zhu1090093659/dsh-web-ui/packages/dsh-git-graph
```

## 配置项
本插件无需用户配置。

host 端在源码内有几处可调常量（如轮询节奏、回退等待、请求体上限），但均无暴露给 DSH 设置面板的入口，调整需改源码后重新构建：

| 内部常量 | 位置 | 默认值 | 作用 |
|---|---|---|---|
| `POLL_INTERVAL_MS` | host/routes.ts:49 | 30000 ms | SSE 订阅期间 host 端轮询 workspace 状态的时间间隔 |
| `HEARTBEAT_INTERVAL_MS` | host/routes.ts:51 | 15000 ms | SSE 心跳注释间隔（防代理丢空连接） |
| `STATUS_TIMEOUT_MS` | host/routes.ts:58 | 15000 ms | 单次 git status 的超时兜底，超时后用 AbortController 杀掉子进程 |
| `BODY_CAP_BYTES` | host/routes.ts:75 | 1 MiB | `/git/*` 请求体上限，超出直接销毁连接 |
| `CONTEXT_FALLBACK_MS` | client/index.ts:94 | 2000 ms | 客户端等待 shell 声明 `conversation.input.selector.context` 槽位的时长，超时后回退到 `conversation.input.dock` |
| `FOCUS_REFRESH_MIN_MS` | client/chips/BranchChip.tsx:32 | 5000 ms | 浏览器窗口聚焦时重新拉取分支状态的节流间隔 |

## 常见问题

**Q: 安装之后分支胶囊没出现在输入区怎么办？**

A: 三步排查。第一，确认 `dsh web` 重启过，bundle 激活是启动时一次性做的，刷新浏览器不足以触发；第二，看一眼浏览器控制台是否有 `[dsh-git-graph]` / 槽位声明相关的报错；第三，等大约 2 秒——本插件会等 shell 声明 `conversation.input.selector.context` 槽位 2 秒后才回退到 dock hero 相位，如果 shell 没声明这个洞，胶囊会自动出现在 hero 行 agent-preset 座位右侧（迟到声明会被忽略）。`packages/dsh-git-graph/src/client/index.ts:184-199`。

**Q: 切分支时被 `tracked-changes-would-be-overwritten` 挡住怎么办？**

A: 这是 git 自己的保护：已跟踪的本地修改会被目标分支覆盖。报错里会列出前两个阻塞文件路径 + 其余文件计数。本插件不做自动 stash（避免意外吞掉你的修改），请先 `git stash` 或 `git commit` 后再切。错误分类在 `packages/dsh-git-graph/src/host/git-service.ts:87-152`，文案见 `packages/dsh-git-graph/src/client/locales.ts:30-35`。

**Q: 我工作区里有多个 Git 仓库，这个胶囊管哪个？**

A: 管当前会话 cwd 所在的那个。它会读 sessions baseline 的 cwd，解析成 `git rev-parse --show-toplevel` 拿到的 repoRoot，所有 `git switch` 都作用在那个 repoRoot 上。切换工作区 = 激活目标工作区并打开它的（复用或新建的）空白会话，不会去改其他会话的 cwd。语义在 `packages/dsh-git-graph/src/client/index.ts:122-132` 与 `packages/dsh-git-graph/README.zh.md:74`。

**Q: 切分支会影响已经在跑的其他会话吗？**

A: 会，因为切的是工作区级磁盘树——所有会话共享同一份 checkout。如果你只是想让某个会话在另一个分支上工作，正确做法是在输入区的工作区胶囊里切到目标工作区（或新建空白会话），不是用本插件的分支胶囊。`packages/dsh-git-graph/src/host/git-service.ts:204-225` / `packages/dsh-git-graph/README.zh.md:74`。

**Q: 每次切换都能成功吗？什么情况会被拒？**

A: 三类守卫挡路：未解决的合并冲突（`conflicts-present`）、正在进行的 git 操作（merge / rebase / cherry-pick / revert / bisect 任一标记存在 → `operation-in-progress`）、目标分支已被其他 worktree 检出（`branch-in-other-worktree`）。这三种直接拒绝并给可读报错，不会偷偷强切。守卫在 `packages/dsh-git-graph/src/host/git-service.ts:323-340`，错误文案见 `packages/dsh-git-graph/src/client/locales.ts:27-29`。

**Q: 同一个 URL 开两个标签页，会不会吃光浏览器的 HTTP 连接数？**

A: 不会。SSE 长连接走跨标签页选主（Web Locks + BroadcastChannel），同一 URL 全浏览器只保留一条 EventSource，其它标签页通过 BroadcastChannel 收事件，HTTP/1.1 同源连接池不被挤占。浏览器既不支持 Web Locks 也不支持 BroadcastChannel 时自动降级为每个订阅开一条（旧行为）。代码在 `packages/dsh-git-graph/src/client/sse-leader.ts:45-122`（对应 issue #383）。

**Q: 在非 Git 目录里新建会话会怎样？**

A: 胶囊直接不显示——host 端 `git rev-parse --show-toplevel` 失败时 `RepoStatus` 返回 `null`，前端拿 `null` 后不渲染控件；不会报错、不会留空块。逻辑在 `packages/dsh-git-graph/src/host/git-service.ts:281-286`、`packages/dsh-git-graph/src/client/index.ts:134-138`。

**Q: 卸载插件之后 git 状态会变吗？**

A: 不会。本插件不在 host 上写任何持久文件，分支切换走的就是磁盘 `git switch`，状态全部由 git 自己管理。卸载 `dsh plugin --profile web remove @linxin666/dsh-client-ui-git-graph` 不会有插件自留的状态。卸载命令见 `packages/dsh-git-graph/README.zh.md:65-67`。

## 上手难度
入门 — 一行命令安装，重启 `dsh web` 即可见分支胶囊；非 Git 目录自动隐藏，无需额外配置。

## 已知问题与限制
- 客户端等待 `conversation.input.selector.context` 槽位声明只有 2 秒，超时后永久回退到 `conversation.input.dock` 的 hero 相位；迟到声明会被忽略，host 重启前不会重试（`packages/dsh-git-graph/src/client/index.ts:191-199`）
- active 会话不显示分支选择控件，分支切换需要新建空白会话或重启 host（`packages/dsh-git-graph/src/client/index.ts:7-9`）
- 切换分支前不做自动 stash，工作树脏时会被 git 自己拒绝（`packages/dsh-git-graph/src/host/git-service.ts:207-225`）
- Windows 强制走 `git.exe`（避开 `.cmd` shim，避免 `--format` 字符串里的 `%` 被 cmd.exe 展开），无法在配置里覆盖（`packages/dsh-git-graph/src/host/git-service.ts:41-43、60-63`）
- 路径必须在 host 已注册工作区列表里，任意目录无法触发 git 操作；realpath 不存在或不在注册表里返回 `workspace-unknown`（`packages/dsh-git-graph/src/host/git-service.ts:30-43`、`packages/dsh-git-graph/src/index.ts:30-43`）
- `/git/*` 请求体超过 1 MiB 直接销毁连接，不解析（`packages/dsh-git-graph/src/host/routes.ts:75、90-95`）
- SSE 轮询每 30 秒一次，每个订阅者跑一次 git status；多工作区同时订阅时进程数 = 订阅者数；目前没有聚合或采样（`packages/dsh-git-graph/src/host/routes.ts:49、188-206`）
- 单次 git status 超时 15 秒（`STATUS_TIMEOUT_MS`），超时用 AbortController 杀掉子进程，UI 拿到的是 timeout 报错而非部分结果（`packages/dsh-git-graph/src/host/routes.ts:58-69、171-186`）
- 切换分支作用在工作区级（`git switch` 影响所有会话），不是 per-session 副本；想并行在多个分支上工作的用户需用「切换工作区」语义而不是分支胶囊（`packages/dsh-git-graph/README.zh.md:74` / `packages/dsh-git-graph/src/host/git-service.ts:196-203`）

---

This document is auto-generated by [deepseek-plugin.org](https://deepseek-plugin.org). HTML page: [dsh-web-ui](https://deepseek-plugin.org/plugins/zhu1090093659/dsh-web-ui/packages/dsh-git-graph)
Wiki generated by AI (model: `MiniMax-M3`)
