Skip to content

feat(tui): add /worktree command for git-worktree sessions#753

Open
sam-saffron-jarvis wants to merge 4 commits into
SamSaffron:mainfrom
sam-saffron-jarvis:feat/worktree-tui-opus
Open

feat(tui): add /worktree command for git-worktree sessions#753
sam-saffron-jarvis wants to merge 4 commits into
SamSaffron:mainfrom
sam-saffron-jarvis:feat/worktree-tui-opus

Conversation

@sam-saffron-jarvis

Copy link
Copy Markdown
Contributor

What

Adds a /worktree command (alias /wt) that lets a TUI chat session run in a git worktree — an isolated second checkout sharing one .git — created and torn down through term-llm instead of by hand. Implements the design in ~/designs/term-llm/tui-worktree.md (TUI scope).

Core: internal/worktree

A new package owning all git logic behind a small API: Create / List / Get / Promote / Remove / Diff.

  • Worktrees live out-of-tree under a managed root: $XDG_DATA_HOME/term-llm/worktrees/<repo-hash>/<name>/.
  • Detached HEAD by default (keeps refs/heads/* clean; promote-to-branch is explicit), matching the Codex precedent.
  • Optional setup script runs in the new tree after creation (e.g. npm install, copying gitignored .env) — the difference between a usable tree and a broken one.
  • Cleanup-on-failure leaves no half-created directory.
  • Adjective-noun slug names (neon-canyon) for zero-friction /worktree new.

Session binding: internal/session

  • New worktree_dir column on the session row (schema + migration 26 + scan/insert/update), mirroring the existing reasoning_effort optional-column pattern, so a session's bound tree survives resume.

TUI surface: internal/tui/chat

  • /worktree dispatch with new / list / switch / pwd (+clipboard) / diff (scrollable pager) / promote / rm (dirty-guarded, two-step force) / shell (--tmux split, locator fallback).
  • A cached footer indicator — ⌥ neon-canyon ⎇ detached@a1b2c3 ±3 — hidden entirely on the root checkout (zero git calls in the common case; TTL-debounced otherwise).
  • Worktree re-entry on resume via Init.

The single-session TUI binds by chdir, so every existing tool operates in the worktree with no per-tool threading; worktree_dir remains the persisted source of truth for a future multi-session surface.

Why

Two long-standing gaps: (1) no cheap isolation — an experimental/risky agent run dirties your real checkout, and contain (Docker) is far heavier than needed; (2) no cheap parallelism of working trees. Git worktrees solve exactly this, and per prior art (OpenCode, Codex) the hard part — creation — belongs in core behind a stable API while the TUI stays a thin list/switch/create surface.

Testing

  • internal/worktree: full lifecycle against real git (create/list/get/remove, setup script + dirty guard, cleanup-on-failure, promote + force-remove deletes branch, diff, duplicate-name, non-repo inert, porcelain parsing).
  • internal/session: worktree_dir Create/Get/Update round-trip incl. clearing.
  • internal/tui/chat: command-surface guards + a full new → bind → diff → (refused) rm → force rm → root integration flow and switch-to-root.
  • go build ./..., gofmt, and go vet clean; full go test ./... green (49 packages, 0 failures).

Notes / follow-ups (out of scope, per design)

  • Setup-script source is TERM_LLM_WORKTREE_SETUP for v1; config-file precedence (repo-local → user) is a follow-up.
  • A modal switcher dialog is sketched but deferred; switching currently uses /worktree switch <name|root>.
  • Web UI / Telegram / queue_agent isolation remain out of scope.

Let a TUI chat session run in a git worktree (an isolated second
checkout sharing one .git) created and torn down through term-llm
instead of by hand. Implements ~/designs/term-llm/tui-worktree.md.

Core (internal/worktree): Create/List/Get/Promote/Remove/Diff over a
managed root ($XDG_DATA_HOME/term-llm/worktrees/<repo-hash>/<slug>),
detached HEAD by default, optional setup script run in the new tree,
cleanup-on-failure, and an adjective-noun slug generator.

Session binding: new worktree_dir column on the session row (schema +
migration 26 + scan/insert/update, mirroring the reasoning_effort
optional-column pattern) so a session's bound tree survives resume.

TUI surface: /worktree (alias /wt) with new/list/switch/pwd/diff/
promote/rm/shell subcommands, a cached footer indicator
("⌥ name ⎇ detached@sha ±N"), and worktree re-entry on resume. The
single-session TUI binds by chdir so every existing tool operates in
the tree with no per-tool threading; worktree_dir stays the persisted
source of truth for a future multi-session surface.

Tests: worktree package lifecycle (real git), session column
round-trip, and chat command-surface guards + a full
new→bind→diff→remove integration flow.
Replace the synchronous /worktree-new path with an async create driven
by a worktree.Progress channel: a footer spinner shows live progress
(git worktree add, setup script) and on completion the session binds to
the new worktree via the existing chdir model (bindWorktree).

- worktree.CreateOptions gains a Progress chan<- Progress; Create emits
  progress events (additive alongside the existing ProgressFn callback).
- Model gains worktreeBusy/worktreeProgress/worktreeOpSeq; the spinner
  keeps ticking while a worktree op runs; renderStatusLine surfaces the
  live progress ahead of any footer message.
- worktreeProgressMsg/worktreeCreateDoneMsg drive the update loop.
/worktree shell now suspends the TUI and runs $SHELL with cwd set to the
bound worktree via tea.ExecProcess (proper terminal release/restore),
returning to chat on exit. --tmux opens a tmux split (new-window
fallback) and warns when run outside tmux. Command construction is
factored into worktreeShellCommand for testability; worktreeShellDoneMsg
reports the outcome and refreshes the footer segment.
Replace the markdown /worktree list with a reusable modal picker
(DialogWorktreePicker): a synthetic root row, one row per worktree
(status dot, dirty count, elided path), and a '+ new worktree…' row.
/worktree and /worktree list open it.

Keys route through the target's chdir binding model:
- enter switches (root -> bindRoot, a worktree -> bindWorktree),
- n / '+ new worktree…' create (async),
- d deletes with an in-place two-press confirm (worktreeDeleteTarget),
- s drops into a shell for the highlighted row.

Delete uses an async removeWorktreeDir/worktreeRemoveDoneMsg that
rebinds the session to root when the removed worktree was the bound one.
The superseded markdown list printer is removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant