songgms/dsh-git-graph

dsh-git-graph

仓库尚未提供可用的公开描述。

待分类deepseek

DeepSeek Git Graph

A Git Graph panel for DeepSeek Harness — commit topology, refs, commit details and per-file diffs, in the same window as your conversation.

English · 中文


The Git Graph panel

What it does

A sidebar panel, next to the conversation, that draws the commit graph of the repository you are working in.

  • The graph — branch lanes as SVG, drawn newest-first, coloured per line so a branch keeps one colour from its tip to the ref it merges into.
  • Refs — local branches, remotes and tags as chips on the rows that carry them, with the current HEAD marked.
  • Filtering — walk a single ref, or search messages, authors and hashes as you type.
  • Commit details — the full hash, parents, author and committer with dates, the commit body, and every changed file with its +/− counts.
  • State marks — a commit on the current branch's first-parent line is drawn with a solid node; a commit absorbed through a merge stays hollow, so the current branch's own history separates from the histories it took in.
  • Type chips — a conventional-commit subject (feat:, fix:, docs:, …) gets a small coloured chip in the message column, so a column of commits scans by type. The message column is wide (the author and date columns are compressed), hover shows the full title, and the date reads MM-DD HH:mm with the full timestamp on hover.
  • Diffs — click a file for a unified diff with additions, deletions and hunk headers styled through the harness's own tokens.
  • Copy and export — copy the full hash, the abbreviation, the subject or the whole message; save the commit as a .patch file.
  • Tags — create a tag at the selected commit.
  • Restore — restore the current branch to any version: a confirmed git reset --hard that moves the branch, the index and the working tree to that commit. It is the one deliberately destructive operation, so the pane asks for an explicit confirmation first and refuses while a merge, rebase or cherry-pick is in progress.
  • Detail pane — defaults to about a third of the panel width (the commit list keeps the rest) and collapses to a slim bar when you need the history full-width.
  • Branch bar — the current branch sits up front with its ahead/behind chips, and switch, new, merge and delete are four independent controls beside it. A merge or pull conflict is never resolved here: it raises a banner with Abort, and deletion is the safe -d form only (unmerged branches are refused).
  • Right-click a commit — a context menu scoped to that revision: create a branch here, merge this commit into the current branch, or switch to, merge or delete a branch whose tip is this commit.
  • Working-tree changes — the detail pane opens with a working-tree section when anything is dirty: one row per path with the porcelain letter as a left-hand marker — A added, M modified, D deleted, R renamed, T type change, U unmerged, ?? untracked — then the path and changed N, its added+deleted line total against HEAD (untracked files have none). Two letters (e.g. MM, AM) mean the index and the working tree disagree.
  • Compare — every file's diff renders as the classic unified lines or as a side-by-side old/new comparison.

The panel follows the harness theme, so it reads correctly in light and dark beside the shipped pages.

Layout

sidebar icon ──▶ main panel
                 ├── repo picker · branch bar (switch/create/merge/delete) · pull · push · stash · refresh
                 ├── toolbar: ref filter · text search · row density
                 ├── commit table  → lane graph (SVG) · refs · subject · author · date
                 │                    right-click a row for branch actions
                 └── detail pane   → hash, parents, author/committer, body,
                                     changed files, unified or side-by-side diff, copy + patch + new tag

Install

The package is a DeepSeek Harness bundle: a Host half that registers HTTP routes and a browser half that renders the panel. Installing it takes two steps: get the package, then install the bundle into a profile.

1. Get the package

From npm — once the package is published to the registry:

npm install dsh-git-graph

It is not published yet; until it is, install it straight from GitHub:

npm install github:songgms/dsh-git-graph
# or
npm install git+https://github.com/songgms/dsh-git-graph.git

2. Install the bundle into DeepSeek Harness

From a Harness session, point the bundle installer at the package directory:

plugin_manager action=install_bundle target=<path to dsh-git-graph>

The target is the package directory: node_modules/dsh-git-graph when the package was installed with npm, or this checkout's own directory when you are developing it.

install_bundle performs the package installation and the bundle selection, and the change survives a restart. Do not hand-edit the profile's package.json or cordis.patch.yml — the installer owns those.

Then:

  1. Reload the page. The browser half is a client module, and a running page's boot graph does not contain it until the shell reboots.
  2. Restart the profile if this package was already installed before the change: the loader caches a plugin module by URL, so a link:-installed bundle keeps executing the JavaScript the process first imported.

Verify it is live with cordis_inspect_query: Slots.listSubTree on sidebar.panellist should show id: git-graph, and on main a key: git-graph. The Host row is include:git-graph.

Configure

Everything is optional; the defaults work without a patch layer.

- id: git-graph
  name: dsh-git-graph
  config:
    extraRoots: []        # extra repositories always offered in the picker
    probeRoots: []        # extra paths probed for a containing repository
    discoverMaxDepth: 6   # parent levels walked when looking for a repository
    scanChildren: true    # also probe each candidate's immediate subdirectories
    maxChildren: 24       # cap on subdirectories probed per candidate
    discoverCacheMs: 5000 # probe-result TTL
    timeoutMs: 30000      # ceiling for one git invocation
    maxCommits: 5000      # maximum commits one page may request

An invalid value is reported with its path and falls back to the default, so a typo in a patch layer never leaves the panel unable to open.

How the panel finds a repository

Two independent sources, merged:

  1. The open workspace — the page's session snapshot carries the directory and the Host probes it, then its parents (discoverMaxDepth), then its immediate subdirectories (scanChildren). A workspace that is a repository is found by the first; a session opened on a parent folder holding several checkouts is found by the last.
  2. The Harness process directory — plus any configured extraRoots or probeRoots.

A repository is identified by git rev-parse --show-toplevel, so the picker shows the repository root rather than whichever subdirectory happened to match. When nothing is found, the empty state accepts a pasted absolute path.

What it writes

Everything the panel does is a read except a small, deliberate set of repository operations, all of which live in the header's top-right corner and are user-only: the agent cannot reach them.

OperationWhat it runsWhy it is safe here
New branch / New taggit branch <name> <commit> / git tag <name> <commit>Add one ref, touch nothing else. An existing name is refused, and a name is checked locally and then by git check-ref-format --branch.
Pullgit pull (merge / rebase / ff-only)The one operation that can leave conflicts. The panel detects them (git's own markers and unmerged paths), shows a banner, and offers Abort — the machinery the read-only design deferred is now in place.
Pushgit push to the branch's upstreamNever touches the working tree; a refused push leaves the repository exactly as it was.
Stashgit stash push -u, list, pop, dropFully reversible: saving captures the changes (untracked files included by default, with an optional message) and the menu restores or deletes entries; a conflicting pop keeps the stash intact.
Switch branchgit checkout <branch> against a verified refs/heads/<name>The branch bar's top row. git itself refuses a switch that would lose work, and the unknown-name path is refused before checkout runs.
Merge branchgit merge --no-edit <branch> against a verified refs/heads/<name>A conflict leaves MERGE_HEAD and unmerged paths, which the same banner and Abort handle as a conflicting pull; a clean merge creates one commit and touches nothing else.
Delete branchgit branch -d <branch> against a verified refs/heads/<name>The SAFE form only: -d refuses a branch whose commits are not merged, so no route can delete commits that exist nowhere else. The checked-out branch is refused up front; there is no force-delete anywhere.
Merge a commitgit merge --no-edit <hash> against a verified commitThe right-click path: merges one revision into the current branch without checking anything out. Conflicts leave MERGE_HEAD and unmerged paths, handled by the same banner and Abort.
Restore to a versiongit reset --hard <hash> against a verified commitThe deliberately destructive one, walled in: the pane requires an explicit confirmation, the hash is resolved first, and a merge/rebase/cherry-pick in progress is refused (OPERATION_IN_PROGRESS) rather than clobbered. --hard means uncommitted changes and commits above the target are gone.

Save as patch is a read: git format-patch -1 --stdout renders the commit as an mbox patch and the browser turns it into a download, so the plugin still creates no file on disk.

Every state-changing route is POST-only and requires the x-dsh-plugin CSRF header, so a cross-site page cannot drive any of them.

Cherry-pick, revert, rebase by hand and fixup/squash are still not exposed. They need machinery this plugin does not have — a rebase todo editor, and a pre-flight check for uncommitted work beyond the explicit confirmation the Restore action already requires. Shipping them without that would turn a plugin that is safe by construction into one that is safe by luck.

Architecture

PiecePathRole
Host halfindex.jsMount only: config schema, webServer route registration.
Git layerlib/git.jsEvery git invocation: argument arrays, no shell, non-interactive env, hard timeout. Parses log --numstat and show --numstat.
HTTP surfacelib/routes.jsThe /api/dsh-git/* handlers.
Live loaderlib/live.jsRe-imports the two modules above when their bytes change.
Layoutlib/graph.jsPure lane assignment for the commit DAG. No DOM, no React.
Client halfclient.jsSidebar icon, panel, graph renderer, detail and diff view. Generated artifact.
Stylesclient.cssTheme tokens only (--dsw-alias-*), light and dark.

Design notes

  • One request per page. git log -z --topo-order --numstat carries both the topology and every commit's change size. -z is not optional: without it git emits the whole numstat stream after the last formatted commit, so every file list lands on the wrong commit.
  • Lanes, not merge geometry. A lane is handed to exactly one parent, so a merge is a plain horizontal edge into the node rather than a special case.
  • Windowed rendering. Only the rows inside the viewport exist in the DOM, and the graph is one absolutely positioned SVG translated to the window, so a line running past the viewport still draws into it.
  • Truncated history is visible. An edge whose parent falls outside the loaded window is dashed to the bottom of the graph.
  • The panel installs its own stylesheet. The client runner's styles builtin is a closure symbol of dynamic packages; an installed client module's factory never receives it. The panel asks the composition for a styles service and otherwise appends its own <style> element.

Why index.js is nearly empty

The loader caches a plugin module by URL, so a link:-installed bundle keeps executing the JavaScript the profile first imported — a set_plugin cycle remounts the fiber but reuses the cached module. So index.js does nothing but mount, and everything behavioural is imported through lib/live.js behind a content stamp:

Edited fileTakes effect
lib/git.js, lib/routes.jsOn the next HTTP request — no restart
index.js, cordis.patch.ymlAfter a profile restart
client.js, client.cssAfter a page reload (the bundle URL is content-revisioned)

Development

npm run build     # inline lib/graph.js and client.css into client.js
npm run check     # parse-check client.js and confirm it is up to date
npm run verify    # every offline suite (needs pwsh)

client.js is a committed build artifact: it is a hand-written window.__ModuleLoader__ package shell, and tools/build-client.mjs inlines lib/graph.js and client.css between markers inside it. Edit those two files, never their inlined copies, then run npm run build.

Verifying

pwsh tools/verify-offline.ps1 [repository-path]   # 232 checks, no server

node tools/serve-routes.mjs 19417                 # the real route table, no app
pwsh tools/verify-live.ps1   -Base http://127.0.0.1:19417/api/dsh-git   # 42
pwsh tools/verify-routes.ps1 -Base http://127.0.0.1:19417/api/dsh-git   # 30
SuiteCovers
tools/verify.mjsparseLog framing, lane assignment and its invariants, edgePath, the browser half's shell and slot registrations, the rendered tree, and the copy menu and ref-action controls driven through their real state machines.
tools/verify-host.mjsThe whole Host half against a real repository: discovery, status, refs, log, commit detail, diff, patch rendering, and every refusal — 13 malformed ref names, duplicate names, unknown kinds, missing commits. The writing path runs against a throwaway repository it creates and cleans up.
tools/verify-reload.mjsThat lib/live.js genuinely re-imports changed bytes.
tools/verify-live.ps1Every route over real HTTP.
tools/verify-routes.ps1/ref and /patch over real HTTP: method and CSRF refusal, creation, duplicate refusal, and that the working tree and HEAD are untouched.

tools/make-demo-repo.ps1 builds the fixture the first two run against — a deliberate merge topology with two feature branches, a nested merge, an annotated tag and a hotfix branch.

Why a harness. A route added to lib/routes.js does not exist in an already-running profile until it restarts, so tools/serve-routes.mjs mounts the same route table on a throwaway port. It adds only exact-path dispatch; everything it answers is the plugin's own handler, guard and CSRF check.

Why nothing captures a child's stdout. The development sandbox refuses the pipes execFile and spawnSync need, so each suite routes git's bytes through a file. The code under test is unchanged either way.

What the suites do not establish: pixels. Rendering, theme resolution and scrolling need the live page.

Contributing

Issues and pull requests are welcome. Two ground rules:

  • client.js is generated. Change lib/graph.js or client.css and run npm run build; a PR that edits the inlined regions directly will be asked to regenerate.
  • New writes need a safety story. If you want cherry-pick or reset in here, open an issue describing the confirmation, conflict and pre-flight handling first — see What it writes for why.

License

MIT © 2026 songgms

DeepSeek Harness is a separate project developed by DeepSeek; dsh-git-graph is an independent contribution to its plugin ecosystem.

dsh-git-graph is a plugin for DeepSeek Harness.