Branch chip in the composer tool row that switches, creates, and refreshes workspace branches, commits a checked subset of changed files with a message the session's own model writes from the diff, and pushes the current branch.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:hunan36/dsh-git-flow
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
English | 中文
A DeepSeek Harness workspace git plugin: a branch chip in the composer tool row. Open it to browse branches, switch branches, create one, merge one into the current branch, and commit a checked subset of files — the commit message is written by the session's own model from the diff.
composer tool row: [⑂ feature/login 3] ← click (a Feature badge sits beside it)
├ Search branches
├ Local branches · 2 ✓ feature/login
│ main
│ ↳ right-click: Checkout · Merge · Rebase · Fetch
│ Push · Rename · Delete · Copy name
├ New branch…
├ Commit… (enabled while the tree is dirty)
├ Refresh status
├ Start git flow branch…
└ Finish git flow branch… (on a flow branch, with a clean tree)
Install
Two ways in; either one is enough.
From the plugin page (GUI)
Sidebar → Plugins → Add plugin, then put this in "Package or address":
@hunan36/dsh-git-flow
Press Install and enable it afterwards ("Enable"). That path runs pnpm add in the profile serving the page (usually web) and writes this package into dsh.profile.bundles for you — you do neither by hand.
The GitHub address https://github.com/hunan36/dsh-git-flow also works, but pnpm pins the dependency to that commit and will not follow new releases. Installing by package name is what makes version comparison and updates possible.
Restart dsh web afterwards: host-half changes are not hot-reloaded, and the browser fetches the new client bundle on restart.
Updating
DSH does not auto-update third-party plugins; updating takes one command:
pnpm --dir ~/.dsh/profiles/web add @hunan36/dsh-git-flow@latest
Then restart dsh web. Host-half changes (services and API routes) only apply after a restart; the browser half refetches its bundle then.
Check the installed and latest versions:
npm view @hunan36/dsh-git-flow version
Installs by GitHub address are not followed automatically (pnpm pinned a commit), so reinstall explicitly to update:
pnpm --dir ~/.dsh/profiles/web add github:hunan36/dsh-git-flow#main
From the CLI (a dedicated profile is recommended)
# 1. Create a dedicated profile (leaves your existing web profile alone)
dsh --profile gitflow-web --from-default-profile web
# 2. Add the plugin — the npm package, a git URL, or a local checkout path
dsh plugin --profile gitflow-web add @hunan36/dsh-git-flow
# dsh plugin --profile gitflow-web add https://github.com/hunan36/dsh-git-flow
# dsh plugin --profile gitflow-web add "$PWD"
# 3. Enable it on the plugin page ("Enable"), or edit the bundle list directly:
# dsh.profile.bundles += "@hunan36/dsh-git-flow"
# The package declares dsh.bundle.patch, so enabling it mounts the GitFlow
# service from the bundle's own cordis.patch.yml.
# 4. Boot
dsh --profile gitflow-web
The headless profile installs too, but it has no webServer: no /api/dsh-git-flow/* route is registered, the browser half does not exist there, and boot reports no error.
A machine without pnpm
dsh's plugin management — the plugin page's Install, and dsh plugin … — shells out to pnpm, and fails outright when there is none:
dsh: pnpm was not found; install pnpm and make it available on PATH.
Install one (Node ships npm):
npm install -g pnpm # the latest 10.x
# or: corepack enable && corepack prepare pnpm@10 --activate
Do not let Corepack use its own default: after a bare corepack enable you get pnpm 8.15.7, which walks straight into the ERR_PNPM_ADDING_TO_ROOT failure below.
If you would rather not install pnpm at all, the plugin itself does not need it (its dependencies are empty):
- Use Node's npm:
cd ~/.dsh/profiles/<profile> && npm install https://github.com/hunan36/dsh-git-flow - Copy the directory: drop the whole
dsh-git-flowfolder (includinglib/) into<profile>/node_modules/, for offline or fully manual setups
Both need one extra step: add @hunan36/dsh-git-flow to that profile's dsh.profile.bundles in its package.json, then restart dsh web. The plugin page and dsh plugin add do that for you; a manual install does not, and the package stays unloaded even though it sits in node_modules.
Point the plugin page at npm (a machine that only has npm)
The plugin page runs pnpm add <spec> by default, but the plugin manager's command is configurable — set it to npm in the profile's cordis.patch.yml and the page installs with npm:
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: plugin-manager
config:
pnpmCommand: npm
Then use the plugin page exactly as usual (paste the address → Install → Enable). The CLI's dsh plugin … add does not honour this setting and still needs pnpm.
Three caveats:
- This is an internal dsh setting and may move between versions; prefer installing pnpm when the machine allows it.
- The arguments dsh passes are
add <spec>,remove <name>, andview <spec> … --json, all of which npm accepts; but pnpm-specific semantics (-w,onlyBuiltDependencies, lockfiles) do not exist under npm, so do not mix both managers in one profile. - The command line shown in the UI and logs still reads
pnpm add …(dsh builds that label literally). To tell which manager really ran, look forpackage-lock.jsonversuspnpm-lock.yamlin the profile directory.
Installation fails with ERR_PNPM_ADDING_TO_ROOT
A dsh profile directory is itself a pnpm workspace (pnpm-workspace.yaml with packages: [.]), and pnpm 8 treats pnpm add there as adding to a workspace root — which it refuses. When the profile has no packageManager field, Node's Corepack fills one in (commonly pnpm@8.15.7), and the install then dies on that check before it ever resolves this plugin, so the failure is not about this package.
Any one of these gets you through:
Installing from the plugin page (GUI): that path runs
pnpm add <spec>and cannot pass-w, so do step 2 first (write the.npmrc) and then press "Retry" in the dialog — the same command then succeeds under pnpm 8.Teach the profile to accept a root dependency, then install exactly as documented:
echo 'ignore-workspace-root-check=true' >> ~/.dsh/profiles/gitflow-web/.npmrcFrom the CLI, pass
-wthrough (dsh forwards extra arguments to pnpm):dsh plugin --profile gitflow-web add -w https://github.com/hunan36/dsh-git-flow.gitPut the profile on pnpm 10 (the version this plugin is tested with): run
corepack use pnpm@10in the profile, or set thepackageManagerfield Corepack added topnpm@10.22.0.
Note that after the first failure Corepack has already written packageManager: pnpm@8.15.7+sha512… into that profile's package.json, so every later plugin operation there uses pnpm 8 — step 4 is the durable fix.
Requires Node ^22.19.0 || >=24.0.0 and pnpm 10; this repository pins it with packageManager: pnpm@10.22.0.
Configuration
Set in cordis.patch.yml:
| Key | Default | Meaning |
|---|---|---|
timeoutMs |
15000 | Deadline for read-only and local git commands |
pushTimeoutMs |
120000 | Deadline for git push, which waits on a remote |
messageMaxDiffBytes |
65536 | Cap on the diff bytes handed to the model (truncated past it) |
messageLanguage |
en |
Default commit-message language, zh or en |
productionBranch |
main |
Production branch name; set master for older repos |
developBranch |
develop |
Integration branch name |
featurePrefix |
feature/ |
Feature branch prefix |
releasePrefix |
release/ |
Release branch prefix |
hotfixPrefix |
hotfix/ |
Hotfix branch prefix |
tagPrefix |
v |
Tag prefix applied when a release or hotfix finishes |
The commit panel carries an English | 中文 switch beside "Draft message". It overrides messageLanguage for that call and remembers the choice in localStorage, so English stays the default until you pick otherwise.
Merging a branch
Right-click a branch row (or press the keyboard menu key / Shift+F10 while it is focused) to get the branch actions below. This section
covers checkout and merge; rebase, fetch, push, rename, delete, and copy live in Branch actions.
- Checkout — the same switch as a left click, including the dirty-worktree confirmation.
- Merge into the current branch — runs
git merge --no-edit <branch>on the current branch; nothing is pushed.
A merge that can fast-forward does; a diverged branch produces a normal merge commit with git's default message. The toast reports which happened. A remote-only row merges its origin/… ref without creating a local branch.
A conflict is not aborted: the conflict markers, the index and MERGE_HEAD stay in the worktree, and the chip starts showing the conflicted files. Resolve them and commit the merge yourself — from a terminal or by asking the session. The commit panel only makes partial commits (git commit --only), which git refuses while a merge is in progress. If local edits would be overwritten, the merge fails with git/dirty-worktree and the worktree is left untouched. Merging the current branch into itself and merging into a detached HEAD are both refused.
Branch actions
The same right-click menu covers the rest of SourceTree's local-branch actions. They act on the selected branch (except rebase, which always replays the current branch onto the selected one):
| Action | What it runs |
|---|---|
Rebase current changes onto <branch> |
git rebase <branch> — the current branch's commits are replayed on top of it, so its history is rewritten |
Fetch <branch> |
git fetch <remote> for the branch's remote; a remote-only row fetches its own remote |
Push to <upstream> (tracked) |
git push <remote> <branch>:<tracked branch> for that branch |
| Push to… | pick a remote and a destination branch: git push --set-upstream <remote> <branch>:<destination> |
| Rename… | git branch -m for a local branch (the upstream config moves with it) |
Delete <branch> |
git branch -d; a not-fully-merged branch asks for confirmation and retries with -D |
| Copy branch name | copies the name to the clipboard |
Remote-only rows offer checkout, merge, rebase and fetch (they merge/fetch their origin/… ref), plus copy; rename, delete and push are
disabled because there is no local branch. The checked-out branch cannot be deleted, checked out again, merged into itself, or rebased onto
itself.
A rebase conflict stops where git stopped: the rebase stays in progress, the chip shows the conflicted files, and the user finishes it with
git rebase --continue (or git rebase --abort) from a terminal — the plugin never aborts or force-continues. Fetch and push are the only
network actions in this menu, and both run only when clicked.
Git flow
The bottom two rows of the branch menu start and finish standard git-flow branches: features and releases branch off develop,
hotfixes branch off production, and a finish merges back with --no-ff.
| Type | Branches off | Merges into | Tag + version bump |
|---|---|---|---|
feature/ |
develop |
develop |
no |
release/ |
develop |
production + develop |
yes |
hotfix/ |
production | production + develop |
yes |
A finish runs: merge, tag, commit the package.json version, push the branches and tag, delete the local branch.
- When
developdoes not exist yet, "Start" offers to create it from production (or the current branch). - Merges, tags, and version commits are not rolled back; a failed push reports "finished locally, but the push failed" and keeps the local result.
- Everything that can be refused is checked before the first write: a dirty worktree, an existing tag, a missing target branch.
- A merge conflict is left in place for you to resolve; nothing is aborted on your behalf.
- A repository without a root
package.jsonskips the version step instead of failing.
Security boundaries
- The browser sends only a
sessionId. The host resolves the repository from that session's workspace, so a page can never name a directory for git to run in. - Arguments are always an array, never a shell string, with
stdin: 'ignore'andGIT_TERMINAL_PROMPT=0, so a credential prompt can never hang a call. pushnever passes--forceor--force-with-lease, and only pushes the current branch's upstream (with--set-upstream <remote> HEAD:refs/heads/<branch>when it has none).- Commits stage only the files checked in the panel. An empty selection answers
git/no-files-selectedand never falls back to an implicitgit add -A. - A merge runs only
git merge --no-edit <name>against the current branch, with a branch name the browser sent. It never passes--no-ff/--force/-X, never pushes, and never aborts a conflicted merge. - Branch actions run exactly one fixed git subcommand each:
git rebase <branch>,git fetch <remote>,git push <remote> <branch>:refs/heads/<dest>,git branch -m <from> <to>,git branch -d|-D <branch>. Remote names are only accepted when they appear ingit remote; fetch never prunes or resets; the checked-out branch can never be deleted; a-Dforce delete is only sent after the unmerged-branch warning and a second confirmation. - Switching branches on a dirty worktree fails by default (
git/dirty-worktree). Only the user's second confirmation in the dialog sends--force. - Discarding runs only after the destructive action is picked in a confirmation dialog: tracked paths go back to HEAD with
git restore --source=HEAD --staged --worktree, untracked paths are removed withgit clean -fd(no-x, so ignored files stay), and a conflicted path is refused rather than guessed at. workspaceRegistryis read lazily throughctx.get('workspaceRegistry')instead of being declared instatic inject, so a profile without one (the shippedheadlessprofile) still activates this entry rather than leaving a permanently pending plugin; every git call there answersgit/not-a-repository.- A missing git, a non-repository workspace, a timeout, and every refusal travel as structured codes (
git/not-installed,git/not-a-repository,git/timeout, …) that the UI renders as copy. Outside a repository — or without git — the chip renders nothing at all.
Layout
src/
├── index.ts host entry (default-exports the GitFlow service)
├── service.ts GitFlow: status / branches / checkout / createBranch / merge / rebase / fetch / push / pushBranch /
│ renameBranch / deleteBranch / remotes / commit / discard / pull / generateMessage
│ + gitflowState / startFlow / initDevelop / finishFlow
├── git.ts GitRunner: subprocess + scrubbed env + timeout + porcelain v2 parsing
├── routes.ts /api/dsh-git-flow/* (registered only where a webServer exists)
├── message.ts commit message: ctx.llm.stream on the session's own provider/model, template fallback
├── contract.ts host↔browser types only
└── client/
├── index.ts slots.inject('conversation.input.left') + locale registration
├── BranchChip.tsx container: status, menu, dialogs, toast
├── BranchMenu.tsx the chip and its anchored menu
├── BranchActionsDialog.tsx rebase confirmation / rename / delete (force) / push-to-remote dialogs
├── CommitDialog.tsx commit panel / new branch / forced-switch and discard confirmations
├── GitFlowDialog.tsx start a flow branch / guided develop creation / finish a flow branch
├── api.ts fetch wrapper + error codes to copy
├── styles.ts the few CSS rules the popover width and file-row ellipsis need
└── locales.ts zh/en dictionaries (en is typed against zh, so a missing key is a compile error)
Build
pnpm install
pnpm build # tsc -> lib/types/**, tsdown -> lib/index.js + lib/client.js
lib/ is committed to the repository (it is not in .gitignore) and the package no longer carries a prepare script, so installing from the git URL needs no build, no dev dependencies, and no entry in pnpm's onlyBuiltDependencies allowlist:
- pnpm 10.34+ blocks a git dependency's
prepare(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED), and the allowlist entry it demands isdsh-git-flow@<spec>#<commit sha>— a string that goes stale on every upstream commit; - shipping the build instead makes
dsh plugin add <spec>takelib/as-is: measured at 263 ms under pnpm 10.34.5.
After editing src/, run pnpm build and commit lib/ along with it, or everyone installing from git gets the previous build.
Artifact contract:
lib/index.js: Node ESM, imported directly by the cordis loader.lib/client.js: a CJS factory wrapped aswindow.__ModuleLoader__.load({ id: "@hunan36/dsh-git-flow", factory: (require) => … })(the id ispackage.json'sname);react,react/jsx-runtime, and@deepseek-ai/dsh-client-ui-primitivesstay external and come from the page's static module table.
Compatibility
Developed and verified against @deepseek-ai/dsh@0.1.6-alpha.2, with peer and dev dependencies pinned to it. dsh is still alpha, so contracts such as conversation.input.left, ctx.webServer.register, and ctx.slots.inject may drift between minor versions. After upgrading dsh, rebuild and confirm the first line of lib/client.js is still window.__ModuleLoader__.load({ id: "@hunan36/dsh-git-flow".
Known limits
- An AI commit message needs one turn already sent in the session (the provider/model recorded in
request/header). Without a route the panel answersgit/no-routeand tells you to send one message first; when the model fails or returns nothing usable, thechore: update N filestemplate fills in and says so. - Messages default to English. The panel's language switch (remembered per browser) and the
messageLanguageconfig are the only two inputs; the template fallback follows the same choice. - Remote branches appear only once
refs/remotes/**exists locally (the plugin never fetches in the background); "Refresh status" makes no network call. - Conflicted files cannot be checked; resolve the conflict in the conversation first.
git switch --forcediscards all local changes, not just the conflicting ones — the confirmation copy says exactly that. To drop a single file's changes instead, use that file's Discard action in the commit panel.- Switching to a ref that exists nowhere fails inside git and is reported as
git/failedwith git's own text. Only stale UI state can reach that path, since the menu lists branches that exist.
Verification (0.1.6-alpha.2)
Measured on an isolated DSH_HOME with profile gitflow-web (@hunan36/dsh-git-flow appended to dsh.profile.bundles): the chip text equals git branch --show-current; the menu lists every local branch plus each remote-only one; search filters rows; switching moves both the chip and the worktree files; a dirty switch is refused and only confirms --force after a second dialog; an invalid branch name errors while a valid one is created and switched to; with a single file checked, the model returned a feat: … message and git log -1 --name-only showed only that file; commit-and-push made the branch appear in git ls-remote with its upstream set; a session whose workspace is not a repository renders no chip; the headless profile boots with no warning and no routes.
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-git-graph★ 9959
Git branch selector and Git graph for the dsh web GUI: switch branches and explore branch-lane and commit history from the conversation header.
Akimiya-z/codex-guard#dsh★ 132
Pre-submit pull-request hygiene checks inside DeepSeek Harness: scans the current diff for TODO leftovers, hardcoded secrets, and non-conventional commit subjects.
Cerbur/clutch-dsh#clutch-dsh-worktree★ 38
Adds a Worktree view to DSH Web UI that groups Sessions by Git worktree while keeping DSH as the source of truth.
xbzbing/dsh-git-panel★ 33
IDE-style Git panel in the DSH Web GUI: a resident tab with a three-column overview (branch/tag list, searchable commit-history graph, commit detail with changed-file tree and message) and a changes page (stats bar, uncommitted list with per-file stage/unstage/discard, commit box with amend); side-by-side diffs carry lazy highlight.js syntax highlighting, on-demand between-hunk context expansion and word-level intra-line emphasis, with image files shown as before/after panes; plus a zsh-style branch marker in the input bar. Every git command runs as an argv array through the host subprocess service, cwd locked to the repo root.
DamonKoy/dsh-web-ui#dsh-git-graph★ 30
Git branch selector and Git graph in the conversation header of the dsh web GUI.
PerryLink/dsh-github★ 28
Official-grade GitHub CI integration: a composite action.yml, a polling PR review bot with idempotent inline comments and a status-check gate, plus PR/issues tools with every write gated by human approval.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.