Skip to content

[Bug]: v0.0.20+ regression: manual-sort drag reorder snaps back; grouped projects lose expand state #2219

@mwolson

Description

@mwolson

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

Regression A (snap-back), reproduces at current main:

  1. Open the app with more than one project in the sidebar.
  2. In the sidebar, click the "Sort projects" icon (arrows-up-down, next to the sidebar header) and set Sort projects to Manual.
  3. Drag a project to a new slot.

Regression B (expand state wiped for grouped projects):

  1. Have at least two physical projects that share a repository (same repo, different worktrees or environments).
  2. Open the same "Sort projects" sidebar menu and set Group projects to Group by repository (or Group by repository path).
  3. Collapse the grouped project row.
  4. Trigger any project event that re-enters syncProjects (e.g. a new project is created, a project refresh runs, etc.).

Expected behavior

A. The dragged project stays in the new slot and the order persists across reloads.
B. The collapsed grouped project row stays collapsed after syncProjects runs.

Actual behavior

A. The project snaps back to its original position.
B. The grouped row re-expands.

Root cause

Both bugs share the same root cause: two readers and writers inside the web app use different key formats for the sidebar project state, and nobody catches the drift.

apps/web/src/uiStateStore.ts tracks two pieces of per-project state:

  • projectOrder -- the manual sort list.
  • projectExpandedById -- the expand/collapse state per row.

The sidebar writes/reads these states using three different key formats depending on the call site:

  • Physical key: ${environmentId}:${normalizeProjectPathForComparison(cwd)} via derivePhysicalProjectKey in apps/web/src/logicalProject.ts.
  • Scoped key: ${environmentId}:${projectId} via scopedProjectKey(scopeProjectRef(envId, projId)).
  • Logical key: the group identity per deriveLogicalProjectKeyFromSettings -- can be the repo canonical key or the physical key depending on the grouping mode.

PR #2055 (commit 188a40c) introduced the mismatch:

  1. syncProjectUiFromStore in apps/web/src/environments/runtime/service.ts:469 (the key: derivePhysicalProjectKey(project) line is 473) started storing the physical key in projectOrder and projectExpandedById.
  2. handleProjectDragEnd in apps/web/src/components/Sidebar.tsx:2857 started passing member.physicalProjectKey (physical key) to reorderProjects (see lines 2870-2873).
  3. But the readers still use scoped keys:
    • orderedProjects memo in apps/web/src/components/Sidebar.tsx:2707-2713 uses getId: (project) => scopedProjectKey(scopeProjectRef(project.environmentId, project.id)).
    • useHandleNewThread in apps/web/src/hooks/useHandleNewThread.ts:168-174 uses the same getId.

Result for Regression A: preferredIds holds physical keys, items expose scoped keys, so orderItemsByPreferredIds never matches anything and the manual order is discarded on every render. Same reader/writer mismatch as #1902.

Result for Regression B: the UI row reads/writes projectExpandedById by the logical group key, but syncProjects seeds it under the physical key. In separate grouping mode logical key equals physical key and the behavior is fine. In repository or repository_path mode the seeded key has no reader, so the row defaults to its initial state every time syncProjects runs.

Impact

Major degradation or frequent failure

Version or commit

Upstream main @ 66c326b (reproduces as of v0.0.20).

Environment

Linux (CachyOS) -- applies to all platforms since the logic lives in apps/web.

Workaround

Regression A: leave Sort projects on Last user message (the default) or Created at -- anything except Manual.
Regression B: keep Group projects on Keep separate.
Neither preserves the intended behavior.

Fix

PR incoming: route both the sort-order and expand-state paths through a single key source, so readers and writers cannot silently drift again. Adds a getProjectOrderKey helper used by both the drag-end writer and the readers, and splits SyncProjectInput into a physical key (sort order) and a logicalKey (expand state) so the store keys match what the UI reads.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions