Skip to content

Worktree cleanup UI: real 'Delete worktree' that runs git worktree remove#49

Open
JeanBaptisteRenard wants to merge 16 commits into
doctly:mainfrom
JeanBaptisteRenard:feat/worktree-cleanup-ui
Open

Worktree cleanup UI: real 'Delete worktree' that runs git worktree remove#49
JeanBaptisteRenard wants to merge 16 commits into
doctly:mainfrom
JeanBaptisteRenard:feat/worktree-cleanup-ui

Conversation

@JeanBaptisteRenard

Copy link
Copy Markdown

Stacked on #47 and #48. Currently 'Hide worktree' just toggles UI visibility — on-disk worktrees accumulate, especially with Agent-tool isolation runs. Adds a real Delete action that runs git worktree remove -f (with double-force fallback for locked worktrees), then cleans the DB cache rows. Hide is preserved for the cosmetic case.

JeanBaptisteRenard and others added 8 commits May 21, 2026 23:02
Subagent transcripts written by Claude CLI live at
<folder>/<parentSessionId>/subagents/agent-<agentId>.jsonl alongside a
.meta.json sidecar holding { agentType, description }. Surface them as
first-class rows in session_cache, keyed by sessionId 'sub:<parent>:<agentId>'.

- adds parentSessionId, agentId, subagentType, description columns
- migration v4 clears the cache so a re-index repopulates everything
- adds idx_session_cache_parent for hierarchy lookups
- new query getCachedByParent + widened cacheGetByFolder
Adds helpers for the on-disk subagent layout:
- enumerateSessionFiles(folderPath) walks top-level + <uuid>/subagents/*.jsonl
  + legacy <uuid>/*.jsonl fallback
- subagentSessionId(parent, agentId) gives the synthetic 'sub:<p>:<a>' id
- resolveJsonlPath(projectsDir, row) reconstructs the absolute path
- readSubagentMeta reads the .meta.json sidecar

readSessionFile now accepts opts.parentSessionId. When set, it requires
isSidechain on at least one entry (defensive guard), reads the sidecar for
agentType/description, falls back to filename for agentId if absent in entries.

Adds 10 unit tests covering both branches, the sidecar guard, the legacy
fallback path, and the synthetic-id format.
Both scanners (synchronous refreshFolder + worker scan-projects) now use
enumerateSessionFiles instead of a flat top-level readdir, so subagent
transcripts get cached as first-class rows with parentSessionId set.

FTS indexing inherits this for free: subagent summaries land in search_fts
the same way top-level sessions do, with type='session' and folder=<parent>.
Full-text search now finds anything an Agent call discussed.
Clicking an Agent tool_use block in the JSONL viewer now loads the matching
subagent transcript and renders it nested below the block. Re-click collapses
without refetching.

- main.js: new IPC read-subagent-jsonl(parentSessionId, agentId) and
  list-subagents(parentSessionId)
- preload.js: window.api.readSubagentJsonl / listSubagents
- jsonl-viewer.js: clickable Agent block, caret indicator, recursive nested
  render. Identical (description, subagentType) pairs are disambiguated by
  occurrence ordinal so parallel fanout calls each open their own transcript.
- style.css: subtle hover + left-border indent for nested transcripts
Runs `npm ci && npm test` on ubuntu-latest against Node 20 and 22.
Triggers on pull requests targeting main and on direct pushes to main.
Migrations that clear session_cache (v2, v3, v4) leave the cache empty on
first launch. The previous get-projects handler returned [] immediately and
fire-and-forgot the worker scan, relying on a later 'projects-changed' IPC
to trigger a reload — but app.js only reloads on that event when the user
happens to be on the Sessions tab. If they were on another tab (or
hadn't navigated to Sessions yet), the list stayed empty until they
manually switched tabs.

Make populateCacheViaWorker return a Promise that resolves when the scan
finishes. Concurrent callers share the same Promise. get-projects awaits
that Promise when the cache is empty, so the response carries the freshly
populated cache and the renderer paints immediately.
buildProjectsFromCache was projecting only the pre-PR#1 columns onto the
session object sent to the renderer. The renderer's hierarchical sidebar
(introduced in PR#2) reads parentSessionId/agentId/subagentType/description
from each session and silently flattens to top-level when they're missing —
which is what happened: all subagents appeared as siblings of their parent
instead of nested under it.

Add the four subagent columns to the projection. No DB or wire change.
Subagent support: index, search, and inline-expand transcripts (fork CI)
@JeanBaptisteRenard JeanBaptisteRenard force-pushed the feat/worktree-cleanup-ui branch from 26c5446 to a9def67 Compare May 21, 2026 21:37
readSessionFile JSON.parse'd every line of a session file in a single
outer try/catch. If a Claude CLI session was actively writing the file
while the worker scan read it, one mid-write line could throw and
invalidate the ENTIRE file's session row. With many parallel live sessions
this manifested as 'most projects show no sessions after a fresh index'.

- Per-line try/catch inside readSessionFile: skip malformed lines, keep
  parsing the rest.
- Per-file try/catch in workers/scan-projects.js: defensive belt and
  braces so one unparseable file can't abort an entire folder scan.
@JeanBaptisteRenard JeanBaptisteRenard force-pushed the feat/worktree-cleanup-ui branch from a9def67 to f1e592b Compare May 21, 2026 21:58
resolveWorktreePath was defined and exported in the file but never
actually called from deriveProjectPath. As a result every worktree under
<project>/.claude/worktrees/<name>/ appeared as a separate project group
in the sidebar instead of being grouped under its parent project.

Wire the call in both branches of deriveProjectPath (direct .jsonl path
and subdirectory path) and export resolveWorktreePath for other callers.
session-transitions.js now tracks per-active-session knownSubagents and emits
two new IPC events:
- subagent-spawned (parentSessionId, agentId, subagentType, description) when
  a new agent-*.jsonl file appears under <sid>/subagents/
- subagent-completed (parentSessionId, agentId) when an existing file's mtime
  has been stable for >30s

Adds two IPCs for read-only live tailing:
- start-subagent-watch (parent, agentId) -> watchId
- stop-subagent-watch (watchId)

The watcher streams new JSONL entries via subagent-watch-event so the renderer
can append them to an open inline-expanded subagent transcript without polling.
- sidebar.js: subagents are no longer flat siblings of their parent. Each
  top-level row now shows a 'N subagents' affordance with a disclosure caret;
  expanded children render indented with a subagentType pill, description as
  label, and persist expansion state per parent in localStorage. Orphan
  subagents (parent absent from cache) hoist to a 'Orphan subagents' group.
- grid-view.js: each active session card now shows a stack of small pills
  for currently-running subagents, color-coded by subagentType, capped at
  5 with a '+N more' overflow. Listens on onSubagentSpawned/onSubagentCompleted.
- jsonl-viewer.js: when an inline-expanded subagent block represents a still-
  running agent, start an fs.watch via the new IPC and append streamed
  entries; show a small '● live' indicator until completion. Stops the
  watch on collapse or subagent-completed.
- style.css: minimal styles for new sidebar/grid/live elements, matching
  existing palette and font weights.
When Switchboard attaches to a session that already has many subagent
files on disk (e.g. a long-running session with 100+ Agent calls), the
first walk of detectSubagentTransitions treated every existing file as a
'first sighting' and emitted subagent-spawned for each. 30s later it
emitted subagent-completed for each. Hundreds of IPC events back to back
froze the renderer UI.

Distinguish bootstrap (first walk for this session) from steady-state.
On bootstrap, record every existing file in knownSubagents silently:
- files modified in the last 60s stay in the active lifecycle (could be
  mid-run, will eventually fire subagent-completed)
- older files are marked completed immediately with no IPC

Only files that appear AFTER the bootstrap walk fire subagent-spawned.
@JeanBaptisteRenard JeanBaptisteRenard force-pushed the feat/worktree-cleanup-ui branch from 5dcc7f1 to 5d0bfbc Compare May 22, 2026 07:29
JeanBaptisteRenard and others added 3 commits May 22, 2026 09:54
…lity

Subagent observability: hierarchy, transitions, badges, live tail (fork CI)
…om disk

The existing 'Hide worktree' affordance only added the path to hiddenProjects;
on-disk worktrees accumulated. This adds a real delete action.

- main.js: new IPC delete-worktree(path). Validates the path matches the
  worktree-layout regex (.claude/worktrees, .claude-worktrees, .worktrees),
  runs 'git worktree remove -f' via execFile (no shell), retries with '-f -f'
  on locked worktrees, then purges any matching session_cache rows and FTS
  entries. Returns { ok, removed } / { ok: false, error }.
- preload.js: window.api.deleteWorktree binding.
- sidebar.js: 'Delete worktree' button on worktree sub-groups, with confirm
  dialog. Keeps 'Hide worktree' for the cosmetic case.

Hide is unchanged; Delete is opt-in and confirmed before running.
Makes it possible to tell the dev build apart from a released installed
binary in the OS task switcher, dock, About menu, and window title.
No-op in packaged builds.
@abasiri

abasiri commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Thanks for this — the core of the feature is sound and it's something we want: execFile with array args and a -- separator (no shell injection), path validation against the known worktree layouts before anything destructive runs, git itself refusing non-worktrees/main checkouts, and an explicit confirm dialog. Nice work on the safety basics.

Main ask: could you isolate this from #47/#48 into a standalone PR against main? The diff here currently carries the whole subagent stack (16 commits, 14 files) while the actual worktree-delete change is ~110 lines across 3 files (main.js, preload.js, public/sidebar.js). Nothing in it depends on the subagent work — it only needs the worktree headers, which already exist on main. The stack is also in flux (#47 just got a fix reverting the index-time worktree collapse, which this feature needs to be reachable at all), so decoupling lets us review and land this on its own merits without waiting on #47/#48.

A few things to address in the standalone version:

  1. Guard against live sessions and dirty state. git worktree remove -f silently discards uncommitted changes, and the automatic double--f retry overrides locks the user may have set deliberately. Nothing checks whether an active PTY session is currently running in that worktree — deleting it leaves a running Claude session in a deleted directory. At minimum, check activeSessions for a matching cwd and block (or warn specifically) before removing; ideally also surface dirty/locked state in the confirm dialog instead of auto-double-forcing.

  2. Deleted worktrees resurrect. The handler removes the worktree from disk and purges cache rows, but the transcripts under ~/.claude/projects/<encoded-worktree-path>/ remain. Any future re-index (cold start after a migration, or Reconcile cache with filesystem on get-projects so sessions stop going missing #60's reconcile-on-get-projects once it lands) re-derives the project from those transcripts and the deleted worktree group reappears, pointing at a nonexistent directory. The handler also actively removes the path from hiddenProjects, making the comeback more likely. Suggest either adding the path to hiddenProjects (rather than removing it) or asking the user whether to delete the transcript folder too.

  3. Minor: a non-string worktreePath throws before validation (.replace on undefined), which rejects the IPC promise and the onclick swallows it — no alert for the user. And the Switchboard (dev) title change is unrelated to this PR; please drop it or move it to its own PR.

Happy to re-review quickly once it's rebased onto main as a standalone change.

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.

2 participants