dsh-client-ui-git-graph
Verified@linxin666/dsh-client-ui-git-graph · v0.3.5 · Apache-2.0 · Web UI
External dsh web GUI plugin: a blank-session git branch selector + Git graph, with real host-side git operations and guards, as a dsh profile bundle
Install
dsh plugin add @linxin666/dsh-client-ui-git-graph Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
- github/zhu1090093659/dsh-web 6178 402
Creators
Readme
dsh-git-graph
English | 中文
External dsh Web GUI plugin: a git branch selector and Git graph panel. The selector appears only in blank sessions, in the context hole of the official input selector row (conversation.input.selector.context, a session-maybe list slot) next to the official workspace selector pill. If the running shell does not declare that slot (the npm SDK rc.6 removed it), it waits CONTEXT_FALLBACK_MS then falls back to conversation.input.dock; in its blank-session hero phase the chip lifts into the official hero row immediately after the agent-preset seat, using the same transparent 28px pill recipe and --dsw-* theme tokens as the official workspace and preset chips. Active sessions expose no branch-selection control. Git capabilities run in the host process (checkout-tree git switch) and the UI is browser React; workspace selection remains the official entry.
Behavior aligns with ZCode's GitBranchSwitcher: searchable popover, a checkmark on the current item, bottom actions "创建并检出新分支… / Git 图谱" (Create and check out new branch… / Git graph), a switch guard (unresolved conflicts / an operation in progress / the target branch checked out by another worktree) and readable errors.
Repository layout and build
Kept as a sibling of the DeepSeek Harness main repo (sibling checkout, same turtle-ui layout; the path is arbitrary, below is only an example):
~/code/deepseek-harness # deepseek-harness checkout (sibling)
~/code/dsh-git-graph # this repository
All peer APIs come from the sibling checkout's source (tsconfig resolves via the paths of ../deepseek-harness/tsconfig.base.json; when the sibling directory has a different name, replace the ../deepseek-harness relative path in the tsconfig files with the actual directory). The type gate is pnpm run typecheck (tsc -b, which also builds the sibling packages referenced by references, writing declaration artifacts into the sibling's lib/ — the same design as turtle-ui).
pnpm install
pnpm run typecheck # tsc -b (including sibling referenced projects)
pnpm test # vitest (core pure functions / real git service / jsdom components)
pnpm run build # tsc -b && tsdown (lib/index.js + lib/invariant.js + lib/client.js)
lib/client.js is the browser bundle (a closure-factory artifact, window.__ModuleLoader__.load), served by the host's client-modules at /plugins/<id>/client.js; the build presets build/tsdown.client.ts + build/web/src/platform.ts are copies taken from the main repo's packages/client/tsdown.client.ts / packages/client/web/src/platform.ts, and must be kept in sync when the main repo changes.
Git installs (consumer machines without a sibling checkout) go through the prepare script: tsdown --config tsdown.prepare.config.ts transpiles directly from src without type checking (tsconfig.prepare.json is self-contained).
Activation
This package is a dsh profile bundle (package.json declares "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }). After activation, the next dsh web (or corresponding profile) startup has the bundle patch's insert line mount ui-git-graph (host half: git service + /git/* routes) together with the browser half (dsh.client declaration) into the Web composition; after a page refresh, the branch pill appears in the hero row after the agent-preset seat for a blank session and is absent from an active session.
Generic install (any machine)
This plugin is merged into the dsh-web family monorepo (github.com/zhu1090093659/dsh-web). The plugin is published to npm; one-line install recommended:
dsh plugin --profile web add @linxin666/dsh-client-ui-git-graph@latest
Or install the family aggregate package @linxin666/dsh-web-all all at once (same one-line dsh plugin --profile web add @linxin666/dsh-web-all@latest).
Install from the repository when you need to debug code:
git clone https://github.com/zhu1090093659/dsh-web.git
cd dsh-web
pnpm install && pnpm -r build
dsh plugin --profile web add link:$(pwd)/packages/dsh-git-graph
The
github:install form applies to a standalone repo whose package sits at the repository root (thepreparescript builds self-contained; pnpm ≥10 rejects it the first time, add the package key to the profile'spnpm-workspace.yamlallowBuildsper the printed error and retry). For subpackages of a monorepo use thelink:form above.
Local development loop (this repo checkout)
dsh plugin --profile <name> add link:/absolute/path/to/dsh-git-graph
A link: install references the local directory directly; a rebuild takes effect immediately without reinstalling (after a code change, pnpm run build then refresh the page). Note that link: takes an absolute path (~ is expanded by the shell, not by pnpm semantics).
Worktree isolation
The branch popover carries two worktree entries (a worktree is a linked git checkout that shares the repository history but owns its files and branch, so parallel sessions never touch each other's tree):
- Start a new session in a worktree… opens a dialog: name the worktree and pick the base branch (the current branch is preselected). The host creates the worktree at
$DSH_HOME/worktrees/<repo-key>/<name>/on a NEW branchwt/<name>(git forbids checking out one branch in two places), registers it as a workspace, and opens its blank session. The current checkout never moves. - Manage worktrees… lists every linked worktree of the repository with branch/head. Managed worktrees can be removed: a dirty worktree rejects once and then offers an inline force-confirm; the
wt/branch survives removal unless the row's delete-branch checkbox is on. The primary checkout row is display-only.
Two gates live in the plugin's settings card (both off by default):
- Auto-isolation (
autoIsolate): the New Session action of a git workspace silently creates a fresh managed worktree and starts the session there — the Claude-desktop-style automatic shape.autoBaselinepicks the base: the checkout's current HEAD (current) or the remote default branch (default, resolved asorigin/HEADwith a HEAD fallback when no remote exists). Workspaces already inside the managed home are never re-isolated, and any failure degrades to the official new-session behavior. This wraps the browser-side workspaces service at runtime — a shape-probed patch, not a source change; an SDK update that changes that service disables the feature with a console diagnostic instead of breaking anything. - Agent tool (
agentTool): registers the model-facinggit_worktreetool (create/list/remove over the same managed home, workspace-gated by the calling session's cwd). This deliberately lifts the package's "git stays off the model-visible surface" rule for opted-in users. Mind the sandbox boundary: a session's cwd is immutable, so the current session cannot move into the worktree, and under workspace-write sandboxing the agent cannot write outside its session root —createtherefore also registers the worktree as a workspace and its reply tells the model to open a new session there. Under danger-full-access the agent may work in the returned path directly.
Removal only ever targets direct children of the repository's managed directory (canonical-path containment on both sides) and unregisters the linked workspace after the disk removal succeeds.
Uninstall
dsh plugin --profile web remove @linxin666/dsh-client-ui-git-graph
Design notes
- Boundary and load-chain research and key decisions: see docs/ADR-001-plugin-boundary.md.
- The host half's
/git/*only accepts paths of registered workspaces (realpath check) and trusted clients (loopback socket + loopback Host, the same fence as dsh-ssh, plus a live paired-device cookie whendsh-remote-web-uiis loaded); the browser cannot run git against arbitrary directories, and a LAN-exposed dsh web answers unpaired non-loopback clients with 403. - The switch semantics are workspace-level:
git switch --no-guess <branch>operates on the repoRoot checkout tree and affects all sessions of that workspace; project switch = activate the target workspace and open its (reused or newly created) blank session, without changing the cwd of existing sessions. - Mount seam:
conversation.input.selector.context(the officially declared session-maybe list slot) is the context hole of the input selector row beside the official workspace pill. The branch pill renders only for blank sessions and hides when there is no session cwd or the workspace is not a Git repository. Declaration-aware fallback waitsCONTEXT_FALLBACK_MSfor that slot (the npm SDK rc.6 shell removed its declaration); if no declaration arrives, it mounts onconversation.input.dockfor the blank-session hero phase. There the chip re-anchors into the official hero row after the agent-preset seat (2px row gap, vertically centered, with matching workspace/preset chip metrics and tokens) and opens its picker downward like the official workspace menu. Active sessions have no branch-selection control. Only one seat is mounted, and late context declarations after the fallback are ignored. - Workspace selection is not inside this plugin: the official workspace pill (
conversation.input.selector.workspace) is the only entry; this plugin only provides git branch context. - Branch state refresh: fetch when the blank-session chip mounts / the popover opens / a switch succeeds, plus host SSE (
/git/events, polling workspace state every 30s while subscribed, each probe bounded by a 15s deadline so a hung git never stalls the stream) pushing external changes and a window-focus refresh (throttled to once per 5s). Active sessions do not subscribe. The SSE stream is shared across tabs through a cross-tab leader relay (Web Locks + BroadcastChannel): one stream per URL browser-wide, so extra tabs never exhaust the same-origin HTTP connection pool (#383).
Check chain
pnpm run typecheck
pnpm test
pnpm run build
Telemetry
The browser half sends one anonymous install heartbeat per UTC day to dsh-market.com: a random localStorage id plus this package's name, nothing else. The server stores only a salted hash of that id, never IP addresses, and exposes aggregate counts only. See docs/telemetry.md for the full contract.