dsh-git
Git in the DeepSeek Harness composer and sidebar: a repo/branch chip, a header button, and a split Git panel with staging, commit, history and diffs.
What it adds
1. Branch chip in the composer. A small pill sits in the composer trailing cluster, left of the model picker and the session-stats control:
main-repo / feature/login ↑2
It shows the repository name, the current branch, ahead/behind markers, and a dirty dot. Click it to open the Git panel.
2. Git button in the conversation header. The same action from the top-right utilities row of an open conversation.
3. A split Git panel in the sidebar.
+----------------------+----------------------+
| changes + staging | history |
| staged / unstaged | branch + filter |
| [ AI ] [ ] [commit] | expandable commits |
+----------------------+----------------------+
| diff |
+--------------------------------------------+
- Top-left: staged and unstaged files, laid out as the repository's own folder tree (collapsible folders with file counts, not a flat list). Stage/unstage a single file or everything, discard a tracked change behind an inline confirm, write a commit message, and generate one with AI (uses the Harness default model, and reports a clear error when no model is configured). Ctrl/Cmd+Enter commits.
- Top-right: commit history with a branch switcher, a text filter, continuous branch lanes drawn down the left edge (the rails run edge-to-edge through every row, including expanded file lists, with the commit dot on the first text line), expandable rows that list the files of the commit, and a right-click menu (copy hash,
reset --soft/mixed/hard,revert). Each row leads with GitLens-style ref chips — a tick for the checked-out HEAD, a branch glyph for local branches, a globe for remote-tracking refs, a tag glyph for tags — sitting at the right of the subject on the same line, withshort hash · author · ageunderneath. - Bottom: the diff of whatever you clicked — a working-tree file, a staged file, or one specific file inside a commit (only that file's patch, never the whole commit).
Narrow screens. Below 720px the two-column grid collapses into one stacked column (changes, history, diff), panes keep their own scroll with sensible max heights, tap targets grow, the short hash hides, the filter box moves to its own row, and per-file actions stay visible on touch devices where there is no hover.
Install
# from npm (scoped: the unscoped name dsh-git belongs to another author)
dsh plugin --profile web add @weidows/dsh-git
# from source
dsh plugin --profile web add github:Weidows/dsh-git
Restart the Harness after installing: client plugins are loaded at boot.
dsh-better-sidebar
dsh-better-sidebar is supported and preferred when installed. dsh-git registers its panel through ctx.betterSidebar.registerTab (order 25, single instance) and skips its own native right-sidebar tab, so you never end up with two Git tabs. Without better-sidebar it falls back to a plain right-sidebar tab.
better-sidebar mirrors native tab kinds into its own registry, so registering a native kind and a better-sidebar tab under the same id makes its bridge throw tab kind "..." is already registered. The two registrations therefore use separate, author-tagged ids — dsh-git-by-weidows (better-sidebar tab) and dsh-git-native-by-weidows (native kind) — and the native kind is never registered while the service is present. Both carry a full description for the new-tab list: repo and branch status, changes and staging, commits, history and diffs.
The panel can also be opened by other plugins:
ctx.betterSidebar.openTab({ type: "dsh-git-by-weidows", title: "Git by@Weidows", meta: { cwd } }, { sessionId })
Host API
All traffic goes through POST /plugins/dsh-git/api/<method> with a JSON body and returns { ok: true, data } or { ok: false, error: { message } }. Requests are rejected with 403 unless they come from the Harness connection. Every git call is execFile("git", argv) — never a shell — with a validated working directory, pathspecs after --, and 24 MB output caps.
| method | body | returns |
|---|---|---|
state |
{ cwd } | repo name, branch, ahead/behind, file list with per-file index/worktree state |
diff |
{ cwd, mode: "worktree" or "staged", path? } | unified patch of the working tree, the index, or one file |
log |
{ cwd, ref?, search?, limit? } | commits (hash, short, subject, author, date, refs) |
commit |
{ cwd, hash } | commit meta, changed files with +/- counts, patch |
commit-files |
{ cwd, hash } | files of a commit |
commit-file |
{ cwd, hash, path } | patch of one file inside a commit |
stage / unstage |
{ cwd, paths: [] } | fresh state |
discard |
{ cwd, paths: [] } | fresh state (tracked files only) |
commit-create |
{ cwd, message, all? } | fresh state |
branches |
{ cwd } | local and remote branches |
checkout |
{ cwd, branch } | fresh state |
reset |
{ cwd, hash, mode: "soft", "mixed" or "hard" } | fresh state |
revert |
{ cwd, hash } | fresh state |
suggest |
{ cwd, sessionId?, language? } | AI commit message derived from the staged/working diff |
Safety
reset --hard,revertanddiscardare destructive; they are only reachable from explicit clicks in the panel. A revert that hits a conflict is rolled back withgit revert --abort, so the working tree is never left mid-merge.- Discarding an untracked file is refused instead of deleting it, and untracked files never show the discard action in the panel.
- Pathspecs and branch names are validated (no leading
-, no whitespace or..in branches) and passed after--. - The plugin never runs a shell, never prompts for credentials (
GIT_TERMINAL_PROMPT=0), and never reads anything outside the session working directory.
Development
lib/index.js host half: the /plugins/dsh-git/api route and all git + LLM work
lib/client.js browser half: slots, chip, header button, panel, native + better-sidebar tabs
cordis.patch.yml bundle patch that inserts the plugin into the profile
The client half is a plain Harness client plugin (window.__ModuleLoader__.load) that registers React components into slots:
conversation.input.right— the branch chipconversation.session.header.utilities— the header buttonsidebar.right.pane.tab— the native tab body (only when better-sidebar is absent)
License
MIT
No comments yet. Be the first to write one.