Before submitting
Area
apps/web
Steps to reproduce
Regression A (snap-back), reproduces at current main:
- Open the app with more than one project in the sidebar.
- In the sidebar, click the "Sort projects" icon (arrows-up-down, next to the sidebar header) and set Sort projects to
Manual.
- Drag a project to a new slot.
Regression B (expand state wiped for grouped projects):
- Have at least two physical projects that share a repository (same repo, different worktrees or environments).
- Open the same "Sort projects" sidebar menu and set Group projects to
Group by repository (or Group by repository path).
- Collapse the grouped project row.
- 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:
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.
handleProjectDragEnd in apps/web/src/components/Sidebar.tsx:2857 started passing member.physicalProjectKey (physical key) to reorderProjects (see lines 2870-2873).
- 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.
Before submitting
Area
apps/web
Steps to reproduce
Regression A (snap-back), reproduces at current
main:Manual.Regression B (expand state wiped for grouped projects):
Group by repository(orGroup by repository path).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
syncProjectsruns.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.tstracks 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:
${environmentId}:${normalizeProjectPathForComparison(cwd)}viaderivePhysicalProjectKeyinapps/web/src/logicalProject.ts.${environmentId}:${projectId}viascopedProjectKey(scopeProjectRef(envId, projId)).deriveLogicalProjectKeyFromSettings-- can be the repo canonical key or the physical key depending on the grouping mode.PR #2055 (commit 188a40c) introduced the mismatch:
syncProjectUiFromStoreinapps/web/src/environments/runtime/service.ts:469(thekey: derivePhysicalProjectKey(project)line is 473) started storing the physical key inprojectOrderandprojectExpandedById.handleProjectDragEndinapps/web/src/components/Sidebar.tsx:2857started passingmember.physicalProjectKey(physical key) toreorderProjects(see lines 2870-2873).orderedProjectsmemo inapps/web/src/components/Sidebar.tsx:2707-2713usesgetId: (project) => scopedProjectKey(scopeProjectRef(project.environmentId, project.id)).useHandleNewThreadinapps/web/src/hooks/useHandleNewThread.ts:168-174uses the samegetId.Result for Regression A:
preferredIdsholds physical keys, items expose scoped keys, soorderItemsByPreferredIdsnever 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
projectExpandedByIdby the logical group key, butsyncProjectsseeds it under the physical key. Inseparategrouping mode logical key equals physical key and the behavior is fine. Inrepositoryorrepository_pathmode the seeded key has no reader, so the row defaults to its initial state every timesyncProjectsruns.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) orCreated at-- anything exceptManual.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
getProjectOrderKeyhelper used by both the drag-end writer and the readers, and splitsSyncProjectInputinto a physicalkey(sort order) and alogicalKey(expand state) so the store keys match what the UI reads.