Code management panel for the DSH web UI that handles everyday Git in five steps: ① env check auto-detects git/gh/ssh and one-click installs missing tools (gh mirror download sources speed up installs, Windows included) with check-update; ② SSH ed25519 key generation / connection tests plus one-click 443 config to bypass blocked port 22, with the Gitee personal token configured here; ③ repo status, side-by-side diffs, stage/commit, branch switching, history with revert/cherry-pick, one-click repo creation and push, auto-ignoring >100MB files; ④ lists all repos of your account for one-click clone; ⑤ five-step npm publish wizard. GitHub / Gitee dual-platform, with the UI switching between 中文 and English live.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:Zhucy123/source-code-mgmt
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
Version: v1.25.0 | 中文版见 README.md
Bilingual UI, live: the panel and host-side messages follow DSH's language setting (Settings → General → Language) — switching between 中文 and English takes effect instantly, no refresh or restart needed.
A source-code management plugin for the DSH Web GUI: one「Code Management」panel chains ① environment check → ② SSH key & connection → ③ code management → ④ clone repos → ⑤ publish npm package, with GitHub / Gitee dual-platform support. The environment check auto-detects Git / GitHub CLI / SSH and can install missing tools in one click (platform-adaptive, sudo-free where possible; gh supports mirror download sources, including on Windows), plus a check-update to track new versions.
The entry is registered into DSH's OWN right sidebar by default (no separate button): on that column's start page「Code Management」is a card beside 工作区文件 / 新建终端 / 浏览器 / 壁纸引擎, and picking it opens the panel as a page in that column. Older hosts degrade automatically: with dsh-better-sidebar it registers as one of its sidebar Tabs; with neither (old hosts) it falls back to the DSH top-right header entry — beside "Session log" while a session is active, pinned at the top-right at the exact same height when there is no session. It never occupies DSH's own left sidebar.
Features
Entry points (selected automatically by what the host offers; at most one exists at any time):
- DSH's own right sidebar (default, harness ≥ 0.1.5):「Code Management」is a card on the column's start page (its guide) plus a page in that column. Picking the card opens the panel in the right sidebar (the host's own behaviour:
openTab(kind, { replaceTab: true }), replacing the start page itself, the way a browser start page yields to the site you click). The card sorts after the three built-ins (workspace files 10 / new terminal 20 / browser 30) and after other plugins (the wallpaper engine uses 100) — we take 110.- There is no separate button: the seat
sidebar.right.pane.tabexisting is the host having that column, and while it exists the plugin registers no header entry at all — that is where "drop the standalone button" lands. - The card's title/description are functions (
title: () => t("代码管理")) that the host evaluates on every render ⇒ switching language needs no re-registration. - The panel uses
variant: "tab": it fills the column and draws no ✕ of its own (the host's tab strip owns closing).
- There is no separate button: the seat
- dsh-better-sidebar installed (older-host fallback):「Code Management」registers as a new Tab page of its sidebar;
- Neither (older-host fallback): the entry lives in the conversation header only, in two strictly mutually exclusive places (exactly one is visible at any time):
- ① with an active (non-blank) session: the right-aligned list beside "Session log" (
conversation.session.header.utilities); - ② with no session / a blank new chat: a pinned entry at the top-right, at the same height as ①. Neither overlaps DSH's own controls, and nothing is ever registered into DSH's left sidebar.
- ① with an active (non-blank) session: the right-aligned list beside "Session log" (
The seat may arrive late (600ms grace window):
sidebar.right.pane.tabbelongs to another package and the loader does not guarantee order. While it might still appear, no fallback button is mounted — otherwise an official host would flash a「Code Management」button that is then replaced by the sidebar card. If the seat shows up inside the window it is never mounted; only if the window expires does the plugin fall back to the older-host path (which those hosts would take anyway, where a button appearing 0.6s later is not visible). Zero I/O — just one timer.
Detection is two in-memory reads at activation time (the seat declaration and
ctx.get('sidebarRightTabs')) — zero I/O, zero network, no impact on DSH startup.
The panel has five steps (①②③ are the core three; ④⑤ are the newer extensions, collapsed by default):
① Environment Check
- Shows the OS (friendly names
Windows/macOS/Linux, from the underlyingwin32/darwin/linuxplatform ids) - Detects Git and GitHub CLI presence and versions (e.g.
git version 2.55.0,gh version 2.97.0) - Detects whether an SSH client is available
- Missing tools → adaptive install guidance + one-click install (mostly sudo-free): when a tool is missing, the row shows「❌ 未安装」+「复制安装命令」(copy install command) +「安装」(install):
- GitHub CLI (Linux / macOS): user-level install without sudo — downloads the official binary for the current platform/architecture from GitHub Releases, extracts it and installs to
~/.local/bin/gh(the directory is created if missing; no admin rights needed); falls back to the system package manager on failure. - Git (Linux): apt / dnf / pacman (auto
sudo -n; passwordless sudo is probed first — when a password is required the panel does not run a doomed command, it shows "run this manually in a terminal" guidance with the exact command). Git (macOS): brew, or Xcode Command Line Tools (xcode-select --install, the OS-provided installer that also ships git/ssh) when brew is absent. - SSH (Windows): built-in optional feature (admin); SSH (Linux): openssh-client via apt / dnf / pacman (same sudo probe); macOS: bundled with the OS.
- Copy install command and the Install button share the same platform-adaptive logic (no more hardcoded Windows winget commands on Linux/macOS).
- Download source selector for GitHub CLI (all platforms): because direct
github.comdownloads can stall on domestic networks, when gh is missing the row shows a「下载源」(download source) dropdown —gh-proxy.com(fastest in tests, ~2-3.5MB/s) →ghfast.top/ghproxy.net(usable, slower) →Official (slow). On Linux/macOS the mirrored.tar.gzis unpacked to~/.local/bin; on Windows the mirrored official.msiis downloaded and silently installed viamsiexec /qn(same effect as winget but bypasses the slow direct download). The host builds the download URL from the chosen source (mirror = prefix + official Releases URL); Install and Copy install command both follow the selection. The mirror list is served by/env(ghMirrors), so the browser never hardcodes mirrors. - Re-check button whenever something is missing: the hint row under the tool list always includes a「重新检查」(Re-check) button — after a manual install or a refresh you can re-detect without restarting anything.
- GitHub CLI (Linux / macOS): user-level install without sudo — downloads the official binary for the current platform/architecture from GitHub Releases, extracts it and installs to
- Live install-progress dialog: clicking Install opens a progress window showing each step ("fetch latest version → download → extract → install → clean up") with streaming output; success/failure reasons are shown when done. On success the dialog says「安装完成…,刷新网页即可生效」(installed — refresh the page to apply); no DSH restart is needed.
- Re-detects automatically after install.
② SSH Key & Connectivity
- Platform selector: GitHub (default) / Gitee — decides the SSH config target and connectivity test below
- Auto-detect ed25519 key: scans
~/.ssh/*.pubfor existing ed25519 public keys — prefersid_ed25519, otherwise the first found (any name, e.g.github_ed25519); falls back toid_ed25519when none exist. The status line shows the actual key filename, and the SSH configIdentityFileuses it too - One-click generate ed25519 key (no passphrase; reuses an existing ed25519 key instead of duplicating)
- One-click write SSH config (GitHub:
github.com → ssh.github.com:443; Gitee:gitee.comport 443) — see Do I need the 443 config? below - Test connection
ssh -T git@github.com(GitHub) orssh -T git@gitee.com(Gitee) - Shows the public key content for easy copy-upload to the platform
- Detects whether
ghis logged in and which account; when not logged in, the「GH login」row shows a「登录 GitHub」(Log in to GitHub) button (host opens a terminal runninggh auth login— interactive, complete it there, then click Re-check) plus「复制登录命令」(copy login command) and「重新检查」(Re-check)
③ Code Management
- Follows ②'s platform: all「detect / create / visibility」logic switches with the platform selector (GitHub via
ghCLI, Gitee via Gitee OpenAPI) - Gitee token (Gitee mode only): enter a Gitee personal access token (needs
projectspermission) → stored at~/.dsh/storages/source-code-mgmt-gitee.jsonon the machine (0600, not inside the plugin dir, never echoed to the browser/logs); one-click clear; invalid tokens are removed automatically - Workspace selector: dropdown of DSH-registered workspace folders
- Select folder →: paste an absolute path or click「Browse…」for a native folder picker; confirmed folders are persisted into a custom folder list (
~/.dsh/storages/source-code-mgmt-dirs.json, separate from the plugin dir — no personal paths leak), shown with a custom-folder badge and a ✕ to remove the entry (record only, never deletes the actual folder) - Shows repo status: platform source, branch, remote, pending change count, ahead/behind remote, >100MB files
- View details: when there are changes, a「查看」(view) button opens a dialog listing changed/added/deleted/renamed files or folders. Every text-previewable file — including new/untracked files — can be expanded to show an inline side-by-side diff (old left / new right, deletions red, additions green); files with no old version (pure additions) show only the 「新版本」column; binary files show no「查看」button (their content can't be previewed as text). When local and remote diverge, a view button on the sync row lists the concrete commits you're ahead/behind
- Local Git workflow (does not change the remote-sync logic):
- Stage / unstage per file in the changes dialog (distinguishing staged/unstaged by
git statusXY codes), with「已暂存 / 未暂存」markers - Commit message input + Commit button above the repo name (git repos with changes only) — custom message, or auto-generated when left empty
- Branch「Switch」 button — dialog listing branches, click to
checkout - History button — dialog listing commits (hash + subject + author + date), each with view (side-by-side diff), revert, cherry-pick (both with confirmation, as they rewrite history)
- Stage / unstage per file in the changes dialog (distinguishing staged/unstaged by
- Remote matching by platform + current account: ③'s「remote / sync」only counts remotes of the current platform whose owner equals the currently logged-in account (GitHub platform =
ghaccount, Gitee platform = Gitee token account). So switching to Gitee never reads the GitHub origin; someone else's / another org's repos (e.g.deepseek-ai/deepseek-harness) are never treated as "your own remote", and ahead/behind is not computed for them. With no account-owned remote, the panel shows「(无)」and offers the same-name repo check + create-and-push flow - Init Git: shown when the folder is not a git repo but a same-name repo already exists remotely — runs
git initonly (+ default identity), no pull, no push, you choose the next step - Create repo & push: repo name defaults to the folder name (read-only), optional private/public; the button is disabled with a hint when a same-name repo already exists. For a brand-new directory with no commits, it auto-stages an initial commit before creating the repo to avoid "no commits found"
- GitHub:
gh repo create --private|--public --source=. --push - Gitee: Gitee OpenAPI
POST /user/reposto create, then sets the SSH remotegit@gitee.com:<owner>/<name>.gitandgit push(through ②'s SSH key)
- GitHub:
④ Clone repos
- Follows ②'s platform: lists every remote repo of the signed-in account (GitHub via
gh repo list, Gitee via OpenAPIuser/repos) and flags which already exist locally (default = default workspace + registered workspaces + custom dirs) - Clone into: optional target parent directory (default = DSH's default workspace; leave empty to clone to the default location); cloning uses the SSH URL and auto-adds the clone to the custom-directory list so ③ can select it directly
- Repos already present locally can't be re-cloned; every other repo has a「克隆」(clone) button
⑤ Publish npm package
- Five-step wizard (run in the target directory): ① check registry
npm config get registry→ ② view auth confignpm config list(auto-redacts token/auth/password) → ③ verify identitynpm whoami→ ④ preview packed filesnpm pack --dry-run(see exactly what would be published, nothing is packed or uploaded) → ⑤ publishnpm publish - Login and publish always use the official registry
https://registry.npmjs.org: even when your global npm config points at a mirror (e.g.registry.npmmirror.com— mirrors only sync, they do not accept publishes),whoami/npm login/npm publishall carry--registry=https://registry.npmjs.org; the status area shows a yellow "publishing uses the official registry" hint when a mirror is configured. - "Open terminal to run npm login" now spawns a terminal safely: it first probes for a terminal that actually exists on PATH (
konsole/gnome-terminal/ xterm-family, etc.), attaches anerrorlistener to every child, and useskonsole --separateon KDE — fixing the old bug where spawning a missing binary (e.g. nox-terminal-emulatoron SteamOS) raised an uncaught async ENOENT that crashed the dsh host process (which looked like yourpnpm dsh webterminal dying). When no terminal exists it returns explicit "run manually: " guidance instead of pretending success. - Publishing requires ticking「I've reviewed the above — confirm publishing to the npm registry」first, preventing accidental publishes
Data loading timing (fetch on open, "refreshing…" indicator)
DSH does not sync repos on startup — it only prefetches static env/SSH/workspace lists. Opening the plugin, switching workspace/folder, refreshing, and any push/pull/stage/commit operation fetch the latest repo status over the network and show a「⟳ 刷新中…」indicator — no stale data (like an old "no changes") when opening/reopening/switching.
Do I need the 443 config?
Short answer for users outside mainland China / port-22-blocked networks: usually no.
- GitHub's standard SSH endpoint is
git@github.comon port 22, and it works out of the box on almost every network outside mainland China. - The plugin's「write SSH config」step writes GitHub's officially supported port-443 fallback (
Host github.com → HostName ssh.github.com, Port 443). It exists for networks that block port 22 (common in mainland China, plus some corporate/school/campus or otherwise firewalled networks). - The step is fully optional and user-initiated — you can simply skip it: generate the key → add the public key to GitHub → test connection → push. Everything runs over port 22.
- Writing it anyway is harmless (GitHub officially supports SSH over 443; the only edge case is networks that block 443 as well). Gitee is a China-hosted platform, so it's only relevant if you actually use Gitee.
Installation
This plugin ships as a Profile Bundle: its
package.jsondeclaresdsh.bundle(carrying acordis.patch.ymlconfig layer), sodsh plugin --profile web addinstalls and activates it in one step — no manual config editing.
Option 1: npm package (recommended)
Run this from any directory (the command locates/initializes the web profile itself):
dsh plugin --profile web add source-code-mgmt
The command runs
pnpm addin the web profile directory, then reconciles the plugin layer: because this package declaresdsh.bundle, it is automatically appended todsh.profile.bundles(see~/.dsh/profiles/web/package.json) and registered into the Cordis loader tree — one command, done.
After installing, refresh the browser (F5). The「Code Management」entry appears (sidebar Tab with dsh-better-sidebar, otherwise the header button + right panel). No DSH restart is needed — host-side tool detection re-probes on every check.
Option 2: local directory (development / testing)
Windows (PowerShell):
# install local source (link: protocol — a symlink, source edits take effect immediately)
dsh plugin --profile web add link:C:/path/to/source-code-mgmt
dsh web
Linux / macOS:
dsh plugin --profile web add link:/home/yourname/path/to/source-code-mgmt
dsh web
Option 3: from GitHub (distribution)
dsh plugin --profile web add git+https://github.com/Zhucy123/source-code-mgmt.git
dsh web
A git install copies the source into node_modules — to pick up source changes, re-run
dsh plugin --profile web add ...(unlikelink:, which is a symlink).
Verifying the install
After installing and refreshing the page:
- Dependency written:
source-code-mgmtis independenciesof~/.dsh/profiles/web/package.json. - Added to the config layer:
source-code-mgmtis in thedsh.profile.bundleslist of the same file (written automatically bydsh plugin add— no manual editing). - Symlink created (
link:only):~/.dsh/profiles/web/node_modules/source-code-mgmtpoints at your source dir (a Junction on Windows). - Entry visible after refresh: sidebar「Code Management」Tab with dsh-better-sidebar, otherwise the header button beside "Session log" opening the right panel.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
Installed with dsh plugin add but no button |
Usually the page was not reloaded after install. Refresh the browser (F5); if the entry still does not appear, restart the dsh web process (it may hold port 3080). |
| Install prints「declares no dsh.bundle」 | The installed version lacks the bundle declaration (old version or a package missing cordis.patch.yml). Reinstall/update with version ≥ 1.9.0. |
| 「Failed to load plugins」 | The host-side lib/index.js failed to boot (usually a dependency resolution issue). Check the startup log and confirm node_modules dependencies are complete. |
Usage
- Refresh the browser (F5) — no DSH restart needed.
- Click the「Code Management」entry (sidebar Tab with dsh-better-sidebar, otherwise the header button beside "Session log").
- ① Confirm Git / GitHub CLI are installed → ② generate a key and test connectivity → ③ pick a workspace, then push or create a new repo.
Backend API routes
| Route | Method | Description |
|---|---|---|
/api/source-code-mgmt/env |
GET | Environment check (git/gh versions) |
/api/source-code-mgmt/install-tool |
POST | One-click install of a missing tool (body tool: git/gh/ssh; optional source for gh: official or a mirror id like gh-proxy.com; package manager picked per platform; NDJSON event stream response) |
/api/source-code-mgmt/install-command |
POST | Copyable install command for a tool + download source (body tool + optional source; same logic as Install) |
/api/source-code-mgmt/ssh |
GET | SSH key / config / gh login status |
/api/source-code-mgmt/gh/login |
POST | Open a terminal running gh auth login (interactive — complete it there, then click Re-check) |
/api/source-code-mgmt/gen-key |
POST | Generate an ed25519 key |
/api/source-code-mgmt/write-config |
POST | Write SSH config (body provider: github default / gitee) |
/api/source-code-mgmt/ssh-test |
POST | Test SSH connectivity (body provider: github default / gitee) |
/api/source-code-mgmt/default-dir |
GET | Current workspace folder |
/api/source-code-mgmt/workspaces |
GET | List all workspace folders + custom folder set |
/api/source-code-mgmt/pick-dir |
POST | Native folder picker on the host; returns the chosen path |
/api/source-code-mgmt/add-workspace |
POST | Validate a folder and persist it to the custom list; returns the merged workspace list |
/api/source-code-mgmt/remove-workspace |
POST | Remove a custom-folder dropdown record only (never deletes the folder) |
/api/source-code-mgmt/align |
POST | Hard align: git fetch + git reset --hard origin/<branch> (full reset to remote) |
/api/source-code-mgmt/init-git |
POST | git init + default identity only — no pull/push |
/api/source-code-mgmt/repo-exists |
POST | Check whether a same-name repo exists (body provider: github/gitee) |
/api/source-code-mgmt/repo?dir= |
GET | Repo status (query provider: github/gitee) |
/api/source-code-mgmt/repo-diff?dir=&path= |
GET | Unified diff text for one changed file, on demand |
/api/source-code-mgmt/stage |
POST | Stage changes (body dir, path; empty path = all) |
/api/source-code-mgmt/unstage |
POST | Unstage changes (body dir, path; empty path = all) |
/api/source-code-mgmt/commit |
POST | Commit with a custom message (body dir, message, paths?) |
/api/source-code-mgmt/branches |
POST | List branches (current first) |
/api/source-code-mgmt/checkout |
POST | Switch branch (body dir, branch) |
/api/source-code-mgmt/log |
POST | Recent commit history (body dir, count?; returns hash/subject/author/date) |
/api/source-code-mgmt/revert |
POST | Revert a commit (body dir, hash) |
/api/source-code-mgmt/cherrypick |
POST | Cherry-pick a commit (body dir, hash) |
/api/source-code-mgmt/commit-diff |
POST | Full patch of a commit (body dir, hash) |
/api/source-code-mgmt/push |
POST | Commit and push (git operation, platform-agnostic) |
/api/source-code-mgmt/push-staged |
POST | Commit only staged content with your message, then push |
/api/source-code-mgmt/pull |
POST | git pull --ff-only (up-to-date / success / conflict feedback) |
/api/source-code-mgmt/merge-push |
POST | Pull and push (git pull --rebase + git push) |
/api/source-code-mgmt/force-push |
POST | Force push (git push --force) |
/api/source-code-mgmt/force-pull |
POST | Force pull (git pull --force) |
/api/source-code-mgmt/create |
POST | Create a repo and push (body provider; GitHub via gh repo create, Gitee via OpenAPI + SSH push) |
/api/source-code-mgmt/set-visibility |
POST | Change repo visibility (body provider; GitHub via gh repo edit, Gitee via PATCH /repos/{owner}/{repo}) |
/api/source-code-mgmt/gitee-token |
GET/POST | GET: token configured? + account; POST: save ({token}) or clear ({clear:true}) the Gitee token |
Security
All routes are loopback-only (sec-fetch-site + Origin checks) — only the local machine's browser can call them; LAN/mobile sources get a 403, same policy as the control panel.
Cross-platform
- Windows / Linux / macOS
- Platform detected via
process.platform;~/.sshresolved viahomedir()(Windows:C:\Users\<user>\.ssh, Linux/macOS:/home/<user>/.sshor/Users/<user>/.ssh) - SSH config gets 0600 permissions on Linux/macOS
- Automatic git / gh / ssh / ssh-keygen binary resolution at startup: ① env overrides → ② PATH lookup (
.exeadded on Windows) → ③ (Windows only) Git's bundled dirs (usr\bin/bin), with the bare command name as a last resort. Git alone is enough even whensshisn't on PATH — no per-machine config - Optional explicit binary paths via env vars:
DSH_SCM_GIT/DSH_SCM_GH/DSH_SCM_SSH/DSH_SCM_SSH_KEYGEN - SSH transport fix: Git for Windows' bundled MSYS
ssh.exe(usr\bin\ssh.exe) can fail withcouldn't create signal pipe, Win32 error 5when spawned from a detached/agent process, breakinggit push/git pull. The plugin injectsGIT_SSHpointing at a workingssh(usually the system OpenSSHC:\Windows\System32\OpenSSH\ssh.exe) for git remote operations - Non-ASCII filename compatibility: git's default
core.quotepathprints paths with non-ASCII bytes as octal-escaped quoted strings (which display as garbled text). The plugin injects-c core.quotepath=falseinto every git invocation (raw UTF-8 output) and additionally unescapes any still-quoted paths viaparseGitPath()— Chinese filenames display correctly in the changes list and in diff headers - One-click missing-tool install across platforms (since v1.15.0, adaptive & mostly sudo-free): GitHub CLI installs user-level on Linux/macOS (official binary →
~/.local/bin/gh, no sudo); git/ssh on Linux use apt-get / dnf / pacman (passwordless sudo probed first, manual guidance otherwise); macOS uses brew or the Xcode Command Line Tools; Windows uses winget / built-in features (fallback choco/scoop). The install runs in a live progress dialog, and a one-click DSH restart is offered when the running host needs it. The native folder picker is Windows-only; on macOS/Linux paste the path into the input box instead
Development
git clone https://github.com/Zhucy123/source-code-mgmt.git
cd source-code-mgmt
# test in your local DSH (installs to the web profile, auto-activated)
dsh plugin --profile web add link:$(pwd)
- Changes to
lib/client.js(browser side) → a page refresh is enough - Changes to
lib/index.js(host/Node side) → restart dsh web
Version history
v1.25.0 (current)
Change: the entry now registers into DSH's own right sidebar by default —「Code Management」is no longer a standalone button:
- Request: register the plugin into DSH's sidebar by default instead of keeping the separate top-right button.
- Context (which "sidebar" this means): the crossed-out element in the screenshot is the top-right「Code Management」button; the circled spot is the empty space below the 工作区文件 / 新建终端 / 浏览器 / 壁纸引擎 card list on the right column's start page. Those four cards come from DSH's own right sidebar (ui-sidebar-right) guide (
sidebar.right.pane.tabseat +sidebarRightTabs.register({ guide })) — notsidebar.footer.action(the left sidebar foot), which has been deliberately untouched since v1.1.2. - Implementation:
slots.inject('sidebar.right.pane.tab', …)at activation — the seat existing is the host having its own right sidebar, so whether the callback runs is the capability gate (no version sniffing, one less thing to drift). Inside it:ctx.get('sidebarRightTabs').register({ id: 'source-code-mgmt/sidebar', kind: 'source-code-mgmt', title: () => t('代码管理'), guide: [{ id: 'panel', order: 110, title, description, icon: RepoIcon }] })— that guide entry is the card on the start page;slots.register({ name: 'sidebar.right.pane.tab', key: 'source-code-mgmt/sidebar' }, () => h(ScmPanel, { variant: 'tab' }))— the page the card opens. The key must equal the tab type's id: the host'sTabSlotdispatches the body usingdefinition.idas the entryKey, so a mismatch shows a card that opens an empty pane.- Card order
110: the built-ins are 10 / 20 / 30 and dsh-plugin-wallpaper-engine uses 100, so our card lands last. title/descriptionare functions the host evaluates per render ⇒ language switching needs no re-registration (the older better-sidebar path takes strings, which is why it must re-register on locale changes).
- Where "drop the standalone button" lands: while the seat exists the plugin registers neither header entry — it is not "register then hide". Branch order is ① official right sidebar → ② dsh-better-sidebar → ③ header fallback, and the first to hold owns the entry exclusively.
- Grace window for a late seat:
sidebar.right.pane.tabbelongs to another package and may be declared after us. NewSCM_FALLBACK_GRACE_MS = 600: no fallback button is mounted during that window (an official host would otherwise flash a button before the card replaces it); if the seat appears inside it the button is never mounted. Zero I/O throughout. - Late services: the seat being declared does not mean
sidebarRightTabsis published, so a shortctx.getpoll (250ms × 40 ≈ 10s) runs before giving up and falling back to ③. The tab body registration is wrapped inctx.effectso DSH disposes it on plugin unload (avoidingINACTIVE_EFFECTon an already-disposed fiber). - Older hosts are unaffected: dsh-better-sidebar hosts take ② (logic unchanged); hosts with neither take ③ (the two header entries keep their original geometry and mutual exclusion).
- Tests: this change was covered during development by four targeted regression files —
verify-entry-runtime.test.mjs(32 checks: all three branches, card shape, icon sizing, fluid layout, unload cleanup),verify-sidebar-entry.test.mjs(16 checks against a faithful host model: injection re-runs per declaration epoch, a keyed seat rejects duplicate keys,ctx.effectscopes disposers to their injection, unload unwinds in both LIFO and FIFO order, the throwing-registration path, and a structural assertion that the fallback can only be mounted from inside the router),verify-sidebar-adversarial.test.mjs(16 checks trying to falsify the claim — can a standalone button appear on a sidebar host, can the user be left with zero entries), andverify-sidebar-postfix.test.mjs(3 checks, written by the independent verifier). Those test files were deleted before this release (see "Changed" below); their conclusions are kept here as the record behind the change. - Independent review (the most valuable part of this round): a separate verifier checked every host-contract claim against the real
app.asar(seat name / guide ordering /entryKey = definition.id/ icon size 22|26 /openTab(kind,{replaceTab:true})/ kind-opened pages need nopatterns/keepMountedreally is significant) and found 4 real defects with adversarial cases, all now fixed:- Two entries at once: with the seat declared after us and dsh-better-sidebar installed, the official card left branch-②'s Tab in place — the takeover tore down the header fallback but not ②. Fix: split
betterEntryActive(which entry owns the slot) fromhasBetterSidebar(is it installed — the panel uses it to hide its own git workflow), and have the official success path callteardownBetterTab(). - Plugin completely unreachable: when the host withdrew the seat (its own reload), the cleanup path carried
stoppedByHost=true, which skipped recovery ⇒ zero entries. Fix: seat withdrawal no longer sets that flag and goes through the sharedrouteToFallback();stoppedByHostis reserved for plugin unload and "② already took over". - The retry ladder only spanned 5 seconds: the ticks were applied as absolute delays from apply instead of intervals, so
[500,500,750,1000,1500,2000,3000,5000×7]burned out inside the first 5s (while the comment claimed ~44s). Consequence: a host whose seat exists but whose service is published after 10s stayed on the fallback button forever. Fix: cumulative scheduling (retryAt += interval, last tick ≈44s) and re-attempting the official registration once the service really is available. - No recovery after the service timed out:
officialGaveUphad meant "give up forever". Fix: it now only means "this 40×250ms round ended"; the ladder upgrades to the official card and retires the fallback when the service appears. Plus one wrong gate:installHeaderFallbackusedhasBetterSidebaras its guard, but that flag stays true after the official entry wins ⇒ the fallback could never mount (the same class of unreachability as defect 2).
- Two entries at once: with the seat declared after us and dsh-better-sidebar installed, the official card left branch-②'s Tab in place — the takeover tore down the header fallback but not ②. Fix: split
- Round 2 found 2 more (both fixed):
- A throwing registration added a second entry: when the official registration is refused (the host's
tabs.registerthrows on a duplicate type id), that catch branch calledinstallHeaderFallback()directly, bypassing the "② already owns the entry" gate — so with ② live the screen showed both the Tab and the header button. Fix: that branch now goes throughrouteToFallback(true). (In round 1 I had assumed thebetterEntryActivegate insideinstallHeaderFallbackwas redundant defence — that was wrong: on the throwing path it is the only interceptor.) - Dead state: once
officialGaveUpstopped being permanent, every read of it was removed, leaving writes only ⇒ a variable that misleads the next reader (its comments still described gates that no longer existed). Fix: deleted outright; re-entry is expressed solely byofficialCleanup/officialInstalled.
- A throwing registration added a second entry: when the official registration is refused (the host's
- The exact bound of the bounded window: independent re-verification measured that a service appearing as late as ~54s still upgrades to the official card (50s upgrades, 54s does not) — because the 44.25s final rung starts one more 40×250ms round. That is a deliberate bounded window, not a defect.
- Round-2 mutation testing: the independent verifier ran 10 mutants, 9 killed; the only survivor was the dead state above (hence its removal). I added a dedicated case for the throwing-registration path: mutating either gate alone is masked by the other, but under a double mutation (catch calls the fallback directly + the
betterEntryActivegate removed) the case does fail — so the invariant is covered. - Round-3 verification: the independent verifier confirmed the dead state is gone with no stale comments and no behaviour change (18/18 in both unload orders; 1000 randomized event sequences with max simultaneous carriers = 1 and zero registry violations), and attacked the throwing path from four angles (never zero entries / never a double registration / never a half registration / no leaked timers). It also measured the late-service upgrade edge at exactly 53s yes, 54s no (matching the code comment and this README) and disclosed two bugs in its own harness, re-running every affected conclusion.
- Closing the last coverage gap: the
betterEntryActivegate and "the catch routes" were each individually unobservable (the router intercepts first). Besides keeping the double-mutation case, there is now a structural assertion:installHeaderFallback(may appear exactly once in the file and only insiderouteToFallback— so a future direct call from another path (the shape of the round-2 duplicate-entry bug) fails immediately. Verified: reverting the catch to a direct call alone now fails that check. - Mutation testing: every invariant above was mutation-verified (remove the grace window / move the card order earlier / mismatch the body key with the type id / let better-sidebar win over the official sidebar / drop the ② takeover / block the seat-withdrawal recovery / revert the ladder to absolute delays / stop the retry upgrade / bypass the router on the throwing path → the matching check fails).
- Cleanup after this round: at the user's request every test file was deleted from the plugin (14
*.test.mjs+smoke-test.mjs+verify-en-dict.mjs). The i18n toolchain undertools/is kept (i18n-apply.mjs,i18n-extract.mjs,client-en.json,host-en.json) — those are the scripts and data source that generate the inlined English dictionary inlib/client.js; deleting them would leave the dictionary hand-editable only. The test conclusions and mutation results above are kept as the historical basis for this change. - Changed:
lib/client.js(desktop needs no action — hot reload; the host half is untouched),tools/client-en.json,README.md,README.en.md,package.json, plus the removal of 15 test files undertools/. Version 1.24.4 → 1.25.0 (a feature takes a minor bump).
v1.24.4 (history)
Fix: the white seam in the window titlebar was still there (v1.24.3's band only covered the panel's own width):
- Symptom: after a full restart the seam persisted — the strip was still broken at the panel's left edge.
- Root cause: the band added in v1.24.3 was
panelWwide (same as the panel), but the seam is not confined to the panel's width — the panel width changes as you drag, so a band that follows it always leaves a segment unpainted. - Fix: the band now spans the full window width (
left: 0; right: 0). The left half is the native titlebar already painted the same colour, so repainting it is a no-op; the right half fills the missing part. It no longer depends on the panel width — resizing or toggling the panel can never leave a gap in the strip. ThechromeRef/getChromeElwidth-sync added for the same purpose was removed (no longer needed). - Regression test: the band must be
left: 0; right: 0full width (mutation-verified: reverting it topanelWwidth makes the assertion fail). - Changed:
lib/client.js(desktop needs no action — hot reload),tools/verify-close-button.test.mjs,README.md,README.en.md,package.json. Version 1.24.3 → 1.24.4.
v1.24.3 (history)
v1.24.3 (history)
Fix: a visible colour seam appeared in the window titlebar once the panel opened:
- Symptom: with「代码管理」open, the native titlebar strip at the very top turned white above the panel, while the left half (above the normal content) stayed the usual light grey
#f9fafb— a hard seam at the panel's left edge. - Root cause (a side effect of the push): the desktop shell paints its 40px strip via
.BynINW_frame(and its:before) using--dsw-specific-sidebar-fill(=--dsw-static-neutral-bluish-50,#f9fafb), and that frame lives inside#root. To make room, this plugin narrows#rootbypanelW(#root { margin-right: var(--scm-push); width: calc(100% - var(--scm-push)) }). The strip narrowed along with#root, so it stopped dead at the panel's left edge; nothing painted the band above the panel, exposing the page background (--dsw-alias-bg-layer-1=#ffffff). The panel was not covering the strip — the strip had been cut short by the push. - Fix: the panel portal now paints a band of the same height and width (panel width) filled with the same token,
var(--dsw-specific-sidebar-fill, transparent), so the titlebar runs the full window width with no seam.pointerEvents: "none"— the band only paints; pointer events still pass through to the native drag region, so window dragging is unchanged.- Its height is
SCM_CHROME_TOP(in a plain browser that resolves to0px→ zero height, invisible, exactly as before; native fullscreen also zeroes it, so no stray band appears). - It must resize in step with the panel during a drag (otherwise the strip tears open as you drag):
makeResizeHandlernow also takes agetChromeEland writes both widths per frame.
- Regression test (
tools/verify-close-button.test.mjs, 9 checks; 1 added here): the band must exist in the portal, use--dsw-specific-sidebar-fill, have heightSCM_CHROME_TOP, setpointerEvents: "none", carry aref, and be handed tomakeResizeHandler(mutation-verified: removing the band's background makes the assertion fail). - Changed:
lib/client.js(no action needed on the desktop app — the host stat-polls every client bundle, default 500 ms, and hot-reloads the plugin over SSE as soon as the file changes; the plugin's React state is dropped, sessions are kept. In the web build a page refresh does it),tools/verify-close-button.test.mjs,README.md,README.en.md,package.json. Version 1.24.2 → 1.24.3.
v1.24.2 (history)
Fix: a grey band at the top of the panel covered the description text (a regression introduced in v1.24.1):
- Symptom: with「代码管理」open, a grey strip sat under the「源代码管理」heading and covered / displaced the "按顺序完成:①环境检查 → …" description line.
- Root cause: to keep the
✕on screen, v1.24.1 made the headerposition: stickyand gave it a fill (--dsw-alias-bg-layer-1). A sticky row floats above the content — it does not push what follows, it hovers there, so its fill landed on the text scrolling underneath; its padding also added height and pushed the description down. Using "floating layer + background" to achieve "always visible" was the wrong approach to begin with. - Fix (structural separation, not another colour or z-index tweak):
- For
drawer, the panel root is nowoverflow: hidden(the panel itself does not scroll), the header is a plain static row (flex: 0 0 auto, no background at all), and only the body container below it scrolls (overflow: auto). The header sits in normal flow and cannot overlap content, so the✕is still always visible while being structurally incapable of covering text — the same approach the native sidebar uses. - The
tabvariant still scrolls as one block (its scrolling belongs to the dsh-better-sidebar host) and is unaffected.
- For
- Regression test (
tools/verify-close-button.test.mjs, 8 checks) rewritten accordingly:ScmPanelmust contain noposition: "sticky"(scoped toScmPanel— the plugin's own diff viewer uses sticky legitimately and must not be flagged),headStylemust carry nobackground, the drawer root must beoverflow: hidden, the scrolling body must follow the header, the✕must remain, and thetabvariant must render none. - Changed:
lib/client.js(refresh the page),tools/verify-close-button.test.mjs,README.md,README.en.md,package.json. Version 1.24.1 → 1.24.2.
v1.24.1 (history)
Fix: on the official DSH desktop build the right-side panel opened but could not be closed:
- Symptom: in the desktop app, clicking「代码管理」opened the right-side panel, but there was no clickable close button and no way back out.
- Root cause (the button was rendered — it just could not be clicked): the panel's own
✕was always there (rendered whenvariant === "drawer"), but the desktop shell writes--dsh-windows-titlebar-height: 40pxontodocumentElement, and the window controls (─ □ ✕) are an OS-painted overlay — they always draw above web content, and no z-index can win against them. The panel wasposition: fixed; inset: 0, so its✕landed at y≈20..42, x≈20..50 from the right edge — exactly underneath the native✕. The button was in the DOM and even hovered, but never received the click. The web build has no such OS strip, which is why this stayed hidden. - Fix (four parts, all covered by regression tests):
- Start the panel below the strip: the overlay changed from
inset: 0totop: var(--dsh-frame-chrome-top, var(--dsh-windows-titlebar-height, 0px))plusright/bottom/left: 0. It prefers DSH's own--dsh-frame-chrome-top— the very variable DSH publishes so modal masks stay off the Windows caption — which resolves to 0 in native fullscreen, so the panel still uses the full height there. Shells that only publish the older variable fall back to--dsh-windows-titlebar-height; a plain browser resolves the whole chain to0px, pixel-identical to before. - Keep the
✕on screen: the panel is long (①②③④⑤), and ordinary scrolling carried the title row — and the✕with it — out of view, forcing a scroll back to the top to close. Note: this version first implemented it withposition: stickyplus a fill, which was the wrong direction (sticky floats above the content and its fill covered the text scrolling underneath) — corrected in v1.24.2 by separating the structure: a fixed header with only the body scrolling. Thetabvariant is unchanged: the dsh-better-sidebar host owns closing there and no✕is rendered. - Escape as a fallback: while the panel is open a
keydownlistener closes it on Esc (removed on close/unmount). In the desktop shell any edge-pinned web control can be covered by the OS overlay, and the keyboard is always available. - The opener doubles as a toggle: the「代码管理」button changed from a one-way
setOpen(true)totoggleOpen(click again to collapse, witharia-expanded). It sits in the header rather than in the strip the OS controls cover, making it the most reliable close affordance of all.
- Start the panel below the strip: the overlay changed from
- New regression test
tools/verify-close-button.test.mjs(7 checks, rewritten to 8 in v1.24.2): theSCM_CHROME_TOPvariable chain and fallback order; the overlay must usetop: SCM_CHROME_TOP; noinset: 0may return to thedrawer(scoped toHeaderScmAction— the plugin's own centred modals legitimately use a full-viewport mask and must not be flagged); the header was sticky and the✕lived inside the scroll container; thetabvariant renders no✕; the resize handle's z-index had to exceed the then-sticky header's (a sticky header covered the handle's inner 4px, leaving a strip that could not be dragged — the handle moved fromzIndex: 1to3, and the assertion was mutation-verified to fail without it); the Esc listener is both attached and cleaned up; and the opener must toggle rather than only open. - Changed:
lib/client.js(refresh the page to apply, no restart needed),tools/verify-close-button.test.mjs(new),README.md,README.en.md,package.json. Version 1.24.0 → 1.24.1.
v1.24.0 (history)
③ faster loading: batch the git calls (measured ~1.2s saved per load):
- Why a system proxy does not help: push/pull/fetch go over SSH (
git@github.com:...), and OpenSSH ignoresHTTP_PROXY/HTTPS_PROXY/git http.proxy— verified withssh -G. Those variables only affect git-over-HTTPS. So for an SSH remote, a system proxy cannot change the speed; that is a protocol fact, not a misconfiguration. Routing SSH through a proxy requires an explicitProxyCommand(needs a helper likencat/connect, absent here). - Measured breakdown (real repo on this machine): local git calls ~330 ms; of that, one
git diff --numstatper changed file ≈ 1258 ms (30 changes → 30 processes);git fetch≈ 4000 ms (network,fullmode only); SSH handshake alone ≈ 3200 ms. Root cause: agitprocess costs ~35 ms on Windows, and the old code spawned one per file — a classic N+1. - Fix: new
viewableMap()batches the binary check — a direct 8KB header read (no process) for files that exist, and at most onegit diff --numstatfor the rest (split into staged/unstaged batches). NewproviderRemoteInfo()uses onegit remote -vto get both the remote name and URL, replacinggit remote+ aget-urlper remote plus anotherget-urlinrepoStatus. Measured: 1258 ms → 58 ms, ~1.24 s saved per load. - Semantics preserved (test-enforced): new
tools/verify-viewable-batch.test.mjs(9 checks) runs the OLD and NEW implementations against a real scratch repo and asserts they agree on every file. It caught a regression I introduced:git diff --numstatprints nothing for a deleted file, so the old logic correctly called it "not previewable" (there is no working copy); the first batched version wrongly said "previewable". Fixed to match. - Bottleneck profiling and optimization verification were done with one-off timing scripts run against a real repo (the source of the numbers above); they carry no assertions and are not part of the regression suite, so they were deleted after use rather than kept in the repo.
- Takeaway: local loading is now ~0.1s and feels instant; the ~4s is network + SSH handshake and a proxy cannot help. The plugin already skips the network fetch in fast mode (using the local tracking ref for ahead/behind), only fetching when you click "Refresh status".
- Also cleaned up: removed 4 assertion-free diagnostic scripts (
scan-keys/scan-host-keys/ two timing scripts) and 1 pre-existing test that cannot run on this machine (verify-cn-paths.test.mjs, which hardcodesC:\Users\Administrator\workspace). Every assertingverify-*.test.mjsand the i18n workflow are kept. - Scope:
lib/index.js(restart dsh web),README.md,README.en.md,package.json. Version 1.23.0 → 1.24.0.
v1.23.0 (historical)
SSH port is now user-selectable (443 through firewalls / 22 standard):
- Request: "if I have a proxy, do I even need 443? let the user choose." Implemented as a user choice.
- Correcting a common premise (verified): SSH does not use
HTTP_PROXY/HTTPS_PROXY—ssh -Gproduces no proxy directive even with them set. So "I have a proxy" does not mean "port 22 works"; 443 exists to get around networks that block port 22. Hence the choice belongs to the user, keyed off their network — not guessed from the presence of a proxy. - Host:
providerCfg(provider, port)picks theHostname(443 → GitHub'sssh.github.com, 22 → standardgithub.com; Gitee uses its own host either way). NewnormalizePort()accepts only 22/443 (anything else falls back to 443).writeSshConfig(provider, port)rewrites the matchingHostblock in place when the port changes (located via the newhostBlockSpan()), producing no duplicate blocks and leaving otherHostentries alone; an unchanged port stays an idempotent no-op.sshTest(provider, port)passes-p <port>explicitly.checkSsh()reportssshGitHubPort/sshGiteePort. - Client: ② gains a port dropdown (443 / 22); the status line shows the actual configured port (no longer a hardcoded
(443)); the selector foll
…
Links
More in this category
zhu1090093659/dsh-web#packages/dsh-git-graph★ 8707
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★ 136
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★ 34
Adds a Worktree view to DSH Web UI that groups Sessions by Git worktree while keeping DSH as the source of truth.
lehhair/dsh-diff-viewer★ 26
PiUI-style diff viewer replacing the stock DiffBlock for write/edit tool calls.
DamonKoy/dsh-web-ui#dsh-git-graph★ 24
Git branch selector and Git graph in the conversation header of the dsh web GUI.
PerryLink/dsh-github★ 22
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.