fix(desktop): order project-tree lanes by recency in the overview, not alphabetically

_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.
This commit is contained in:
briandevans
2026-07-12 20:40:50 -07:00
committed by Teknium
parent 0d07fe63f9
commit f7de2ca416
2 changed files with 45 additions and 2 deletions

View File

@@ -125,6 +125,42 @@ def test_linked_worktrees_fold_under_their_common_repo_root():
assert linked["path"] == "/elsewhere/wt"
def test_overview_orders_lanes_by_recency_not_alphabetically():
# Two linked-worktree lanes under one common repo root whose ALPHABETICAL
# order (wt-aaa, wt-zzz) is the OPPOSITE of their activity order (wt-zzz is
# the more recently active). The overview (hydrate=False) empties lane
# session arrays for payload slimness — but the lane sort must still run on
# real recency, matching the drill-in (hydrate=True) order, not collapse to
# alphabetical because the rows were dropped before sorting.
resolve = _resolver(
{
"/repo": ("/repo", "/repo"),
"/wt-aaa": ("/repo", "/wt-aaa"),
"/wt-zzz": ("/repo", "/wt-zzz"),
}
)
sessions = [
_session("/repo", branch="main", last_active=5000),
_session("/wt-aaa", last_active=1000), # alphabetically first, older
_session("/wt-zzz", last_active=9000), # alphabetically last, newer
]
def _non_trunk_labels(hydrate):
tree = pt.build_tree([], sessions, [], resolve, hydrate=hydrate)
project = tree["projects"][0]
return [
g["label"]
for repo in project["repos"]
for g in repo["groups"]
if not g["isMain"]
]
# Overview path: recency order (newer first), NOT alphabetical.
assert _non_trunk_labels(hydrate=False) == ["wt-zzz", "wt-aaa"]
# Drill-in path already sorts by recency — the two paths must agree.
assert _non_trunk_labels(hydrate=True) == ["wt-zzz", "wt-aaa"]
def test_kanban_task_worktrees_collapse_into_one_bucket():
resolve = _resolver(
{

View File

@@ -376,8 +376,6 @@ def _build_repos(sessions: list[dict], resolve: Optional[Resolve], hydrate: bool
group = entry["group"]
group["sessions"].sort(key=_session_time, reverse=True)
count = len(group["sessions"])
if not hydrate:
group["sessions"] = []
repo_identity = _path_key(entry["repo_key"])
repo = repos.get(repo_identity)
@@ -397,6 +395,15 @@ def _build_repos(sessions: list[dict], resolve: Optional[Resolve], hydrate: bool
for repo in repo_list:
repo["groups"] = _sort_lanes(repo["groups"])
_disambiguate_labels(repo["groups"])
# Drop per-lane session rows only AFTER sorting: _lane_sort_key ranks
# non-trunk lanes by most-recent activity, which it derives from the
# session rows. Clearing them earlier makes every lane look inactive on
# the overview (hydrate=False) path and collapses the sort to
# alphabetical. Counts were already captured above, so the payload stays
# slim without losing the recency order.
if not hydrate:
for group in repo["groups"]:
group["sessions"] = []
_disambiguate_labels(repo_list)
return repo_list