Project resolution: project → worktree → session model, history, and UI#822
Merged
backnotprop merged 13 commits intoMay 30, 2026
Merged
Conversation
… roots
Resolve a session's owning project from its launch cwd instead of trusting the
transient cwd (agents cd around; launches happen from subdirs/worktrees):
- project-resolver.ts: pure resolveProjectCore(cwd, declaredRoots, gitProbe) —
(1) nearest declared root at/above cwd, (2) git toplevel, (3) cwd. Worktrees
roll up to the main repo, tagged with {cwd, branch}. 12 unit tests cover the
hierarchy + cwd contract VCs (subdir→root, worktree rollup, mygroup/ workspace).
- project-registry.ts: declared-root support (sticky flag), getDeclaredRoots(),
registerResolvedProject() (owning project + worktree/sub-repo child row).
- session-factory: attribute sessions to the resolved project; keep cwd
operational; matchKey keyed on operational scope to keep worktrees distinct.
- session record/summary carry projectCwd + worktree; manual /daemon/projects add
marks the entry declared (supersedes git boundary, enables non-git workspaces).
Backend only — UI tree (project → repo|worktree → session) is the next layer.
23 integration tests that build real git repos, linked + detached worktrees, nested repos, non-git workspaces of sibling repos, and declared roots on disk, run the real resolver + registry through every angle, and tear down all temp environments. Covers: subdir→root normalization, worktree rollup + tags, two worktrees stay distinct, non-git fallback, nested-repo innermost wins, declared workspace rollup (mygroup/), nearest-declared-wins, non-ancestor ignored, and registry persistence (parent+child rows, sticky declared flag, idempotency).
5 more integration cases pinning the context intersections: deep cwd inside a sub-repo of a declared workspace, deep cwd in a nested repo, a worktree located inside a declared workspace (owns the workspace, sub-scope = worktree dir), a deep cwd within that worktree, and a worktree located outside the declared workspace (ignores the declaration, rolls up to its own main repo).
…odel, history map, followups - hierarchy rule 4/5: 'worktree' tier generalized to sub-repo-under-declared-root - core-model-and-project-ux.md: agent→cli(resolveProject)→daemon + project UX goals - plan-history-usage-map.md: full audit of history readers/writers - project-resolution-followups.md: open items (history migration, global view, UI tree)
…ayout
History now keys on resolveProject's owning project (threaded into the plan/annotate
servers) and nests worktree history under a sanitized worktree segment:
~/.plannotator/history/{project}/{worktreeSeg?}/{slug}/NNN.md
Writer and reader use the identical {project, worktreeSeg} captured per session, so a
version written under a worktree is read back from the same place. The 6 storage fns
gain an optional trailing worktreeSeg (backward-compatible). detectProjectName kept as
the standalone fallback. +8 storage tests; 739 pass, typecheck clean.
Verified live: a plan in a worktree writes history/{repo}/{branch-seg}/{slug}/ and the
version browser reads it back via /api/plan/versions. Closes the history divergence
(core VC1/VC3, cwd VC8 history path).
buildSessionTree(projects, sessions) in packages/ui/utils/sessionTree.ts groups the live session list by owning project (projectCwd ?? cwd) then by worktree.cwd, seeds worktrees from registry rows (so zero-session worktrees still show), synthesizes worktree/project nodes for sessions with no matching row (orphan-safe, never drops a session), and sorts deterministically (name then cwd; sessions by createdAt then id). 20 unit tests incl. counts-reconcile + duplicate-name + equal-createdAt determinism. Pure: no React, no I/O. No UI yet.
listAllHistory() in packages/shared/storage.ts walks ~/.plannotator/history and
disambiguates the optional worktree level (a dir with NNN.md files = slug dir; else its
children are slug dirs and it is a worktreeSeg), returning {project, worktree?, slug,
versionCount, latest} per plan. New auth-gated GET /daemon/history (optional ?project=)
returns the index. Integration test covers flat + worktree-nested layouts.
Verified live: enumerates 4132 real entries (114 worktree-nested) without error; 401
without a token. Closes core VC4 (cross-project history / the old archive's browse job).
…) + state plan 7 facts: launcher unchanged; sidebar regroups by project->worktree->session (live-only, all projects, active expanded/others collapsed-expandable); active project = current session's owning project; history browsable + filterable by project; active+history conjoined (Active<->All filter) with a full-page history view (Git Dashboard pattern). State plan: reuse sessions/projects/activeSessionId; derive tree + active project; add appStore expand-state, a history store, and daemonApiClient.getHistory().
Rewrite AppSidebar from by-mode grouping to a depth-indented, live-only tree built from buildSessionTree(projects, sessions): projects (default collapsed, active auto-expanded), worktrees (collapsible, default expanded), sessions (leaf, mode icon, active highlight). appStore gains expandedProjects (projects) + collapsedWorktrees (worktrees) Sets with immer; useActiveProjectCwd derives the active project from the current session. Tight 26px rows, depth-based indent (fixes worktree left-drift), subtle chevrons/icons. Closes hierarchy VC6/VC7. Launcher untouched.
Replace the landing's flat 'Active sessions' list with a conjoined view under the project selector: Active<->All toggle (default Active) + project filter, plus a full-screen mode (FullSessionsHistoryView, Git Dashboard carousel pattern). 'All' lists history (past plans); clicking a history entry opens the saved plan via a new createAnnotateSession on its file path. Adds daemonApiClient.getHistory()/ createAnnotateSession + HistoryListResponse type/guards, a history-store (clone of the git-dashboard store), and listAllHistory now returns each entry's latestVersionPath. Extracts the shared ROW/pad row-style from the sidebar. Review fix: history rows for untracked/custom-named projects open via the absolute file path (no cwd dependency). Typecheck + build clean; 1527 tests pass. Closes Phase 4 (with the sidebar).
Audit of the persistent-mount / never-die session model: what stays mounted, the per-session pollers/subscriptions that keep running while hidden, list re-render churn, and where it fails at scale (~fine 1-5, degraded 5-10, breaks 10+). Reconciles with performance/findings.md; recommends eviction + visibility-gating + virtualization + code-splitting as the survivability path.
Addresses #822 review finding: the resolver's ancestor check (isAtOrUnder) appends a forward slash, so on Windows a declared root C:\\work\\group never prefix-matched a session at C:\\work\\group\\repo — declared workspace grouping silently did nothing and repos fell back to their individual git toplevels. git --show-toplevel already emits forward slashes on every platform, so normalize backslashes to forward slashes in norm(); the input cwd and declared roots then agree with it and the whole comparison chain is correct on Windows. One-function change. Adds Windows-path regression tests (fail without the fix).
Addresses #822 review finding. The worktree history segment was sanitizeTag(branch || basename(cwd)) ?? undefined — lossy as a key: - a 1-char/unsanitizable branch coalesced to undefined and dropped history into the project's FLAT path, mixing with the main checkout and other such worktrees; - distinct branches that normalize identically (feat_x and feat-x both -> feat-x) merged two worktrees' histories into one folder. Extract a pure worktreeSegment() that appends a short hash of the worktree's absolute path (its stable identity) to a readable label, falling back to wt-<hash> when the label is empty. Always non-empty, never collides. Adds unit tests. Note: #822 review finding #6 (same-basename projects collide in the landing filter) is intentionally NOT addressed here — it stems from history being keyed by project name at the storage layer (same name -> same folder on disk), so it is not a clean UI-only fix and is out of scope for this change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Project resolution: project → worktree → session model, history, and UI
Layered on top of #733 (
feat/single-server-runtime). Squash-merges into it.Establishes a single, correct ownership model — a session always belongs to one project, optionally a worktree — and surfaces it end to end. Decision facts in
goals/architecture/decisions/.Backend
project-resolver.ts):resolveProject(cwd)→ owning project + optional worktree. Rules: nearest declared root → git toplevel → cwd. Worktrees roll up to the main repo (tagged with branch); declared non-git workspaces (mygroup/of sibling repos) become the project. Git-injected → pure & testable.registerResolvedProject.projectCwd+worktree; operationalcwdpreserved for git ops.history/{project}/{worktree?}/{slug}/; reader == writer.listAllHistory()+GET /daemon/history.Frontend
buildSessionTree(projects, sessions)→project → worktree → session, orphan-safe.Tests
~40 resolver/registry tests (real git/fs integration incl. worktree × declared crossings) + history, tree-builder, history-store, and client tests. Full suite green (1527 pass); typecheck + single-file build clean. Backend verified live through the compiled daemon (worktree rollup, history layout,
/daemon/history) and via a real Claude hook → CLI → daemon probe.Not in this PR / follow-ups
project-resolution-followups.md): session-snapshot meta (projectCwd/worktree), the "Add project → declared workspace" UI verification, CLAUDE.md matchKey/history-layout doc updates, and test-hygiene (a couple of tests write to the real history dir).long-running-session-costs.mddocuments the perf tail of the never-die session model (eviction/virtualization recommended) — informational, not addressed here.