DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wplct /

wplct/dsh-codex-style-worktree

Verified

Codex 风格 Worktree:会话默认在独立 git worktree 中工作,输入框上可选择本地或工作树,清理有保护、删除前留可恢复快照。

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@c4aa6d6c

Codex-style Worktree (dsh-codex-style-worktree)

English | 简体中文

A "work like Codex" placement choice for DSH sessions: on the new-session hero row, pick Local or New local worktree (the default) next to the workspace chip; after you send the first message the plugin creates a detached checkout and switches that session's cwd into it. No new Workspace, no naming, no branch from the plugin (Git owns branch naming at commit time).

  • Version: 0.5.1 (2026-10-20; package / plugin id / loader row name: dsh-codex-style-worktree; profile-patch insert row id: codex-worktree)
  • API namespace: /api/dsh-codex-style-worktree, RPC method prefix dsh-codex-style-worktree/; sidecar: $DSH_HOME/dsh-codex-style-worktree/state.json; disable marker: .dsh-codex-style-worktree-off
  • GitHub: https://github.com/wplct/dsh-codex-style-worktree
  • Requirements: DSH 0.2.0-rc.2 / 0.2.1-rc.2, Node.js >= 22.19.0, git on PATH
  • License: MIT

0.5.1: the checkout is created on the first message

  • Bug fixed: opening a blank Session no longer creates a checkout. The desktop fires agent/created the moment the New Session appears — long before the first prompt — so the old wiring decided the work location at Session-open time; a choice made in that blank Session therefore applied only to the next Session (switching to Local still created a checkout, switching back to New local worktree created none).
  • Creation now runs on the first step of the Session's first turn (agent/pre-step with turn === 1) — i.e. after you send the first message — and the per-workspace placement is read at that moment, so the choice made in the blank Session applies to that Session.
  • Blank Sessions no longer leave stray checkouts behind; the progress card and sidebar marker appear after the first message instead of when the blank Session opens.
  • Nothing else changes; there is no config, sidecar or settings migration. Upgrade with dsh plugin --profile desktop update dsh-codex-style-worktree and reopen DSH once.

0.5.0: rename-only release

  • Package name / plugin id / loader row name / API namespace / RPC prefix / sidecar directory all move from dsh-codex-worktree to dsh-codex-style-worktree; the display title "Codex-style Worktree" is unchanged.
  • The settings namespace stays codex-worktree (the profile-patch insert row id is untouched), so saved settings survive; every editable field is unchanged.
  • On first boot the sidecar directory migrates automatically from ~/.dsh/dsh-codex-worktree to ~/.dsh/dsh-codex-style-worktree (rename, no data loss; if the rename fails the old directory keeps being read).
  • The repository opt-out marker is now .dsh-codex-style-worktree-off; the old .dsh-codex-worktree-off is still honored.
  • Upgrade path: remove the old package, install the new one (dsh plugin --profile desktop remove dsh-codex-worktree, then dsh plugin --profile desktop add github:wplct/dsh-codex-style-worktree), then fully quit and reopen DSH once.

What changed in 0.4.0 (vs 0.3.0)

Capability 0.3.0 0.4.0
Cleanup guard Cleanup deletes directly Refused when the checkout is another active session's current cwd (worktree-in-use); cleaning your own session's checkout first moves its cwd back to the workspace; force overrides after confirmation
Snapshot before delete Only autoPrune kept a ref for detached commits Every delete path: when the checkout is detached and HEAD ≠ baseCommit, write refs/dsh-codex-style-worktree/<timestamp+session suffix> and record it in the sidecar
Snapshot restore Manual git branch only /wt-snapshots, /wt-restore <ref>, and a "recoverable snapshots" list in the settings page
Archive cleanup Checkouts always kept after a session ends Optional cleanupOnArchive (default off): a lazy sweep removes clean, unused checkouts after archive
RPC 6 endpoints 7 (adds restore); the list response gains per-repository snapshots
Sidecar v2 v3 (adds a snapshots map; v1/v2 are migrated in memory on read, no data loss)

What changed in 0.3.0 (vs 0.2.x) — history

Capability 0.2.x 0.3.0
When the worktree is created Automatically for every new session (no choice); a "New Worktree" button could also create one on demand You choose a placement on the hero row; the worktree is created after the first message (first-turn agent/pre-step)
Selector location A dock button above the composer The hero workspace row: a pill between 📁 workspace ▼ and standard mode ▼
Remembered choice None Per workspace, persisted in the sidecar (placements), restart-safe
Workspace The old button called workspaces.create, cluttering the sidebar Never creates a Workspace; only the session cwd changes
Progress feedback None Inline three-step progress card above the composer (projection-driven, minimum 1 s)
Sidebar No marker Managed sessions show a read-only worktree icon (tooltip = checkout path)
Settings page Read-only config Editable form; save writes the active profile patch and applies live (volatile, no restart)
State sync Some RPC reads Everything is event/projection-driven, zero polling
RPC Included create, etc. status/list/clean/prune/placement/settings, no create/cancel

1. What it does

  1. On the new-session hero row (the blank state without a session id) the workspace row shows a placement pill: New local worktree (default) or Local;
  2. the choice is written back to the host immediately and persisted per workspace path in the sidecar; later sessions in that workspace inherit it, across restarts;
  3. you send the first message → the Session's first turn starts with agent/pre-step (before the model's first step) → the host decides by that workspace's placement whether to create a checkout;
  4. with "New local worktree": a detached HEAD checkout is created at <repo>/.agents/worktrees/wt-<timestamp> and this session's working directory is switched to it; the session stays attached to its original Workspace;
  5. creation progress is pushed through the codexWorktree session projection and rendered as an inline progress card above the composer (Prepare workspace → Check out files → Set up environment); when done the card fades and the header chip plus the sidebar marker appear;
  6. with "Local": nothing is created; the session runs in its original Workspace directory;
  7. the plugin never creates branches, never names anything, never merges, never pushes and never deletes your branches. Git decides the branch name at commit time; use plain git switch -c when you want a branch. mode: branch makes the plugin create a <branchPrefix><name> branch up front (not the default).

2. Interaction

2.1 New-session hero row: placement selector

Position: the official conversation.hero.options seat — the compact options area of the New Session row, right-aligned after the workspace and preset controls, rendered only once the blank new Session exists. It sits on the same row as 📁 workspace ▼ and standard mode ▼:

📁 shian360 ▼      standard mode ▼        ⑂ New local worktree ▼
┌────────────────────────────────────────────────────┐
│ Describe what you want to build, / for commands…    │
└────────────────────────────────────────────────────┘
  • Options (single select, current one ticked):
    • Local / 本地 — run directly in the current workspace directory;
    • New local worktree / 新建本地工作树 — run in a new checkout (default);
    • Grey hint at the bottom of the menu: Creating a local worktree does not add a Workspace; it only switches this Session’s working directory.
  • Visibility: only on the blank new-session hero row (this seat is session-scoped, so it renders once the blank Session exists); hidden for non-Git workspaces (isGit=false from the host) and for existing sessions.
  • Seat choice: this is the official list seat, not the shipped WorkspacePicker's conversation.hero.workspace (that seat is kind: "single" and owned by the workspace picker; registering there would shadow the shipped workspace switcher). The plugin registers under its own id in conversation.hero.options and never shadows shipped UI.
  • Persistence: the choice is written back immediately through the placement RPC and stored per workspace path in the sidecar. The control updates optimistically and rolls back with a message if the write fails; if reading fails it degrades to a read-only display of the default and never blocks sending.
  • Relation to config: the selector reads/writes the per-workspace placement, not profile config; defaultPlacement only decides the value for a workspace that has no stored placement yet. First open shows placements[path] ?? defaultPlacement.

2.2 Creation after the first message

  1. You send the first message;
  2. the Session's first turn begins and the host's agent/pre-step listener runs before the model's first step (a blank Session on its own creates nothing);
  3. the host re-reads that workspace's placement: local → do nothing; worktree → run Prepare workspace (create the pool directory + self-ignoring .gitignore) → Check out files (git worktree add --detach) → Set up environment (canonicalize the path, workingDirectory.set, write the sidecar record);
  4. every phase is pushed through the session projection, and the client renders an inline progress card above the composer:
┌────────────────────────────────────────────────────┐
│ ⑂ Creating worktree                                │
│   ✓ Prepare workspace                              │
│   ◐ Check out files                                │
│   ○ Set up environment                             │
│                                             Cancel │
└────────────────────────────────────────────────────┘
  • The card is shown for at least 1000 ms (creation is usually < 0.5 s); on completion all three steps are ticked and Worktree created fades out after ~400 ms.
  • Only "Cancel" is offered: it hides the card immediately while creation continues; the plugin does not roll back and offers no "don't use a worktree" remedy.
  • Failure: the card turns into an error state with the reason, and the session continues in its original directory without blocking the conversation. Error codes:
Error code Meaning
not-git-repository The workspace is not a Git repository
no-commit The repository has no commit yet
disabled enabled: false, or .dsh-codex-style-worktree-off exists at the repository root
root-unreachable The configured root is not writable, or the session sandbox forbids that path
limit-reached The repository reached maxPerRepo (clean up old checkouts first)
other message git error / timeout etc., shown verbatim

2.3 Session ownership and directory

  • The session always stays with its original Workspace; the only side effect is workingDirectory.set for the current session.
  • The plugin never calls workspaces.create / workspaces.rename / sessions.create and never adds a Workspace; acceptance: the number of workspaces in workspace.json is unchanged before and after.
  • Default mode: detached: the plugin creates no branch, the checkout stays on a detached HEAD; Git decides the branch name at commit/push time (use git switch -c when you want a named branch). mode: branch switches to creating <branchPrefix><name> up front.

2.4 Sidebar marker (read-only)

  • Slot: sidebar.session.row.leading (the official ambient-marker seat: a persistent leading icon area of the session row; the component reads sessionId). This is a deliberate design decision: the row's action strip is a hover-only button strip and is unsuitable for a permanent marker; the seat does not belong to the shipped workspace picker and never shadows shipped UI.
  • Managed sessions show a 14 px worktree icon (⑂-style SVG, currentColor, muted), with the tooltip Codex-style Worktree · <checkout path>.
  • Not clickable: clicks fall through to the row (keeping "open session"); no hover card and no navigation.
  • Data source: the host registers the codexWorktree session projection; the client reads sessions.list.getSnapshot().byId[sessionId].projectionValues?.codexWorktree and renders the icon only when phase === 'ready', hiding it for null/missing. Zero RPC, zero polling.
  • Creation, cleanup and pruning changes appear immediately via projection pushes.

Known limitation: when you clean a non-live session's checkout from the settings page, the host cannot append a projection event for that session, so its sidebar marker disappears only on that session's next consistency repair (reopening the session or its first step).

2.5 Session header chip

  • Shows Codex-style Worktree plus status (managed / not managed / error).
  • Popover: path, repository, base commit, mode, change count (dirty), cleanup action.
  • The basic state comes from the projection first; when you open the popover it makes one status call for details such as the dirty count (on demand, not polling).
  • No naming, and no "don't use a worktree" rollback.

2.6 Settings page

One Codex-style Worktree section with two blocks:

  1. Configuration (editable form)
    • Save → goes through the host settings service, writes the active profile's cordis.patch.yml, and takes effect immediately (volatile config: no restart, no re-mount);
    • the official DSH "Plugins" settings page also gets an auto-generated form via configure({ auto: true }); both share one revision and conflicts are surfaced;
    • "Restore defaults" writes the 21 user-visible fields back to their schema defaults;
    • conflict (stale revision) → "The configuration was changed elsewhere" and the form refreshes automatically;
    • invalid value → an error is returned, the previous value is kept and the constraint is shown;
    • settings service unavailable (headless / non-profile composition) → degrade to read-only and keep the original hint.
  2. Worktree management (the existing UI): checkouts grouped by repository (path / mode / dirty / created), a per-repository "recoverable snapshots" list (ref / commit / created / restored marker plus a Restore button), refresh, per-row cleanup (in-use or dirty checkouts need confirmation/force) and prune now.

3. Install and upgrade

3.1 Install

Prerequisites: DSH 0.2.0-rc.2 / 0.2.1-rc.2, Node.js >= 22.19.0, git; finish or stop running tasks/sessions first.

dsh plugin --profile desktop add github:wplct/dsh-codex-style-worktree

Then quit and reopen DSH once (the bundle layer takes effect; this is not a page refresh). Afterwards configuration changes need no restart (volatile, live).

3.2 Upgrading from older versions

0.5.0 → 0.5.1 (current):

  • bug fix only: the checkout is created on the first message instead of when a blank Session opens, so the placement choice applies to the Session it was made in;
  • no migration: run dsh plugin --profile desktop update dsh-codex-style-worktree, then quit and reopen DSH once.

0.4.x → 0.5.0 (rename-only):

  • rename only, no feature changes (the 0.4.0 cleanup guard, restorable snapshots and cleanupOnArchive all stay);
  • follow the "0.5.0: rename-only release" section: remove + add + restart; settings values, sidecar records and snapshot refs are preserved (the ref prefix follows the new name as refs/dsh-codex-style-worktree/; refs written by the old version can still be pointed at with git update-ref).

0.3.x → 0.4.0 (history):

  • no breaking UI changes: the placement selector, creation-after-send, three-step progress card, read-only sidebar marker, chip and projections all stay; existing placement/config records keep working;
  • the sidecar state.json is migrated in memory on read from v2 (or older v1) to v3 (adds snapshots; older versions also get placements) with no data loss, and the next write persists v3;
  • the settings page gains cleanupOnArchive (default false); leaving it alone keeps the 0.3.0 "do not delete on session end" behavior;
  • new cleanup guard: cleaning a checkout used by another live session returns worktree-in-use (cleaning your own session's checkout first moves its cwd back to the workspace; override with force after confirming); new /wt-snapshots, /wt-restore <ref> and the settings-page "recoverable snapshots" list;
  • DSH still needs one restart after upgrading (the bundle layer takes effect); configuration changes continue to apply without a restart.

0.2.x → 0.3.0 (history):

  • The old "New Worktree" button is gone; creation only happens after the first message. The create / cancel endpoints and the pending/adoption logic were removed;
  • the sidecar state.json migrates automatically from v1 to v2 (keeps records, adds an empty placements map, bumps the version); old v1 records keep showing in the chip and settings page;
  • one-time manual cleanup (after confirming): workspaces the old button created (if the sidebar has extras), the corresponding .agents/worktrees/wt-* checkouts and leftover pending records;
  • verify the profile has no leftover auto-worktree rows in its dependencies / bundles / cordis.patch.yml (0.2.2 already cleaned those) and that the market's disabled list has no old package name;
  • restart DSH once after upgrading so both the new bundle and the client artifact are refreshed; the one-time cleanup done when upgrading to 0.3.x does not need to be repeated.

3.3 Direct install (optional local/offline path)

The repository also ships a direct-install script that bypasses the package manager, for local development or offline use:

node install.mjs --dry-run   # print the plan only, write nothing
node install.mjs             # install into the default profile (desktop)
node install.mjs --profile web
DSH_HOME=/path/to/.dsh node install.mjs --profile desktop

What the script does:

  1. validates the source package (package.json, cordis.patch.yml, lib/**, client/** must all be there; a missing half aborts with an absolute path in the error);
  2. copies package.json, cordis.patch.yml, README*.md, lib/** and client/** (recursively) into <profile>/node_modules/dsh-codex-style-worktree/ — staged first in a temporary directory on the same volume, never overwriting the live directory in place;
  3. runs node --check on every .js file in the staged copy and publishes only if all of them pass; a failure rolls back;
  4. backs up <profile>/cordis.patch.yml to .bak-<ISO timestamp> and appends the marked patch block (the package's cordis.patch.yml minus # comment lines; that file currently holds only the config-less insert row, defaults come from lib/config.js). If the file already contains # >>> dsh-codex-style-worktree >>>, the append is skipped;
  5. prints target path, file count, backup paths and next steps.

The direct installer explicitly does not: modify the profile's package.json, run pnpm/npm, or restart DSH.

4. Uninstall and rollback

Uninstall with the symmetric DSH plugin command, then restart DSH:

dsh plugin --profile desktop remove dsh-codex-style-worktree

If the package was installed with the direct-install script from section 3.3, use uninstall.mjs instead:

node uninstall.mjs --dry-run   # print the plan only
node uninstall.mjs             # default profile
node uninstall.mjs --profile web

uninstall.mjs removes the marked block (markers and everything between them, other lines preserved, file backed up to .bak-<ISO timestamp> first) from <profile>/cordis.patch.yml, deletes <profile>/node_modules/dsh-codex-style-worktree/ recursively, and prints every action and backup path; it is idempotent and --dry-run writes nothing. It also reports (without deleting) install snapshots .dsh-codex-style-worktree.previous-* and the sidecar ledger.

Goal Action
Full uninstall dsh plugin --profile desktop remove dsh-codex-style-worktree (direct install: node uninstall.mjs), then restart DSH
Revert the profile patch only cp <profile>/cordis.patch.yml.bak-<timestamp> <profile>/cordis.patch.yml, delete node_modules/dsh-codex-style-worktree/ manually, restart
Roll back to the previous package rename the printed .dsh-codex-style-worktree.previous-<timestamp> directory back to node_modules/dsh-codex-style-worktree, restart

Uninstall never deletes your worktrees or branches: check /wt-status, clean up with /wt-clean, or run git worktree remove <path> / git branch -d <branch> yourself. Deleting the sidecar ledger ($DSH_HOME/dsh-codex-style-worktree/) also clears the remembered placements.

5. Configuration

5.1 Where configuration lives

  • Defaults and validation: lib/config.js (resolveConfig is the single source of truth; EDITABLE_FIELDS defines the settings form fields and their order);
  • package cordis.patch.yml: a plain insert row (id: codex-worktree, name: dsh-codex-style-worktree) with no config, so the market can hot-mount it;
  • runtime override: the - id: codex-worktree config row in the active profile's cordis.patch.yml (the settings page maintains it automatically);
  • every user-visible field is volatile: saving applies immediately, no restart or re-mount required.

5.2 User-visible fields (editable in settings, 21)

Field Type Default Control Constraints / notes
enabled boolean true switch Master switch; when off, new sessions get no worktree (existing checkouts are untouched)
defaultPlacement worktree|local worktree select Placement for a workspace with no stored record yet
directory string .agents/worktrees text Pool directory: relative, no ./.. segments
root string "" text Empty = inside the repository; may be absolute or repository-relative, no ..
mode detached|branch detached select detached creates no branch (default); branch creates <branchPrefix><name> up front
namePrefix string wt- text Checkout name prefix; no / \, must not start with -
branchPrefix string wt/ text Branch prefix for mode: branch; must not start with - or /
baseRef string HEAD text Local base revision; must not start with -; resolved locally (network only via fetchBeforeCreate)
fetchBeforeCreate boolean false switch Run git fetch --prune origin before creation; failures only warn and never block; skipped without origin
autoPrune boolean true switch After each successful creation, prune the repository's oldest checkouts in the background (keep the newest autoPruneKeep)
autoPruneKeep number 15 number Newest checkouts kept per repository; non-negative integer
pruneGraceMs number 60000 number Checkouts younger than this are never auto-pruned (milliseconds); non-negative integer
cleanupOnArchive boolean false switch After a session is archived, a lazy sweep deletes its clean (no uncommitted changes) and unused worktree; off by default to keep the Codex-style "do not delete on session end" behavior
poolGitignore boolean true switch Write a self-ignoring .gitignore when the pool is created, keeping the main checkout's git status clean
gitTimeoutMs number 20000 number Hard timeout for one git command (milliseconds); positive integer
skipChildAgents boolean true switch Never create a worktree for subagents/child sessions
skipSessionIdPatterns string[] ["^session-taskboard-"] multiline text One regular expression per line; matches are skipped (taskboard sessions by default)
skipPresets string[] [] multiline text One regular expression per line; matching agent presets are skipped
disableMarker string .dsh-codex-style-worktree-off text A file with this name at the repo root or in the session cwd disables creation; plain file name, no / \
maxPerRepo number 32 number Upper bound of plugin-managed checkouts per repository; positive integer; hitting it reports limit-reached
noticeOnCreate boolean false switch Queue one extra user-context message describing the new checkout

Hidden internals (not in the form):

Field Default Notes
gitCommand git Internal: git executable name/path
cleanupOnDispose false Reserved: delete the checkout on session dispose (currently always kept)

5.3 root semantics and the safety boundary

The DSH workspace-write file sandbox is bounded by the session's original workspace directory (header.cwd), not the whole disk.

  • root: "" (default): the pool is <repo>/<directory>, so checkouts always live inside the repository (that is, inside the session workspace) → workspace-write can read and write them. Safest;
  • non-empty root: checkouts are created under that root (absolute, or repository-relative). If it lies outside the session workspace, writes from a workspace-write session would be rejected by the sandbox; creation fails with root-unreachable in the progress card and the session continues in its original directory. Only danger-full-access sessions can create worktrees under an external root;
  • automatic pruning/deletion is subject to the same sandbox constraints: cleaning an external root requires the session to have the matching permissions; otherwise it fails loudly instead of force-deleting.

6. Worktree management

Slash commands (management only; naming belongs to Git):

Command Purpose
/wt-status Show this session's checkout: path, repo, branch (detached when detached), base, dirty
/wt-clean [--force] [--branch] Remove this session's checkout; refuses uncommitted work or an in-use checkout unless --force; --branch also deletes the branch (meaningful with mode: branch)
/wt-snapshots List the current session's repository snapshots (all repositories when unmanaged): ref, commit, created time, restored marker
/wt-restore <ref> Restore a snapshot into a new detached checkout (wt-restored-…) and print the restored path and commit

6.1 Cleanup entry points and the in-use guard

Three entry points with identical rules:

Entry point Notes
/wt-clean [--force] [--branch] Terminal-style management command for this session's checkout
Session-header chip popover Cleans the current session's checkout in place; dirty or in-use checkouts show a confirmation / force affordance
Settings page "worktree management" row delete Cleans any managed checkout from the per-repository list
  • In-use guard (worktree-in-use): if the checkout directory is the current working directory of another live session, deletion is refused with code=worktree-in-use. This mirrors Codex, which refuses to delete the "current checkout" outright.
  • Cleaning your own checkout is not refused: when /wt-clean or the current session's chip cleans that session's own checkout, the plugin first moves the session's cwd back to its original workspace (the response carries movedTo) and then deletes — the session keeps working.
  • Force override: after confirming, force (the --force flag, or the "force clean/delete" button in the UI) overrides the guard — the other live sessions lose their working directory (their cwd points at a deleted checkout) and uncommitted changes in the checkout are lost too.
  • Dirty checkouts are still refused first with worktree-dirty; force overrides both the in-use and the dirty check.
  • If the snapshot conditions in 6.2 apply, the plugin writes the snapshot ref first and then deletes.

6.2 Snapshots and restore

  • When a snapshot is taken automatically: before deleting a detached (no branch) checkout whose HEAD ≠ baseCommit, the plugin writes the current HEAD as the snapshot ref refs/dsh-codex-style-worktree/<timestamp+session suffix> and records it in the sidecar snapshots. This applies to every delete path (/wt-clean, chip cleanup, settings-page delete, prune, the archive lazy sweep).
  • /wt-snapshots: lists the current session's repository snapshots (all repositories when the session is unmanaged): ref, commit, created time, restored marker.
  • /wt-restore <ref>: restores a snapshot into a new detached checkout (named wt-restored-<timestamp>) and returns { ref, path, commit, repoRoot }; the restored checkout is not bound to the original session and does not switch any session's cwd — it just brings the data back.
  • Settings page "worktree management": the list response carries a top-level snapshots array (each row has repoRoot, newest first) and the page groups it by repository into a "recoverable snapshots" list (ref, commit, created, restored marker); clicking Restore creates a new checkout and refreshes the list.
  • A restored checkout is an ordinary git worktree (in the pool, visible in git worktree list) but is not written into sidecar records: it has no sessionId and is not bound to a session, so it never shows up in the managed list or as a /wt-clean target; remove it manually with git worktree remove <path> when done. refs/dsh-codex-style-worktree/… are plain Git refs, so git branch <name> <ref> still works.
  • The same snapshot can be restored repeatedly (each restore creates a new checkout and updates that snapshot's restoredAt / restoredPath).
  • Snapshot failure policy: prune and the archive lazy sweep skip removing that entry to avoid losing commits; an explicit user clean only logs a warning and proceeds.
  • Snapshots only capture the committed HEAD; uncommitted changes are not included and cannot be restored.

6.3 Archive cleanup (cleanupOnArchive, off by default)

  • Setting cleanupOnArchive (boolean, default false): after a session is archived, delete its clean worktree.
  • It is a non-polling lazy sweep that runs opportunistically when: a new session is created successfully, the worktree management page is opened (a list request), a prune request runs, or about 1 second after the archive strip stops (workspace / session-stop, via an unref'd timer that never keeps the process alive).
  • Only checkouts that are both clean (dirty ones are skipped) and unused (in-use ones are skipped) are removed; the snapshot rules from 6.2 still apply before deleting.
  • Why off by default: it preserves the Codex-style "do not delete on session end" behavior; enable it when you want disk reclaimed automatically (or clean manually with /wt-clean / prune).

6.4 Automatic pruning and manual prune

  • With autoPrune: true (the default), every successful creation prunes the repository's oldest checkouts beyond autoPruneKeep in the background; dirty checkouts and anything younger than pruneGraceMs are left alone.
  • A manual prune (the settings page's Prune now, or the prune endpoint) runs immediately for one or all repositories and returns the removed entries plus the snapshots it created.
  • A pruned detached commit gets a refs/dsh-codex-style-worktree/<name> snapshot first (see 6.2), recoverable with /wt-restore or git branch <name> <ref>; uncommitted changes cannot be recovered.

7. RPC and state sync

7.1 RPC endpoints

The browser calls through the authenticated /api carrier: connection.rpc.call('/api', 'dsh-codex-style-worktree/<endpoint>', payload, signal); the host registers /api/dsh-codex-style-worktree/<endpoint> and asserts message.method === 'dsh-codex-style-worktree/<endpoint>'; responses are {ok:true,value} or {ok:false,error:{code,message,details}}.

Endpoint Request Response value Error codes
status { sessionId } { managed, path?, workspaceRoot?, repoRoot?, mode?, branch?, baseCommit?, createdAt?, dirty? } bad-request
list { workspacePath? } { repos: [{ repoRoot, records: [...] }], snapshots: [...], config } (settings-page management list; snapshots is a top-level array, each row carrying repoRoot, newest first) bad-request
clean { sessionId, force?, deleteBranch? } { removed, path, branchDeleted, snapshots, movedTo? } (movedTo means this session's cwd was moved back to the workspace; when the checkout is gone: { removed, path, alreadyGone: true, snapshots }) worktree-dirty, worktree-in-use, not-managed
prune { repoRoot? } { removed, snapshots } bad-request
placement read { workspacePath }; write { workspacePath, mode } { workspacePath, mode, defaultMode, isGit } bad-request, not-git-repository
settings {action:'get'} / {action:'set', patch, revision} / {action:'reset'} { ns, revision, writable, values, defaults, fields: [...] } settings-unavailable, conflict, invalid-config
restore { ref } (snapshot ref, required) the new checkout: { ref, path, commit, repoRoot } (not bound to the original session, no cwd switch, not written to records) bad-request, snapshot-not-found (other failures are operation-failed)
  • There is no create and no cancel: creation is triggered only by the first message of a Session (first-turn agent/pre-step); the progress card's "Cancel" merely hides the card on the client.
  • Projection data never goes through RPC; status is only used on demand by the chip popover and the settings page.

7.2 Session projection (zero polling)

  • The host registers the projection unit codexWorktree (stateVersion: 1); every change appends the event codex-worktree/state, whose data.state carries the complete post-change state;
  • state: { phase: 'creating'|'ready'|'error', step?: 'prepare'|'checkout'|'finalize', path?: string|null, repoRoot, workspaceRoot, placement?: 'worktree'|'local', mode, branch?: string|null, baseCommit?: string|null, error?, updatedAt } or null;
  • the client subscribes to sessions.list snapshot entries at byId[sessionId].projectionValues.codexWorktree (useSyncExternalStore) for the progress card, sidebar marker and chip base state;
  • no setInterval / no polling anywhere;
  • projection events go into the session log (~4 small events per new session) — that is DSH's native state-sync mechanism and the price of zero polling;
  • cleanup: live session → the host appends null, the marker disappears immediately; non-live session → null is appended on the next consistency repair (see the known limitation in 2.4).

7.3 Sidecar data ($DSH_HOME/dsh-codex-style-worktree/state.json) v3

{
  "schemaVersion": 3,
  "records": {
    "session-…": {
      "sessionId": "session-…",
      "repoRoot": "/abs/repo",
      "workspaceRoot": "/abs/repo",
      "path": "/abs/repo/.agents/worktrees/wt-…",
      "poolRoot": "/abs/repo/.agents/worktrees",
      "repoLabel": "repo-<hash8>",
      "mode": "detached",
      "branch": null,
      "baseCommit": "<sha>",
      "source": "startup",
      "createdAt": "ISO"
    }
  },
  "placements": {
    "/abs/repo": { "mode": "worktree", "updatedAt": "ISO" }
  },
  "snapshots": {
    "refs/dsh-codex-style-worktree/…": {
      "ref": "refs/dsh-codex-style-worktree/…",
      "repoRoot": "/abs/repo",
      "commit": "<sha>",
      "sessionId": "session-…",
      "createdAt": "ISO"
    }
  }
}
  • v1/v2 → v3 migration: missing sections are filled in memory on read (v1 gets placements, v2 gets snapshots) with no data loss; the next write persists v3;
  • writes are atomic (tmp + rename); a corrupt file is renamed to a backup and the store continues from an empty state;
  • stale placements are cleaned lazily: nonexistent paths are ignored, entries unused for more than 90 days are dropped;
  • snapshots backs the restore list (the top-level snapshots array in list, and the settings page's "recoverable snapshots" block); each restore appends restoredAt and restoredPath to that row, and the snapshot can still be restored again.

8. Compatibility, limitations and risks

  • DSH versions: 0.2.0-rc.2 / 0.2.1-rc.2; depends on the base bundle's sessionProjections, settings and configEditor — missing ones degrade gracefully: no projections → sidebar marker / progress card unavailable; no settings → read-only config;

  • Node.js >= 22.19.0 (the client uses React 18+ useSyncExternalStore);

  • Creation happens only on the first step of a Session's first turn and only for top-level Sessions whose recorded creation source is startup; resume / clear / compaction / subagents / taskboard sessions / matching presets / disable marker / enabled: false / existing record / placement=local never create one. A blank Session that is opened and never used creates nothing, so a placement choice made in it is read at send time and cannot lag one Session behind;

  • No new Workspace: sessions stay in their original workspace; only the cwd changes;

  • No naming, no branch (detached by default); mode: branch is opt-in; the plugin never merges, pushes or deletes user branches;

  • Non-Git workspaces: the selector is hidden and nothing is created;

  • Read-only sandbox / workspace-write boundary / no commit / limit: structured errors that never block the session;

  • concurrent creation in one repository is serialized by createKeyedMutex; any exception inside the agent/pre-step listener is swallowed and warned about, so the plugin can never prevent a session from starting or block the first message;

  • projection events enter the session log (~4 per new session), the cost of zero polling; an ephemeral channel could replace them later (out of scope for this release);

  • known limitation: cleaning a non-live session's checkout from the settings page leaves its sidebar marker until that session's next consistency repair (reopening the session or its first step; see 2.4);

  • seat choice (by design, not a limitation): the placement pill registers in the official conversation.hero.options list seat, not the single-value conversation.hero.workspace seat owned by the shipped WorkspacePicker (that seat is kind: "single" with replaceRisk: "shadows-shipped-ui"), so the shipped workspace switcher is never shadowed; the sidebar marker uses the official ambient-marker seat sidebar.session.row.leading, not the hover-only row action strip;

  • the sidebar marker is only a marker: not clickable, no hover card, no navigation;

  • the "don't use a worktree" rollback feature is explicitly out of scope; the progress card's "Cancel" only hides the card.

  • The cost of force-cleaning: overriding worktree-in-use makes the other live session lose its current working directory (its cwd points at a deleted checkout) and throws away any uncommitted changes in it; cleaning your own session's checkout is not refused, its cwd is moved back to the original workspace.

  • Restored checkouts are not managed by the plugin: they are not written to sidecar records and are not bound to a session, so remove them manually with git worktree remove; snapshots only hold the pre-delete committed HEAD.

  • Snapshots only contain committed content: the ref points at the pre-delete HEAD, so uncommitted changes are not in it; a restored checkout is not bound to the original session either.

  • Archive cleanup is a lazy sweep: cleanupOnArchive is not immediate; it only runs at the moments listed in 6.3, and skips checkouts still used by a live session.

Out of scope: cloud / Codex Web integration / multiple placements; automatic merge, PRs, environment installation; remembering placement per session (instead of per workspace); switching an existing session into a worktree mid-conversation.

9. Acceptance criteria (AC)

# Item Verification
AC-1 The placement selector appears between "workspace" and "standard mode" Manual GUI + smoke-client render assertions
AC-2 The choice is remembered per workspace: switching to "Local" makes new sessions run in the original directory with no .agents/worktrees output; it survives restart Manual + live-host RPC + sidecar assertions
AC-3 Default worktree: the first new session in a new workspace creates a worktree after sending and switches cwd Integration test (fake ctx + real git)
AC-4 After sending, a three-step progress card appears above the composer (projection-driven, zero polling), for at least 1 s; the chip/marker appear when done Manual + smoke-client state-machine assertions
AC-5 "Cancel" only hides the card and creation continues; no "don't use a worktree" offered smoke-client + code grep
AC-6 The number of workspaces in workspace.json is unchanged and the session stays in its original workspace live-host assertions + manual
AC-7 Managed sessions show a read-only icon in the sidebar (not clickable, tooltip with path); it disappears immediately after cleanup (projection push, no polling) Manual + smoke-client
AC-8 Editable settings: disabling "auto-create" stops new worktrees without a restart; values are written to the profile patch; the official Plugins page also shows an auto form Manual + live-host RPC + patch assertions
AC-9 Conflicts/invalid values in settings produce clear messages and do not corrupt the current config Unit tests (revision / validation)
AC-10 The old button, create/cancel endpoints and pending logic are gone; old records can still be displayed/cleaned Code grep + tests
AC-11 Unit tests and host/client smoke are all green Local scripts
AC-12 A real DSH host: routes registered, placement read/write, projection pushes, settings get/set, client bundle served Temporary profile + curl RPC

10. Troubleshooting

Symptom Action
No placement selector on the new-session hero row It only shows for a blank new session whose workspace is a Git repository; existing sessions and non-Git workspaces hide it (check isGit from the placement RPC).
Selected "New local worktree" but nothing was created Confirm the workspace's placement is still worktree when you send the first message (it is remembered per workspace and may have been switched to local); look for the first-step skip reason in the session log; confirm enabled: true, the repository has a commit, maxPerRepo is not reached and there is no disable marker.
Chose "Local" but an earlier session still runs in a worktree Placement only affects future sessions; existing sessions keep their cwd. Clean their checkouts from the settings page if needed.
The progress card shows an error Follow the error code: not-git-repository / no-commit / disabled / root-unreachable / limit-reached; the session continues in its original directory and the conversation is not blocked.
The progress card is stuck on "Check out files" git may be hanging or timing out: check that git is on PATH and the repository is healthy; raise gitTimeoutMs if needed.
Settings save reports "configuration was changed elsewhere" Revision conflict: the form refreshed automatically, confirm and save again; both forms (Plugins page / this page) share one revision.
Settings save reports settings-unavailable This composition has no settings service (headless / non-profile): config degrades to read-only; edit the active profile's cordis.patch.yml (- id: codex-worktree) and restart.
Managed sessions have no sidebar icon The icon only shows when the projection is phase === 'ready'; after cleaning a non-live session it clears on the next consistency repair. Confirm both server and client restarted on 0.5.1.
Cleanup fails with worktree-in-use The checkout directory is the cwd of another live session (Codex-compatible refusal): run /wt-clean from that session (its cwd is moved back to the workspace), or override with force / "force clean" once it no longer needs the directory; note this loses that session's working directory and uncommitted changes.
No restore entry point / empty snapshot list Snapshots are only created when deleting a detached checkout whose HEAD ≠ baseCommit (see 6.2); use /wt-snapshots to check the current session's repository (all repositories when unmanaged), and open the "worktree management" block in the settings page.
Restored but cannot find the new checkout in the managed list Restores produce a plain git worktree (wt-restored-…) that is not written to sidecar records, so it is not in the managed list; use the path returned by /wt-restore or git worktree list, and remove it with git worktree remove.
cleanupOnArchive is on but the checkout survives archive The lazy sweep only runs at the 6.3 triggers (new session, list, prune, after the archive strip stops); dirty or in-use checkouts are skipped.
The chip says "not managed" although the session is in a worktree The session is not managed by this plugin (created by an older version, manually via git worktree add, or placement=local); old records still appear in the settings page and can be cleaned.
The 0.2.x "New Worktree" button is gone Removed in 0.3.0; creation now happens automatically after the first message.
Old workspaces/checkouts remain after upgrading The 0.2.x button may have created workspaces and checkouts: confirm the extra sidebar workspaces, the corresponding .agents/worktrees/wt-* directories and pending records, then clean them manually (see 3.2).
Startup error Cannot find package 'dsh-codex-style-worktree' Re-run dsh plugin --profile desktop add github:wplct/dsh-codex-style-worktree (direct install: node install.mjs), confirm <profile>/node_modules/dsh-codex-style-worktree/lib/index.js exists, restart DSH.

11. Layout

dsh-codex-style-worktree/
├── install.mjs          # direct install: copy into the profile + append the marked block (--dry-run / --profile)
├── uninstall.mjs        # direct uninstall: drop the marked block + remove the directory (idempotent, --dry-run)
├── package.json         # 0.5.1; exports "." (host) / "./client" (web); engines node >=22.19
├── cordis.patch.yml     # plain insert row (id: codex-worktree, no config)
├── lib/                 # host half: config defaults/validation, volatile live config, first-message creation,
│                        # git wrapper, pool rules, cleanup guard (worktree-in-use), snapshot/restore, archive lazy sweep,
│                        # sidecar v3 (records + placements + snapshots), session projection,
│                        # RPC routes (placement/settings/status/list/clean/prune/restore), commands
├── client/client.js     # browser half: 5 slots (placement selector / progress card / sidebar marker / settings page / chip)
│   └── README.md        # client-half slots, projection, RPC, zero-polling notes
├── scripts/             # smoke-host.mjs / smoke-client.mjs
├── test/                # node --test unit/integration tests
├── README.zh.md         # Chinese primary README
├── README.md            # this English counterpart
└── LICENSE              # MIT

Local development and checks:

npm test                 # unit / integration tests (node --test)
npm run smoke            # host + client smoke
npm run verify           # the above + node --check client/client.js
npm run install:dry      # direct-install dry run
—/ 5

No ratings yet

Verified DSH bundle

Commit c4aa6d6c7bc4

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout