Creating a project whose resolved primary path already belongs to a
non-archived project now raises a clear ValueError naming the existing
project (create_project) — duplicated projects each seeded an identical
copy of the repo subtree, multiplying the duplicate-lane bug per copy.
The agent-facing project_create tool is idempotent instead: it re-activates
the existing project rather than erroring. allow_duplicate_path=True keeps
deliberate duplicates possible. Also updates the legacy non-git lane-id
expectation to the branch-style id introduced for #53329.
_build_repos emptied each lane's sessions array for the overview (hydrate=False)
payload BEFORE _sort_lanes ran. _lane_sort_key derives a lane's activity from
max(_session_time(s) for s in group["sessions"]), so with the rows already gone
every non-trunk lane scored activity=0.0 and the sort key
(is_trunk, is_kanban, -activity, label) collapsed to alphabetical-by-label. The
documented intent — branches and linked worktrees sort by most-recent activity,
then label — was silently defeated on the projects.tree RPC that feeds the
desktop sidebar overview, while the drill-in path (hydrate=True) kept the rows
and sorted correctly. Any repo with two or more non-trunk lanes showed a
different order in the overview than when opened.
Move the session-clearing to after _sort_lanes/_disambiguate_labels so the sort
reads real recency. Lane counts are still captured before clearing, so
sessionCount and the slim overview payload are unchanged — only the order is
fixed, and the overview now matches the drill-in.
_place_by_heuristic used the raw path as the lane key for non-git
project folders, while the desktop overlay independently computed
::branch::main for the same session (since git_branch was null).
The ID mismatch caused duplicate lanes — one from the backend with
the folder name, one from the overlay labeled 'main'.
Use _branch_lane_id(path, DEFAULT_BRANCH_LABEL) so the backend's
lane key matches the overlay's expected ::branch::main scheme,
eliminating the duplicate lane.
The main-checkout test compared the two probe roots with raw string
equality. When they differed only in separator spelling, the repo's own
checkout was misclassified as a linked worktree: it fell through to the
worktree branch and was labeled by directory basename. The sidebar then
showed one checkout twice — a dir-labeled lane plus the branch-labeled
`main` lane built from the same sessions.
Compare with `_path_key` so platform path identity decides, matching how
every other path comparison in this module is already keyed.
Tests cover the single-checkout case and the main + linked-worktree case;
both fail before this change (the lane comes back labeled `repo`, not
`main`).
Sessions with no cwd — or whose folder can't be promoted to a project (the
bare home dir, a deleted workspace, HERMES state) — were dropped from the
project tree entirely, so the grouped sidebar silently showed fewer chats
than flat Recents. Collect them into a synthetic `__no_project__` node at
the head of the list. It carries one lane purely to hold the rows, and is
omitted when empty so a project-less install stays blank.
The name-based sibling probe can't reach every dead worktree: a dir renamed away
from its repo's prefix (`hermes-salvage-drafts` next to `hermes-agent`) shares
nothing to trim back to, and a scratch dir under /tmp was never a worktree at
all. Those fall through to the path-only heuristic, which reports the cwd as its
own repo root, and Tier 2 promotes it to a top-level project — one that can't be
opened and can only be dismissed by hand.
Gate auto-project promotion on the directory still existing, threaded in as an
injected predicate to keep the builder pure. The guard keys on the directory,
not on git-ness, so a plain non-git folder that's still on disk keeps its
project; callers that can't stat (remote backends) omit it and keep every
candidate. A stale persisted `git_repo_root` gets the same treatment, so a
deleted repo can't resurrect from the recorded value alone.
`_probe_sibling_worktree` recovers a deleted `<repo>-<suffix>` worktree by
trimming its name back to a sibling that still resolves, but it only trimmed
the LEAF segment. A session's cwd is usually a subdir of the worktree
(`<repo>-<suffix>/apps/desktop`), whose basename shares nothing with the repo,
so the probe no-oped and the dead path fell through to the path-only heuristic
— which minted it as its own main repo root, and then as a top-level project.
Walk the ancestors, deepest first, with the probe budget shared across the whole
walk so a deeply nested cwd can't fan out into a probe storm.
Addresses @teknium1's review of #61950:
- The desktop live overlay (workspace-groups.ts) matched cwd membership
case-sensitively, so a fresh mixed-case/separator Windows session missed
its explicit/auto project until the next backend tree refresh. Mirror the
backend identity (isWindowsPath/comparisonSegments/pathKey) in isPathUnder,
liveSessionProjectId, and overlayRepoLanes lane matching. Comparison-only —
emitted ids/labels keep their spelling. POSIX stays case-sensitive.
- Backend _is_windows_path missed root-relative `\wsl.localhost\...` (single
leading backslash), leaving that historical spelling case-sensitive. Classify
any backslash-rooted path as Windows.
Tests: WSL-spelling collapse + explicit-project precedence (project_tree),
Windows/WSL live-overlay membership + POSIX case-sensitivity (workspace-groups).