···11+# Sidebar Container Refactor Plan
22+33+Status: planning
44+Issue: [#249](https://github.com/onevcat/Prowl/issues/249)
55+Related: [#222](https://github.com/onevcat/Prowl/issues/222)
66+77+## Goal
88+99+Refactor the repository sidebar so each repository behaves as one stable visual and drag unit, while worktrees remain selectable, reorderable, and efficient to update.
1010+1111+This should fix the structural mismatch where the app treats repositories as reorderable units but SwiftUI `List` sees repository headers and worktree rows as separate rows. That mismatch shows up as:
1212+1313+- incorrect repository drag insertion indicators when dragging downward across expanded repositories
1414+- unstable bulk expand/collapse animations
1515+- potential sidebar flicker during drag when live terminal / notification / ordering updates arrive
1616+1717+## Current Findings
1818+1919+### 1. Repository sections are not actual list rows
2020+2121+`SidebarListView` renders repositories through an outer `ForEach(...).onMove`.
2222+2323+`RepositorySectionView` then returns:
2424+2525+```swift
2626+Group {
2727+ header
2828+ .tag(SidebarSelection.repository(repository.id))
2929+ if isExpanded {
3030+ WorktreeRowsView(...)
3131+ }
3232+}
3333+```
3434+3535+In practice, the outer data model says "repository row", but `List` receives separate rows:
3636+3737+```text
3838+Repository A header
3939+ Repository A worktree
4040+ Repository A worktree
4141+Repository B header
4242+ Repository B worktree
4343+```
4444+4545+This explains the observed downward-drag indicator bug:
4646+4747+```text
4848+Target Repo header
4949+o-----------
5050+Target Repo worktree
5151+```
5252+5353+SwiftUI is placing the indicator between list rows. It does not know that the target repository header and its worktree rows should be treated as one repository-level drop zone.
5454+5555+### 2. `List(selection:)` is doing too much
5656+5757+The current `List` carries several behaviors at once:
5858+5959+- special rows: Canvas, Shelf, archived worktrees, repository list header
6060+- repository row selection for plain folders
6161+- worktree multi-selection
6262+- repository expand/collapse
6363+- native repository reorder
6464+- native worktree reorder for pinned and unpinned groups
6565+- reveal-in-sidebar via `ScrollViewReader.scrollTo`
6666+- native sidebar styling and accessibility
6767+6868+This makes local fixes brittle because changing row structure affects selection, drag, animation, and scroll identity at the same time.
6969+7070+### 3. Live state still reaches rows during drag
7171+7272+Some expensive state reads have already been isolated, such as moving repository tab-count reads into `RepoHeaderTabCountBadge`.
7373+7474+Remaining drag-time churn sources include:
7575+7676+- `WorktreeRowsView` animates changes to `rowIDs`
7777+- each worktree row reads terminal notification/task/run-script state
7878+- notification-driven reorder can call `withAnimation(.snappy)` and mutate `worktreeOrderByRepository`
7979+- row hover/action UI changes while a drag session is active
8080+8181+These are not necessarily the root cause of the drop-indicator bug, but they are credible contributors to #222-style flicker.
8282+8383+## Recommended Direction
8484+8585+Use a custom sidebar scroll container rather than trying to keep the current flat `List` structure.
8686+8787+Recommended shape:
8888+8989+```text
9090+ScrollViewReader
9191+└── ScrollView
9292+ └── LazyVStack or VStack
9393+ ├── Special rows
9494+ ├── RepositoryContainerRow
9595+ │ ├── RepositoryHeaderRow
9696+ │ └── WorktreeRows
9797+ └── FailedRepositoryRow
9898+```
9999+100100+Key property: repository containers are the only repository-level siblings in the outer stack. Expanded worktrees are children inside the container, not siblings beside it.
101101+102102+This aligns UI boundaries with model boundaries:
103103+104104+- repository reorder indicators target repository containers
105105+- expand/collapse animates inside a container
106106+- worktree reorder indicators target worktree rows inside one container
107107+- live worktree updates do not change the outer repository list shape
108108+109109+## Options Considered
110110+111111+### Option A: Keep `List`, wrap worktrees inside one repository row
112112+113113+Pros:
114114+115115+- preserves some native sidebar styling
116116+- repository-level `onMove` might remain mostly native
117117+118118+Cons:
119119+120120+- nested selectable worktree rows inside a single `List` row no longer participate naturally in `List(selection:)`
121121+- worktree-level `onMove` becomes awkward inside a row
122122+- native selection and keyboard behavior still need replacement
123123+- likely keeps a hard-to-debug mix of native and custom drag logic
124124+125125+This option reduces the indicator bug but does not cleanly solve the broader sidebar design.
126126+127127+### Option B: Move fully to `ScrollView` + explicit rows
128128+129129+Pros:
130130+131131+- model and visual structure match
132132+- repo and worktree drag/drop can be made explicit and testable
133133+- selection, focus, and reveal behavior are owned by our code instead of `List` side effects
134134+- easier to freeze drag-time updates intentionally
135135+- eliminates `List` cell reuse as a class of expand/collapse animation bugs
136136+137137+Cons:
138138+139139+- must replace native `List(selection:)`
140140+- must rebuild keyboard navigation, multi-selection, reorder, and accessibility affordances
141141+- more implementation work
142142+143143+This is the recommended route for #249 if the goal is "fix the sidebar design once" rather than patch one symptom.
144144+145145+### Option C: Short-term drag-time freeze only
146146+147147+Pros:
148148+149149+- small
150150+- may help #222 flicker
151151+152152+Cons:
153153+154154+- does not fix repository insertion indicator because row boundaries remain wrong
155155+- leaves the main structural mismatch in place
156156+157157+This can be kept as a subset of Option B, but it is not enough alone.
158158+159159+## Proposed Architecture
160160+161161+### SidebarPresentationModel
162162+163163+Introduce a pure presentation model that flattens current repository state into explicit sidebar units.
164164+165165+Suggested model:
166166+167167+```swift
168168+struct SidebarPresentation: Equatable {
169169+ var items: [SidebarItem]
170170+}
171171+172172+enum SidebarItem: Equatable, Identifiable {
173173+ case listHeader(SidebarListHeaderModel)
174174+ case special(SidebarSpecialRowModel)
175175+ case repository(SidebarRepositoryContainerModel)
176176+ case failedRepository(FailedRepositoryModel)
177177+}
178178+179179+struct SidebarRepositoryContainerModel: Equatable, Identifiable {
180180+ var repositoryID: Repository.ID
181181+ var title: String
182182+ var rootURL: URL
183183+ var kind: Repository.Kind
184184+ var isExpanded: Bool
185185+ var isRemoving: Bool
186186+ var worktreeSections: WorktreeRowSections
187187+}
188188+```
189189+190190+Rules:
191191+192192+- outer `items` contains one item per repository, not one item per row
193193+- worktree sections remain inside the repository container
194194+- presentation construction should be pure and unit-tested
195195+- live terminal state should not be part of the broad presentation model unless required for layout identity
196196+197197+### Selection
198198+199199+Replace `List(selection:)` with explicit selection handling.
200200+201201+Keep `RepositoriesFeature.State.selection` and `sidebarSelectedWorktreeIDs` as the source of truth, but route clicks through helper functions:
202202+203203+- repository header click:
204204+ - plain folder: select repository
205205+ - git repository: toggle expand by default, or select repository if a future repository-detail mode needs it
206206+- worktree row click:
207207+ - normal click: select worktree and focus terminal
208208+ - Cmd-click: toggle multi-selection
209209+ - Shift-click: optional follow-up, only if current behavior supports it through `List`
210210+- Canvas / Shelf / Archived rows: dispatch existing actions
211211+212212+Selection visuals should be explicit in `RepositoryHeaderRow` and `WorktreeRow`, not inherited from `List`.
213213+214214+### Keyboard Navigation
215215+216216+Preserve the existing command actions first:
217217+218218+- `selectNextWorktree`
219219+- `selectPreviousWorktree`
220220+- `revealSelectedWorktreeInSidebar`
221221+- numbered hotkeys
222222+223223+Do not try to rebuild full Finder-like keyboard navigation in the first pass unless it is currently user-visible and relied upon.
224224+225225+Required V1 behavior:
226226+227227+- command shortcuts still select worktrees
228228+- selected row is scrolled into view on reveal
229229+- focus returns to terminal after single worktree selection
230230+- sidebar focus does not accidentally forward text while Canvas / Shelf rules say it should not
231231+232232+### Repository Reorder
233233+234234+Replace `ForEach(...).onMove` with explicit repository drag/drop.
235235+236236+Suggested approach:
237237+238238+- make `RepositoryContainerRow` draggable with repository ID payload
239239+- render a custom repository insertion indicator between repository containers
240240+- compute drop destination as a repository index
241241+- dispatch existing `.worktreeOrdering(.repositoriesMoved(offsets, destination))` or a new clearer action such as `.repositoriesReordered([Repository.ID])`
242242+243243+The custom indicator should always render at repository container boundaries:
244244+245245+```text
246246+Target Repo header
247247+ Target Repo worktree
248248+o-----------
249249+```
250250+251251+This directly fixes the current downward-drag indicator bug.
252252+253253+### Worktree Reorder
254254+255255+Keep worktree reorder scoped inside one repository container.
256256+257257+Suggested approach:
258258+259259+- worktree rows are draggable with worktree ID payload
260260+- pinned and unpinned sections keep separate drop zones
261261+- main and pending rows remain non-movable
262262+- drop destination maps to existing reducer actions:
263263+ - `.pinnedWorktreesMoved(repositoryID, offsets, destination)`
264264+ - `.unpinnedWorktreesMoved(repositoryID, offsets, destination)`
265265+266266+Cross-repository worktree drag can stay out of scope. The current model does not appear to support moving worktrees between repositories.
267267+268268+### Drag-Time Freeze
269269+270270+Add a small sidebar drag state to suppress non-essential row churn.
271271+272272+During any sidebar drag:
273273+274274+- freeze hover-only row actions
275275+- hide pull request / notification popover affordances that resize rows
276276+- suppress row-ID animations caused by notification-driven reorder
277277+- defer "move notified worktree to top" until drag ends, or apply it without animation after drop
278278+279279+This addresses #222 without requiring every live data read to stop.
280280+281281+### Expand / Collapse
282282+283283+Move expand/collapse animation into `RepositoryContainerRow`.
284284+285285+Rules:
286286+287287+- outer repository container identity must not change when worktrees appear/disappear
288288+- single repo expand/collapse animates child rows inside the container
289289+- bulk expand/collapse updates many containers, but the outer stack still has stable repository items
290290+- avoid animating row identity and live status changes in the same transaction
291291+292292+### Reveal In Sidebar
293293+294294+`ScrollViewReader.scrollTo` can still work, but scroll IDs must be explicit:
295295+296296+- repository container: `SidebarScrollID.repository(repositoryID)`
297297+- worktree row: `SidebarScrollID.worktree(worktreeID)`
298298+- special rows: `SidebarScrollID.canvas`, etc.
299299+300300+When revealing a collapsed worktree:
301301+302302+1. expand its repository
303303+2. yield for layout materialization
304304+3. scroll to `SidebarScrollID.worktree(worktreeID)`
305305+4. consume pending reveal
306306+307307+This matches the current two-yield approach but removes dependency on `List` row materialization.
308308+309309+### Accessibility
310310+311311+Minimum accessibility requirements:
312312+313313+- repository headers expose button/row labels and expanded state
314314+- worktree rows expose selection state
315315+- drag handles or rows expose reorder affordance where AppKit/SwiftUI can support it
316316+- Canvas / Shelf / Archived rows keep meaningful labels
317317+318318+If full native `List` accessibility cannot be matched in V1, document the gap and keep keyboard command coverage strong.
319319+320320+## Implementation Plan
321321+322322+### Phase 0: Baseline and Guardrails
323323+324324+- Add a short manual repro checklist for:
325325+ - repository drag up/down over expanded target
326326+ - bulk expand/collapse with many repositories
327327+ - worktree reorder in pinned/unpinned groups
328328+ - sidebar multi-selection
329329+ - reveal-in-sidebar
330330+- Add signposts around sidebar presentation build and drag state transitions if trace work is needed.
331331+- Keep current `List` code untouched until presentation tests exist.
332332+333333+### Phase 1: Pure Presentation and Reorder Mapping
334334+335335+Files likely involved:
336336+337337+- `supacode/Features/Repositories/Models/SidebarPresentation.swift` (new)
338338+- `supacodeTests/SidebarPresentationTests.swift` (new)
339339+- existing reducer ordering tests
340340+341341+Deliver:
342342+343343+- pure sidebar presentation builder
344344+- stable scroll IDs
345345+- pure drop-destination mapping for repository and worktree reorder
346346+- tests for:
347347+ - expanded repository keeps one outer item with child rows
348348+ - failed repositories preserve order
349349+ - plain folders produce repository containers with no worktree children
350350+ - pinned/main/pending/unpinned sections are preserved
351351+ - repository drop destinations map to expected order
352352+ - worktree drop destinations map within pinned/unpinned sections
353353+354354+### Phase 2: New Container Views Behind a Switch
355355+356356+Files likely involved:
357357+358358+- `SidebarListView.swift`
359359+- `RepositorySectionView.swift`
360360+- `WorktreeRowsView.swift`
361361+- new `SidebarContainerListView.swift`
362362+- new `RepositoryContainerRow.swift`
363363+364364+Deliver:
365365+366366+- render the new container sidebar behind a local compile-time or private runtime switch
367367+- no reducer changes except new presentation helpers if needed
368368+- preserve row styling visually before enabling custom drag/drop
369369+370370+This phase should be screenshot/manual verified before deleting the old `List` path.
371371+372372+### Phase 3: Explicit Selection and Reveal
373373+374374+Deliver:
375375+376376+- click handling for repository and worktree rows
377377+- explicit selection visuals
378378+- multi-selection behavior matching current sidebar expectations
379379+- reveal-in-sidebar via new scroll IDs
380380+- focused terminal handoff after single worktree selection
381381+382382+Tests:
383383+384384+- pure selection helper tests if logic is factored out
385385+- existing reducer selection tests should keep passing
386386+387387+### Phase 4: Custom Repository Reorder
388388+389389+Deliver:
390390+391391+- repository drag payload
392392+- custom repo-level insertion indicator
393393+- drop handling that dispatches repository reorder
394394+- drag-time UI freeze for non-essential row affordances
395395+396396+Manual verification:
397397+398398+- dragging a repository upward shows indicator below the target repository container when appropriate
399399+- dragging a repository downward never shows the indicator between target header and target worktree rows
400400+- failed repository rows either reorder correctly or are explicitly non-reorderable
401401+402402+### Phase 5: Custom Worktree Reorder
403403+404404+Deliver:
405405+406406+- pinned/unpinned scoped worktree drop zones
407407+- custom worktree insertion indicator
408408+- main/pending rows stay non-movable
409409+- existing persistence paths remain unchanged
410410+411411+Manual verification:
412412+413413+- pinned worktree reorder persists
414414+- unpinned worktree reorder persists
415415+- dragging over main/pending rows does not create invalid moves
416416+417417+### Phase 6: Remove Old `List` Path and Polish
418418+419419+Deliver:
420420+421421+- delete old `List(selection:)` implementation
422422+- remove obsolete `RepositorySectionView` / `WorktreeRowsView` pieces or fold them into new components
423423+- final accessibility pass
424424+- final animation pass for bulk expand/collapse
425425+- update issue #249 with final implementation notes
426426+427427+## Verification Matrix
428428+429429+Automated:
430430+431431+- `SidebarPresentationTests`
432432+- existing `RepositoriesFeatureTests` ordering tests
433433+- existing `RepositorySectionViewTests` migrated or renamed
434434+- `make check`
435435+- `make build-app`
436436+437437+Manual:
438438+439439+1. Select a plain folder repository row.
440440+2. Select a git repository worktree row and confirm terminal focus.
441441+3. Cmd-click multiple worktree rows and confirm bulk archive/delete commands still target selected rows.
442442+4. Expand/collapse one repository.
443443+5. Bulk expand/collapse at least 10 repositories.
444444+6. Drag repository upward and downward across expanded repositories.
445445+7. Drag pinned worktrees within a repository.
446446+8. Drag unpinned worktrees within a repository.
447447+9. Trigger reveal-in-sidebar from Canvas or command.
448448+10. Verify Canvas / Shelf / Archived rows remain selectable.
449449+11. Verify notification/task/run-script indicators update without moving rows during a drag.
450450+451451+## Risks
452452+453453+### Native `List` behavior loss
454454+455455+Risk: custom scroll rows may lose some free AppKit sidebar behavior.
456456+457457+Mitigation:
458458+459459+- preserve command-based navigation first
460460+- add explicit accessibility labels/traits
461461+- keep manual keyboard/accessibility checklist
462462+463463+### Reorder implementation complexity
464464+465465+Risk: custom drag/drop can become more complex than native `.onMove`.
466466+467467+Mitigation:
468468+469469+- keep pure drop-index mapping tested
470470+- keep repository reorder and worktree reorder separate
471471+- defer cross-repository worktree moves
472472+473473+### UI regressions from broad rewrite
474474+475475+Risk: replacing the sidebar in one PR touches selection, animation, and drag.
476476+477477+Mitigation:
478478+479479+- stage behind a private switch until visual behavior is verified
480480+- land presentation model tests first
481481+- keep reducer actions and persistence shape stable
482482+483483+### Performance regressions
484484+485485+Risk: replacing lazy `List` with `VStack` could render too much.
486486+487487+Mitigation:
488488+489489+- start with `LazyVStack`
490490+- switch only repository containers to non-lazy child stacks if expand/collapse animation needs it
491491+- measure with the existing signpost / Instruments workflow if the sidebar feels worse
492492+493493+## Recommendation
494494+495495+Proceed with Option B as the #249 plan: a custom `ScrollView` sidebar with repository containers as outer items.
496496+497497+Do not attempt to fix the repository insertion indicator through reducer index changes. The indicator is a symptom of the current `List` row structure, not the persisted ordering logic.
498498+499499+The safest execution path is:
500500+501501+1. pure presentation model and tests
502502+2. render-only new sidebar path
503503+3. explicit selection/reveal
504504+4. custom repository reorder
505505+5. custom worktree reorder
506506+6. remove old `List` path
507507+508508+This is larger than a tactical #222 fix, but it addresses the underlying sidebar design mismatch and gives future sidebar features a cleaner foundation.