wplct/dsh-codex-style-worktree

dsh-codex-style-worktree

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

未分類

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)

Capability0.3.00.4.0
Cleanup guardCleanup deletes directlyRefused 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 deleteOnly autoPrune kept a ref for detached commitsEvery 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 restoreManual git branch only/wt-snapshots, /wt-restore <ref>, and a "recoverable snapshots" list in the settings page
Archive cleanupCheckouts always kept after a session endsOptional cleanupOnArchive (default off): a lazy sweep removes clean, unused checkouts after archive
RPC6 endpoints7 (adds restore); the list response gains per-repository snapshots
Sidecarv2v3 (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

Capability0.2.x0.3.0
When the worktree is createdAutomatically for every new session (no choice); a "New Worktree" button could also create one on demandYou choose a placement on the hero row; the worktree is created after the first message (first-turn agent/pre-step)
Selector locationA dock button above the composerThe hero workspace row: a pill between 📁 workspace ▼ and standard mode ▼
Remembered choiceNonePer workspace, persisted in the sidecar (placements), restart-safe
WorkspaceThe old button called workspaces.create, cluttering the sidebarNever creates a Workspace; only the session cwd changes
Progress feedbackNoneInline three-step progress card above the composer (projection-driven, minimum 1 s)
SidebarNo markerManaged sessions show a read-only worktree icon (tooltip = checkout path)
Settings pageRead-only configEditable form; save writes the active profile patch and applies live (volatile, no restart)
State syncSome RPC readsEverything is event/projection-driven, zero polling
RPCIncluded 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 codeMeaning
not-git-repositoryThe workspace is not a Git repository
no-commitThe repository has no commit yet
disabledenabled: false, or .dsh-codex-style-worktree-off exists at the repository root
root-unreachableThe configured root is not writable, or the session sandbox forbids that path
limit-reachedThe repository reached maxPerRepo (clean up old checkouts first)
other messagegit 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.

GoalAction
Full uninstalldsh plugin --profile desktop remove dsh-codex-style-worktree (direct install: node uninstall.mjs), then restart DSH
Revert the profile patch onlycp <profile>/cordis.patch.yml.bak-<timestamp> <profile>/cordis.patch.yml, delete node_modules/dsh-codex-style-worktree/ manually, restart
Roll back to the previous packagerename 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)

FieldTypeDefaultControlConstraints / notes
enabledbooleantrueswitchMaster switch; when off, new sessions get no worktree (existing checkouts are untouched)
defaultPlacementworktree|localworktreeselectPlacement for a workspace with no stored record yet
directorystring.agents/worktreestextPool directory: relative, no ./.. segments
rootstring""textEmpty = inside the repository; may be absolute or repository-relative, no ..
modedetached|branchdetachedselectdetached creates no branch (default); branch creates <branchPrefix><name> up front
namePrefixstringwt-textCheckout name prefix; no / \, must not start with -
branchPrefixstringwt/textBranch prefix for mode: branch; must not start with - or /
baseRefstringHEADtextLocal base revision; must not start with -; resolved locally (network only via fetchBeforeCreate)
fetchBeforeCreatebooleanfalseswitchRun git fetch --prune origin before creation; failures only warn and never block; skipped without origin
autoPrunebooleantrueswitchAfter each successful creation, prune the repository's oldest checkouts in the background (keep the newest autoPruneKeep)
autoPruneKeepnumber15numberNewest checkouts kept per repository; non-negative integer
pruneGraceMsnumber60000numberCheckouts younger than this are never auto-pruned (milliseconds); non-negative integer
cleanupOnArchivebooleanfalseswitchAfter 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
poolGitignorebooleantrueswitchWrite a self-ignoring .gitignore when the pool is created, keeping the main checkout's git status clean
gitTimeoutMsnumber20000numberHard timeout for one git command (milliseconds); positive integer
skipChildAgentsbooleantrueswitchNever create a worktree for subagents/child sessions