`projects.tree` answers for the backend's own profile, so the grouped
sidebar had nothing to draw once the user asked to see every profile.
Run the same authoritative builder once per profile against that
profile's state.db and merge the results by folder, so one checkout is
one group no matter how many profiles work in it, and the owning profile
rides on each session row where the badge and filter can read it.
Group totals are summed in SQL rather than over the loaded page — a
number that shrank as you scrolled would be worse than no number.
Scope the batched sidebar slices while we're here: cron and messaging
came back cross-profile unconditionally, which is why a concrete profile
showed another profile's Telegram threads and cronjobs.
Closes#65710Closes#42651Closes#70629
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.
A linked worktree at <repo>-<suffix> that has been deleted leaves its
sessions with a dangling cwd: the git probe fails and no git_repo_root was
persisted, so the path-only heuristic promoted each one to its own
standalone project. Every abandoned worktree added another phantom entry to
the sidebar, and they accumulate indefinitely.
Recover the parent by trimming one -<segment> at a time off the basename and
returning the first sibling that resolves. A deleted worktree has no checkout
to return to, so its sessions land in the parent's trunk lane rather than a
lane keyed by the dead path.
Live worktrees are unaffected — they resolve through the git probe and keep
their own lane.
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).