tu1918/dsh-git-plugin ↗★ 0

dsh-git-panel

右侧边栏常驻的Git工作区管理面板 适合需要在DSH内直接进行代码暂存、提交和推送的开发任务。

包名
dsh-git-panel
兼容性
待验证
Harness 依赖范围
^0.2.0-rc.2 || >=0.2.1-0 <0.3.0
Cordis 依赖范围
^4.0.4
版本
1.0.1
许可证
MIT
最近更新
2026年10月6日

同名包的其他仓库

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:tu1918/dsh-git-plugin

dsh-git-panel

A resident git panel for the DSH Web GUI's right sidebar: see the session workspace's changes, grouped the way git groups them, with the branch's state against its upstream — without leaving DSH and without a modal overlay covering the conversation.

The panel as a tab of the right sidebar: the branch rail above, the staged changes / changes / untracked files sections and the commit box, and the recent-commits list below with its swimlane graph

Released as v1.0.0: the foundation, a read-only panel, the commit loop (stage → commit → push), the diff view, branch management with the merge state, an AI-written commit message, a commit detail — then M5a's discard / undo / stash, M5b's commit-file drill-down, copy entries, the commit graph, multi-repository workspaces, the history row's rewriting operations (revert, cherry-pick, reset, squash, drop), and the release pass itself, which closed the one defect left open (a git operation could leave the panel spinning for good). Every item of the original M5 list — discard, stash, the commit graph, undo, multi-repository — has shipped.

Docs

FileWhat it is
docs/TODO.mdThe open-items list — only what has not shipped, each entry with why it is not done and where its plan lives. An entry is deleted once it ships.
docs/plan.mdThe execution plan: milestone status against the original acceptance criteria, what each completed milestone delivered and where, every deliberate deviation from the original requirements with its reason, and the next milestone's task list.

What works today

AreaState
Change list: staged / changes / untracked / conflicts. Each row leads with a glyph for what the file IS (code, markup, stylesheet, data, image, prose, shell, settings, or a plain page), and its own right-hand column carries the status letter, which is the English initial of the state (U for untracked) with VS Code's ! for a conflict, plus the meaning as a tooltip✅
That list as a file tree — directories fold, single-child chains compact into one row, each directory carries its subtree's file count — or as a flat list; the choice is remembered per user✅
Branch rail: name, detached / unborn / upstream-gone, ↑ahead ↓behind✅
Recent commits: paged by look-ahead, pushed/unpushed marker, read only while its tab is showing✅
Each history row is decorated with the refs pointing at that commit — local branches, remote-tracking branches (upstream/main) and tags (v0.2.9) as badges on its second line, three inks for the three kinds, at most three plus a +N that names the rest. Read from the ref namespaces rather than from git's decoration string, which cannot tell a local branch with a slash from a remote-tracking branch✅
Auto-refresh from filesystem events — one recursive watch on the work tree, one on the git directory — pushed over SSE with what moved (refs / index / worktree); a file an agent writes shows up without touching .git at all✅
A state-file poll as the fallback when events are unavailable (network drives, platforms without recursive watch), and a stat of the same files when even that fails✅
Live git status/branches/log over /git-panel/*✅
Stage / unstage, one file or a whole group✅
Commit box with an explicit scope: the index only, or every tracked change (the button says which, and how many)✅
Pull ↓ / push ↑n / sync ⇅, with the upstream set on the first push✅
Fetch every remote — a cloud with a ↓ under it, beside Pull — updating the remote-tracking branches without touching the working tree, the index or the current branch, so the ↑/↓ counts and the ○/● markers learn what the remote has and a merge can never be the side effect. A repository with no remote is told so rather than shown a silent success✅
A git operation is bounded end to end. The host's deadline escalates SIGTERM → SIGKILL and settles on the process ending, so a helper that ignores the polite signal (or holds the pipes open after its parent dies) can no longer leave a request pending; the request carries a deadline of its own in the host and one more in the browser. A mutation that runs past it says the result is unknown — git may still be running — and the panel re-reads the repository instead of lying about what happened✅
Remote-tracking branches in the branch picker, as a read-only section (origin/feature plus its tip's subject), read while the picker is open and so refreshed by the same fetch. The rows are labels, not buttons on purpose: checking a remote branch out needs the rebase-onto-origin / drop-local-commits decision and can land in a conflict, which belongs with the conflict view✅
Multi-repository workspaces (FR-8): when the session's directory is a container rather than a repository, one level below it is scanned for repositories (skipping dot-directories, node_modules, dist, build) and the rail grows a picker — only when there is a choice, so a single-repository workspace looks exactly as before. The default is the repository whose git state moved most recently, and the choice is remembered per container. The browser may only choose among roots the host itself found there, so naming a path of its own gets a refusal✅
HTTPS credentials. A remote that wants one and has none is named as such (auth-required, with the origin git itself printed), and the same failure notice grows a username / password form. "Save and retry" stores the pair through the harness's own credential seam — ctx.credentials, so the provider owns where the value lives, and this profile's local provider writes its own 0600 document rather than us inventing a store — and then retries the very operation that failed. Every later push/pull/fetch/sync resolves the stored credential and hands it to git through GIT_ASKPASS, with GIT_TERMINAL_PROMPT=0 unchanged so nothing can ever hang on a prompt. The value never reaches a log line✅
Push refused as non-fast-forward → points at Sync instead of git's hint text✅
A pull whose tracked upstream was deleted on the remote says so and names the way out (git branch --unset-upstream, or repointing it) — instead of forwarding git's "no such ref was fetched", which never says the branch is gone✅
UI in zh + en✅
Change groups fold away individually, and stay folded (the count stays visible)✅
A resident staged drawer directly above the commit box: it is what that box commits, and its empty state says so✅
Changes and Untracked are sections of ONE scrolling list rather than drawers of their own: the panel has a single resize grip (the dock's), and each section's header pins itself to the top of that scroller, so a partially scrolled list still says which section you are in✅
A clean working tree is stated by the sections themselves: Changes and Untracked stay on screen counting zero (the paragraph that used to say "no uncommitted changes" is gone), and only the staged drawer needs a note, because its header alone does not say why it is empty✅
A group with no rows offers no bulk action — except the staged drawer, which keeps its button on screen, disabled, with the empty note as the explanation✅
One bottom pane, one strip of tabs — recent commits, plus one tab per open diff — opening on the history tab by default and folded/opened by the tabs themselves (clicking the tab that is showing puts the pane away), sized by a drag handle, keeping every open tab's content loaded; the fold and the height are remembered. Any number of diffs may be open at once, a tab is marked by the same rule as the history tab, and hovering (or focusing) a diff tab reveals that tab's own × — closing one file is not closing the pane✅
Diff view: click a change row → the diff opens as a tab of the bottom pane (beside the commit history, never a modal), half the screen tall and drag-resizable, inline or side-by-side, remembered; the diff's own header carries its operations only (layout, reload, and — in the dock — open in a new tab of the right sidebar, which moves the reading out of the dock and into a tab of its own so it gets the column's full height), and a long file name is ellipsized in the tab and in the diff header, with the directory giving way first (FR-1.2)✅
Diff operations have three anchors rather than one toolbar, because the three kinds cannot share a place: the header's labelled group carries the VIEW operations (layout, open-in-tab, reload — data-op-group="view"), the row between two hunks is where BETWEEN-LINE operations go (registered but not built), and every line's tail reserves a slot in CSS for an operation on that line (data-op-group="line"). Nothing is drawn for the two ends that have no control yet: a clickable box with no action behind it is worse than none✅
Side-by-side diff is a fixed split: each half is half of the pane whatever the lines are, and each half is its own scroller — a line too long for its half is reached with the split's shared horizontal bar, and the two halves are kept in step on both axes so a paired line stays paired. Two independent scrollers cannot share a native scrollbar, so the halves hide theirs and the split draws one bar per axis at its own edges: the vertical one down the right edge, the horizontal one across the bottom. The halves are separated by a 16px lane (outside both scrollers, so nothing a half scrolls can push it around), with a hairline down the divider. A per-line action does not live in that lane — the halves have nothing per-row between them — but in the tail of the line's own row. The inline layout is the single-scroller one, where the full width is the reading width✅
Word-level highlighting inside a changed line, from VS Code's own diff engine✅
Diff of the index vs HEAD (--cached) or the worktree vs the index, untracked as all-new✅
Binary files, conflicts' combined diffs, and >5000-line diffs each stated rather than mis-drawn✅
Branch picker: switch, create (from HEAD or a chosen branch), delete — with the unmerged case asking for a second, forced click✅
That picker as a real dropdown: it floats over the panel (hanging off the branch button and lined up with it, capped at the room left) instead of pushing the staged drawer, the commit box and the change list down; an outside press or Escape closes it✅
A blocked switch shows git's own multi-line refusal, verbatim✅
An interrupted operation's own bar: a merge, a revert, a cherry-pick or a rewrite that stopped on conflicts (or on a pick that became empty) says which one it is and offers the moves that operation really has — continue, abort, and (for a rebase) skip. The state comes from stat-ing the markers in the git directory (MERGE_HEAD, REVERT_HEAD, CHERRY_PICK_HEAD, rebase-merge/), not from the conflict list: once every conflict is staged the list is empty while the operation is still open, and a stopped rebase may have no conflict at all. Continue and skip wait until no unmerged path is left, abort takes two clicks✅
A conflicted row's + is labelled as marking it resolved — the same git add it always was✅
Commit message written by the deployment's default model from the staged diff, truncated to a budget and said so, landing in the box as editable text✅
A history row IS one real button — both of its lines, so the clickable area is exactly the hover band — and selecting it splits the pane: the entries stay on the left, that commit's information opens on the right (metadata, then its file list with per-file churn and git's own binary answer)✅
One height budget for the whole column (ui/panel-layout.ts): every region is bounded, the change list is the only one that grows (a zero flex basis keeps its content out of the arithmetic, so a thousand changed files cannot squeeze the drawers or the dock), the staged drawer is content-sized with no floor — a collapsed one leaves no blank band, so the commit box sits tight against it — and the dock's drag, plus any height it remembers from a taller window, is clamped at what the other regions leave it✅
Section headers pinned to the top of the list they scroll in: a partially scrolled change list still says whether it is showing changes or untracked files✅
A change row's toolbar: right-click it (or Shift+F10 / the menu key on the focused row) for that row's own action — stage, unstage, or mark a conflict resolved. Two groups, one hairline: git's actions first, the copying entries after them, and nothing painted red — §4.3's warning is the second click and the sentence the entry reads out between the two. Every entry carries a mark, and the card opens at the pointer, only as wide as its own words, pulled back inside the panel and flipped above a pointer that sits too low. Deliberately NOT the rail's dropdown: a right-click used to open the branch list's full-width strip✅
Discard a change (FR-6.1): a working-tree row gains a third button — and its toolbar a third entry — that takes two clicks and says "cannot be undone" between them. It restores a tracked file from the index (never from HEAD, so it also works before the first commit) and deletes a file the index has never seen; a staged or conflicted row offers it nowhere✅
Undo the newest commit (FR-3.8): the newest history row's toolbar carries one armed entry whose confirmation says which undo is coming — an unpublished commit is reset (--mixed, its changes return to the working tree), a published one is reverted (a new commit; history is never rewritten). The host re-resolves HEAD and re-asks the pushed question at execution time, so a stale row is refused rather than silently undoing a commit nobody pointed at✅
The stash (FR-6.2), opened from the rail like the branch list: save the working tree (with an optional label, and untracked files only if you ask for them), read the stack, apply or pop an entry, or drop one. Entries are addressed by commit id rather than by stash@{n}, so a stack another window shifted cannot be acted on at the wrong position; dropping is the one irreversible action here and takes two clicks✅
A switch git refuses because the working tree is in the way (FR-4.4) shows git's multi-line refusal and offers "stash, then switch to …": one click stashes (untracked files included, because those are exactly what git sometimes names) and retries the very switch that was blocked✅
Drill into one file of a commit (FR-7.2): every row in a commit's file list is a button that opens that file as the commit changed it, in the same bottom diff tab a change row uses — `git show -m --first-parent --
`, read against the revision rather than the working tree, so an uncommitted edit to the same file cannot appear in it. The reading follows moved refs only, and it is not swept away when the file is absent from the change list✅
Commit graph (FR-7.1): every history row carries its own swimlane strip — a first parent continues straight down, an extra parent opens a lane, and a line rejoins when the branches meet. The assignment is one pass over all loaded commits, so loading the next page extends the diagram without redrawing it (pagination cannot break the lines); the strip's width is one number shared by every row, so a merge cannot shift the hashes✅
Rewriting a commit from its row's toolbar, every entry armed (two clicks, the second one named): Revert this commit (a new commit that reverses it — any commit, and history is never rewritten), Cherry-pick this commit (apply it to the current branch as a new commit; a pick whose change is already here is cleaned up and refused rather than left as an operation with nothing to do), Squash into the previous commit (folds it into it