14 KiB
Project Ordering Mode
Problem
The sidebar can group workspaces by project, but project header order is currently coupled to workspace sorting instead of having its own user choice.
src/renderer/src/components/sidebar/SidebarWorkspaceOptionsMenu.tsx:59defines oneSort bycontrol for workspaces; it has no separate project-order setting.src/renderer/src/store/slices/ui.ts:723stores onlysortBy, andsrc/shared/types.ts:2543persists the same workspace sort field.src/renderer/src/components/sidebar/worktree-list-groups.ts:36derivesProjectGroupOrderingfromsortBy, so project headers follow the highest-ranked visible workspace in Recent/Smart and fall back to manual order otherwise.src/renderer/src/components/sidebar/WorktreeList.tsx:3765computes the ordered workspace ids, andsrc/renderer/src/components/sidebar/WorktreeList.tsx:4060feeds the derived project ordering intobuildRows(...).src/renderer/src/components/sidebar/WorktreeList.tsx:826only enables project-header drag when project grouping is manual and there are no Project Groups.src/renderer/src/components/sidebar/project-header-drag.ts:1already implements pointer-based project header dragging for virtualized rows, andsrc/renderer/src/store/slices/repos.ts:699,src/main/ipc/repos.ts:1029, andsrc/main/persistence.ts:2497already persist whole-repo manual reorders.
Result: users cannot choose Manual vs Recent ordering specifically for projects, the default is not Manual, and changing workspace sort can unexpectedly reorder projects.
Goal
Add a project-only ordering preference with two modes:
Manual, the default. Project headers render in the persisted manual project order, and users can drag project headers to reorder them.Recent. Project headers render by the most recent visible workspace activity in each project.
This must affect only project header order in groupBy: 'repo'. Worktree/workspace rows inside each project must continue to use the existing workspace sortBy, filtering, pinning, lineage, status, and manual-order behavior.
Non-goals
- Do not change
sortBysemantics or the workspaceSort bycontrol. - Do not change worktree
manualOrder,sortOrder,lastActivityAt, orbuildWorktreeComparator(...). - Do not reorder worktrees inside a project when the project ordering mode changes.
- Do not change Project Group header order; groups continue to use
ProjectGroup.tabOrder. - Do not add GitHub-specific behavior. Repo/project ordering must remain provider-neutral and work for GitLab, folder projects, local repos, SSH repos, and runtime environments.
- Do not add telemetry in this pass unless a later product requirement asks for it.
Design
-
Add a persisted project-order preference.
- Introduce
ProjectOrderBy = 'manual' | 'recent'insrc/shared/types.tsor a concrete sidebar ordering module. - Add
projectOrderBy: ProjectOrderBytoPersistedUIState, defaulting to'manual'ingetDefaultUIState()(src/shared/constants.ts:387). - Add
projectOrderByandsetProjectOrderByto the UI slice next tosortBy(src/renderer/src/store/slices/ui.ts:723). LikesetSortBy, this setter is a bareset({...})— it does not persist on its own. - Persist through the debounced UI writer, not the setter.
sortBy/groupByreach disk only via the explicit field list inApp.tsx'swindow.api.ui.set({...})effect (src/renderer/src/App.tsx:1019) plus its dependency array (src/renderer/src/App.tsx:1042). AddprojectOrderByto both, or the value lives only in memory and silently resets to'manual'every restart. - Normalize persisted values in
src/main/persistence.tsthe same waysortByis normalized atsrc/main/persistence.ts:323, and wire the normalizer into bothgetUI()(src/main/persistence.ts:3116) andupdateUI()(src/main/persistence.ts:3147); invalid or missing values resolve to'manual'. - Hydration in
src/renderer/src/store/slices/ui.ts:1862should read the normalized value without migrating existingsortBy.
- Introduce
-
Expose the choice in the sidebar options menu.
- In
SidebarWorkspaceOptionsMenu, keep the existingSort bysubmenu as workspace sorting. - Add a second radio submenu labeled
Project orderwithManualandRecent. - Show it only when
groupBy === 'repo', because project ordering has no visible effect innone,workspace-status, orpr-status. - Use existing
DropdownMenuRadioGroupand sidebar/menu styling fromdocs/STYLEGUIDE.md; no new tokens or custom colors. - Copy should make the scope clear: Manual description
Drag projects to arrange them; Recent descriptionMost recent workspace activity.
- In
-
Decouple project header ordering from workspace sorting.
- Replace
getProjectGroupOrdering(groupBy, sortBy)with a project-order resolver that depends ongroupByandprojectOrderBy, not workspacesortBy. - Update
buildRows(...)sogroupBy !== 'repo'ignores project order,projectOrderBy === 'manual'uses persisted manual ranks, andprojectOrderBy === 'recent'sorts project header entries by a per-repo recent timestamp. - Recent must be timestamp-based, not first-encounter. Today's
'visible-worktree-order'path (src/renderer/src/components/sidebar/worktree-list-groups.ts:548) works only because the caller pre-sorts the worktree array by recency whensortByisrecent/smart. Decoupling fromsortByremoves that guarantee — the incoming array may be name- or manual-sorted — so the Recent resolver must explicitly computemax(lastActivityAt)per repo rather than relying on encounter order. - Recent project rank should be the maximum
lastActivityAtamong that repo's visible, unpinned worktrees passed intobuildRows(...). Empty placeholder projects and imported-worktree-card-only projects fall back toRepo.addedAt, then manual rank, then label. - Do not reorder
group.items; only reorder the project header entries beforeappendOrderedGroups(...). This preserves the existingorderMainWorktreeFirst(...)and workspace row ordering atsrc/renderer/src/components/sidebar/worktree-list-groups.ts:632. - With Project Groups, keep group headers ordered by
tabOrder; apply Manual/Recent only to project entries within each group or the ungrouped bucket.
- Replace
-
Make Manual drag/drop project-scoped (no-Project-Groups case only in v1).
- Enable project header dragging when
groupBy === 'repo' && projectOrderBy === 'manual', regardless of workspacesortBy. TodaycanReorderRepoHeadersis additionally gated on!hasProjectGroups(src/renderer/src/components/sidebar/WorktreeList.tsx:827); the only change here is dropping thesortBy-derivedprojectGroupOrderinginput in favor ofprojectOrderBy === 'manual'. The!hasProjectGroupsgate stays. - Keep using pointer events from
project-header-drag.tsunchanged; the virtualized-row reasoning atsrc/renderer/src/components/sidebar/project-header-drag.ts:3still applies. For the no-Project-Groups case it already does exactly what we need: a flat whole-list permutation committed throughreorderRepos(...), preserving current behavior and IPC rejection semantics. - Continue to suppress click-to-collapse only after a promoted drag, matching
src/renderer/src/components/sidebar/project-header-drag.ts:142. - Defer grouped (within-Project-Group) drag to a follow-up.
useRepoHeaderDragis built for a single flatorderedRepoIdspermutation:endDrag(src/renderer/src/components/sidebar/project-header-drag.ts:130) computesfromIndex/insertAtover one array and callsonCommit→reorderRepos, andonHandlePointerDown(src/renderer/src/components/sidebar/project-header-drag.ts:300) snapshots every on-screen[data-repo-header-id]with no notion of group boundaries. Supporting in-group reorder needs new hook logic — bucket-aware drop targets, rejecting drops outside the source sibling bucket, computing a midpoint between neighborprojectGroupOrdervalues, and a second commit mode that callsmoveProjectToGroup(projectId, sameGroupId, order)(src/renderer/src/store/slices/repos.ts:138) instead ofreorderRepos. This is its own change and is out of scope for v1. - In v1, Project-Groups users keep the project actions menu as the move surface (it already calls
moveProjectToGroup). Manual project order still applies to their headers via the row-builder sort (step 3); only drag-to-reorder is unavailable inside groups. - Do not turn a project reorder drag into a cross-group move in any version. The existing project actions menu remains the cross-group move surface.
- Enable project header dragging when
-
Keep persistence and runtime parity intact.
- Local no-group manual reorder continues through
repos:reorder(src/main/ipc/repos.ts:1029) andStore.reorderRepos(...)(src/main/persistence.ts:2497). - Remote no-group manual reorder continues through
repo.reorder(src/renderer/src/store/slices/repos.ts:723). - Grouped manual reorder via drag is deferred (see step 4); the follow-up would route through
projectGroup.moveProject/moveProjectToGroup(...), which already handles local and runtime-environment calls. In v1, grouped projects move only through the existing actions menu. - The project-order preference is renderer UI state, not repo metadata. It persists through the
App.tsxdebouncedwindow.api.ui.set({...})writer (step 1), not through any repo record.
- Local no-group manual reorder continues through
-
Tests.
- Add row-builder tests proving
projectOrderBy: 'manual'orders project headers byrepoOrderwithout changing workspace row order. - Add row-builder tests proving
projectOrderBy: 'recent'orders project headers by max visibleworktree.lastActivityAtwhile preserving each project's child row order. - Add Project Group tests for Manual and Recent within groups, plus unchanged
ProjectGroup.tabOrder. - Rewrite, do not extend, the existing Recent/
getProjectGroupOrderingtests. The current cases atsrc/renderer/src/components/sidebar/worktree-list-groups.test.ts:639("orders repo headers by first encounter…"),:663("…highest-ranked visible child"), and thegetProjectGroupOrderingblock at:752all assert the old first-encounter coupling and are semantically incompatible with timestamp-based Recent. Replace them with tests for the new resolver and themax(lastActivityAt)ordering. - Add UI slice/persistence normalization tests for default Manual, invalid value fallback, and hydration. Include a persistence-writer test (or note) that
projectOrderByis part of theApp.tsxui.setpayload so it actually round-trips across restart. - Grouped manual drag persistence tests are deferred with the grouped-drag feature (step 4).
- Add row-builder tests proving
Edge cases
- Existing users with no
projectOrderByget Manual, even if their workspacesortByis Recent. - Changing workspace
sortBymust not change project header order unlessprojectOrderBy === 'recent'and the visible worktree set/activity data changes. - Changing
projectOrderBymust not mutate any worktreemanualOrderor repo order by itself. - Manual drag while a project is added or removed can race. Whole-repo reorder (the only v1 drag path) keeps the existing permutation rejection and refetch behavior. (Grouped midpoint writes and their refetch-on-failure handling come with the deferred grouped-drag follow-up.)
- In Recent mode, projects with no visible worktrees should still render when they are placeholders or imported-worktree-card candidates; they sort after projects with activity.
- Pinned worktrees remain in the Pinned section. They should not make their project jump in Recent ordering unless an unpinned visible workspace in that project is also recent.
- Filters and hidden sleeping/default-branch workspaces affect the visible worktree set. Recent project order should reflect the rows the user can currently see.
- Project Group collapse state must not change when the order mode changes.
- Dragging a project inside a collapsed Project Group is impossible because its repo headers are not mounted; no special handling is needed.
- Dragging across Project Group boundaries should not silently move the project; cross-group moves stay on the actions menu. (In v1 there is no in-group drag at all — see step 4 — so this only constrains the deferred grouped-drag follow-up.)
- SSH/runtime projects must use the same store actions as local projects. No local filesystem path assumptions are needed.
- Folder projects have synthetic worktrees and should participate through the same
lastActivityAtand manual repo order paths. - Multi-window or external mutations are last-writer-wins through existing persistence. A rejected whole-repo permutation refetches repos (existing behavior); a failed grouped move would do the same once grouped drag lands.
- Behavior change on upgrade: existing users on
sortBy: recent/smartcurrently see project headers bubble to follow workspace activity. After this change they default to Manual project order, so headers stop bubbling until they pick Recent in the new submenu. This is intended (matches the "noprojectOrderBy→ Manual" edge case) but is a visible change worth calling out in release notes.
Rollout
- Add
ProjectOrderBytypes/defaults/normalization, the UI slice setter, theApp.tsxdebounced-writer field, and persistencegetUI()/updateUI()wiring. - Add the
Project ordersubmenu inSidebarWorkspaceOptionsMenu. - Update
WorktreeListto readprojectOrderBy, pass it to row construction, and enable project drag only in Manual project order (no-Project-Groups case, keeping the!hasProjectGroupsgate). - Update
worktree-list-groups.tsto order project headers by Manual or Recent independently from workspacesortBy, with Recent usingmax(lastActivityAt)per repo. - Add focused row-builder and UI persistence/round-trip tests; rewrite the incompatible first-encounter/
getProjectGroupOrderingtests. - Run targeted Vitest for sidebar row ordering and repo slice tests, then
pnpm typecheckandpnpm lint. - Validate in Electron: default startup shows Manual project order, the choice survives restart, project drag/drop reorders project headers only (ungrouped), Recent project order follows workspace activity, and worktree rows inside each project do not change when toggling project order.
Deferred follow-up (separate change): extend useRepoHeaderDrag for grouped sibling buckets — bucket-aware drop targets and a second commit mode using moveProjectToGroup(...) midpoint ordering inside Project Groups — plus its grouped-drag persistence tests.