DeepSeek Harness Plugin

Zhucy123/source-code-mgmt

Stars ★ 0 Category Git & Code Review Added 2026-08-30

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.tab existing 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).
  • 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.

The seat may arrive late (600ms grace window): sidebar.right.pane.tab belongs 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 underlying win32 / darwin / linux platform 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.com downloads 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.gz is unpacked to ~/.local/bin; on Windows the mirrored official .msi is downloaded and silently installed via msiexec /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.
  • 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/*.pub for existing ed25519 public keys — prefers id_ed25519, otherwise the first found (any name, e.g. github_ed25519); falls back to id_ed25519 when none exist. The status line shows the actual key filename, and the SSH config IdentityFile uses 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.com port 443) — see Do I need the 443 config? below
  • Test connection ssh -T git@github.com (GitHub) or ssh -T git@gitee.com (Gitee)
  • Shows the public key content for easy copy-upload to the platform
  • Detects whether gh is 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 running gh 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 gh CLI, Gitee via Gitee OpenAPI)
  • Gitee token (Gitee mode only): enter a Gitee personal access token (needs projects permission) → stored at ~/.dsh/storages/source-code-mgmt-gitee.json on 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 status XY 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)
  • 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 = gh account, 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 init only (+ 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/repos to create, then sets the SSH remote git@gitee.com:<owner>/<name>.git and git push (through ②'s SSH key)

④ Clone repos

  • Follows ②'s platform: lists every remote repo of the signed-in account (GitHub via gh repo list, Gitee via OpenAPI user/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 config npm config list (auto-redacts token/auth/password) → ③ verify identity npm whoami → ④ preview packed files npm pack --dry-run (see exactly what would be published, nothing is packed or uploaded) → ⑤ publish npm 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 publish all 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 an error listener to every child, and uses konsole --separate on KDE — fixing the old bug where spawning a missing binary (e.g. no x-terminal-emulator on SteamOS) raised an uncaught async ENOENT that crashed the dsh host process (which looked like your pnpm dsh web terminal 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.com on 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.json declares dsh.bundle (carrying a cordis.patch.yml config layer), so dsh plugin --profile web add installs 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 add in the web profile directory, then reconciles the plugin layer: because this package declares dsh.bundle, it is automatically appended to dsh.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 ... (unlike link:, which is a symlink).

Verifying the install

After installing and refreshing the page:

  1. Dependency written: source-code-mgmt is in dependencies of ~/.dsh/profiles/web/package.json.
  2. Added to the config layer: source-code-mgmt is in the dsh.profile.bundles list of the same file (written automatically by dsh plugin add — no manual editing).
  3. Symlink created (link: only): ~/.dsh/profiles/web/node_modules/source-code-mgmt points at your source dir (a Junction on Windows).
  4. 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

  1. Refresh the browser (F5) — no DSH restart needed.
  2. Click the「Code Management」entry (sidebar Tab with dsh-better-sidebar, otherwise the header button beside "Session log").
  3. ① 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; ~/.ssh resolved via homedir() (Windows: C:\Users\<user>\.ssh, Linux/macOS: /home/<user>/.ssh or /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 (.exe added 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 when ssh isn'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 with couldn't create signal pipe, Win32 error 5 when spawned from a detached/agent process, breaking git push/git pull. The plugin injects GIT_SSH pointing at a working ssh (usually the system OpenSSH C:\Windows\System32\OpenSSH\ssh.exe) for git remote operations
  • Non-ASCII filename compatibility: git's default core.quotepath prints paths with non-ASCII bytes as octal-escaped quoted strings (which display as garbled text). The plugin injects -c core.quotepath=false into every git invocation (raw UTF-8 output) and additionally unescapes any still-quoted paths via parseGitPath() — 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.tab seat + sidebarRightTabs.register({ guide })) — not sidebar.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's TabSlot dispatches the body using definition.id as 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 / description are 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.tab belongs to another package and may be declared after us. New SCM_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 sidebarRightTabs is published, so a short ctx.get poll (250ms × 40 ≈ 10s) runs before giving up and falling back to ③. The tab body registration is wrapped in ctx.effect so DSH disposes it on plugin unload (avoiding INACTIVE_EFFECT on 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.effect scopes 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), and verify-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 no patterns / keepMounted really is significant) and found 4 real defects with adversarial cases, all now fixed:
    1. 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) from hasBetterSidebar (is it installed — the panel uses it to hide its own git workflow), and have the official success path call teardownBetterTab().
    2. 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 shared routeToFallback(); stoppedByHost is reserved for plugin unload and "② already took over".
    3. 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.
    4. No recovery after the service timed out: officialGaveUp had 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: installHeaderFallback used hasBetterSidebar as 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).
  • Round 2 found 2 more (both fixed):
    1. A throwing registration added a second entry: when the official registration is refused (the host's tabs.register throws on a duplicate type id), that catch branch called installHeaderFallback() 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 through routeToFallback(true). (In round 1 I had assumed the betterEntryActive gate inside installHeaderFallback was redundant defence — that was wrong: on the throwing path it is the only interceptor.)
    2. Dead state: once officialGaveUp stopped 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 by officialCleanup/officialInstalled.
  • 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 betterEntryActive gate 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 betterEntryActive gate 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 inside routeToFallback — 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 under tools/ 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 in lib/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 under tools/. 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 panelW wide (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. The chromeRef/getChromeEl width-sync added for the same purpose was removed (no longer needed).
  • Regression test: the band must be left: 0; right: 0 full width (mutation-verified: reverting it to panelW width 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 #root by panelW (#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 to 0px → 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): makeResizeHandler now also takes a getChromeEl and 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 height SCM_CHROME_TOP, set pointerEvents: "none", carry a ref, and be handed to makeResizeHandler (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 header position: sticky and 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 now overflow: 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 tab variant still scrolls as one block (its scrolling belongs to the dsh-better-sidebar host) and is unaffected.
  • Regression test (tools/verify-close-button.test.mjs, 8 checks) rewritten accordingly: ScmPanel must contain no position: "sticky" (scoped to ScmPanel — the plugin's own diff viewer uses sticky legitimately and must not be flagged), headStyle must carry no background, the drawer root must be overflow: hidden, the scrolling body must follow the header, the ✕ must remain, and the tab variant 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 when variant === "drawer"), but the desktop shell writes --dsh-windows-titlebar-height: 40px onto documentElement, 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 was position: 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):
    1. Start the panel below the strip: the overlay changed from inset: 0 to top: var(--dsh-frame-chrome-top, var(--dsh-windows-titlebar-height, 0px)) plus right/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 to 0px, pixel-identical to before.
    2. 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 with position: sticky plus 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. The tab variant is unchanged: the dsh-better-sidebar host owns closing there and no ✕ is rendered.
    3. Escape as a fallback: while the panel is open a keydown listener 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.
    4. The opener doubles as a toggle: the「代码管理」button changed from a one-way setOpen(true) to toggleOpen (click again to collapse, with aria-expanded). It sits in the header rather than in the strip the OS controls cover, making it the most reliable close affordance of all.
  • New regression test tools/verify-close-button.test.mjs (7 checks, rewritten to 8 in v1.24.2): the SCM_CHROME_TOP variable chain and fallback order; the overlay must use top: SCM_CHROME_TOP; no inset: 0 may return to the drawer (scoped to HeaderScmAction — 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; the tab variant 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 from zIndex: 1 to 3, 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 ignores HTTP_PROXY / HTTPS_PROXY / git http.proxy — verified with ssh -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 explicit ProxyCommand (needs a helper like ncat/connect, absent here).
  • Measured breakdown (real repo on this machine): local git calls ~330 ms; of that, one git diff --numstat per changed file ≈ 1258 ms (30 changes → 30 processes); git fetch ≈ 4000 ms (network, full mode only); SSH handshake alone ≈ 3200 ms. Root cause: a git process 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 one git diff --numstat for the rest (split into staged/unstaged batches). New providerRemoteInfo() uses one git remote -v to get both the remote name and URL, replacing git remote + a get-url per remote plus another get-url in repoStatus. 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 --numstat prints 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 hardcodes C:\Users\Administrator\workspace). Every asserting verify-*.test.mjs and 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 -G produces 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 the Hostname (443 → GitHub's ssh.github.com, 22 → standard github.com; Gitee uses its own host either way). New normalizePort() accepts only 22/443 (anything else falls back to 443). writeSshConfig(provider, port) rewrites the matching Host block in place when the port changes (located via the new hostBlockSpan()), producing no duplicate blocks and leaving other Host entries alone; an unchanged port stays an idempotent no-op. sshTest(provider, port) passes -p <port> explicitly. checkSsh() reports sshGitHubPort / sshGiteePort.
  • Client: ② gains a port dropdown (443 / 22); the status line shows the actual configured port (no longer a hardcoded (443)); the selector foll

…

Content from the project README on GitHub ↗

Links

More in this category

View the whole category →

Community comments

Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.