Highlight relevant sidebar rows with Pinned / Active sections#328
Merged
Conversation
Synthetic folder worktrees seed into the `.unpinned` bucket by default and now flow through the shared `pinWorktree` / `unpinWorktree` actions that govern git worktrees. `reconcileSidebarState` skips the `mainID == worktreeID` prune for folder repos so a folder pin survives `.repositoriesLoaded`. The folder row's view path resolves via `Repository.folderWorktreeID(for:)` so it stays visible across pin / unpin transitions, and `.pin` / `.unpin` deeplinks accept folder targets.
Two View-menu toggles under "Group Relevant Sidebar Rows": `@Shared(.sidebarGroupPinnedRows)` and `@Shared(.sidebarGroupActiveRows)`, both default `true` so the feature is discoverable on first launch. Each is independent: turning one off hides only its hoisted section. `SidebarActiveClassification` is a 10-bucket priority enum keyed off `hasUnseenNotifications`, `hasAgentAwaitingInput`, `!agents.isEmpty`, `!runningScripts.isEmpty`. `SidebarHighlightOrdering` owns the priority + alphabetical sort with direct unit coverage. The `hasAgent` flag matches visible agent-badge presence so a row with an agent badge surfaces in Active even when the agent isn't actively working. Replaces `WorktreeRowDisplayMode` with `ResolvedRowDisplay` which the view consults for title / subtitle composition; covered by `ResolvedRowDisplayTests`. Highlight rows get a colored `repo · trail` subtitle with `.layoutPriority(1)` on the trail so the disambiguating worktree name doesn't truncate first under a narrow sidebar. Adds a sidebar bottom-card onboarding entry pointing at the new grouping toggles, dismissable via the menu or by toggling both off.
Hoist the full sidebar layout decision onto `RepositoriesFeature.State`. The view becomes a single `ForEach(structure.sections)` over a flat enum (`.highlight`, `.repository`, `.folder`, `.failedRepository`, `.placeholder`) that the reducer pre-computes through a targeted post-reduce hook. Per-repo slot layout (main / pinnedTail / pending / unpinnedTail) moves into `BusinessLogic/SidebarStructure.swift` with a seen-set dedupe so a pre-existing double-bucket row renders in at most one slot, and every per-repo slot filters against `hoistedRowIDs` so a hoisted git main or pending row never double-renders. The structure also caches `slotByID`, `hotkeySlots`, `repositoryHighlightByID`, and `reorderableRepositoryIDs` so the view does zero derivation. `.deletingScript` rows are excluded from the Active candidate set so a row mid-delete doesn't surface in the rail. The recompute hook is gated by `\.sidebarStructureAutoRecompute` and only fires for actions enumerated in `Action.affectsSidebarStructure`, so tests that don't care about sidebar layout don't need to acknowledge a derived-cache mutation. `SidebarStructureTests.swift` covers the integration boundary: placeholder mode, git main exclusion from Pinned, hotkey dedup, archived filter, per-bucket dedupe, Active classification + per-repo dedup, failed-repo section positioning, and folder hoist drop.
Move the highlight-onboarding auto-dismiss from the menu binding setter to the reducer's `.sidebarGroupingTogglesChanged` handler so any path that mutates the grouping toggles fires the dismiss, not just the menu (deeplinks, defaults edit, programmatic writes). Add `SidebarItemGroup.translateFilteredMove` so `.onMove` offsets emitted against the post-hoisting visible row list translate back to the full bucket order, keeping hoisted siblings' relative positions stable across in-bucket drags. Covers the inclusive upper-bound case where a drag lands on visible's last index without off-by-oneing over a hoisted tail row. `SidebarGroupingDismissTests` scopes `defaultAppStorage = .inMemory` so its @shared(.appStorage) writes don't leak across the suite.
When `Nest Worktrees by Branch` is on, `SidebarBranchNesting.buildRows`
re-sorts each bucket alphabetically by branch name before nesting, but
`SidebarItemGroup.computeSlots` kept bucket order. The mismatch made
⌃1..⌃0 hotkeys (and the focus-scene menu projection) point to bucket
positions while the view rendered alphabetical positions. Mirror the
sort inside `computeSlots` (gated on the per-repo effective
`nestWorktreesByBranch && isGitRepository`) so the structure-derived
`slotByID` / `hotkeySlots` line up with what the user sees.
Route `worktreeID(byOffset:)` through `sidebarStructure.hotkeySlots`
so Select Next / Previous Worktree walks the same visible top-down
order as the hotkeys (hoisted Pinned + Active first, then per-repo
with hoisted rows filtered out). Previously arrow nav walked the raw
worktree list and jumped to the bucket-order neighbor when a row was
hoisted into a highlight section.
Flip `SidebarStructureAutoRecomputeKey.testValue` to `true` so the
post-reduce hook keeps the cache fresh in tests, matching production.
Wire `.createWorktreeInRepository`, `.createRandomWorktreeInRepository`,
and `.sidebarNestByBranchChanged` into `affectsSidebarStructure`.
Update affected TestStore expectations to mirror the recompute via
`$0.reconcileSidebarForTesting()` / `$0.recomputeSidebarStructureIfChanged()`;
two legacy PR-refresh tests opt out via
`withDependencies { $0.sidebarStructureAutoRecompute = false }`.
…smiss - Read the custom repo title (and color) in `computeSidebarStructure` via a shared `Repository.sidebarDisplayName(custom:fallback:)` helper so the hoisted-row subtitle stays in lockstep with `RepoSectionHeaderView`. Add `.repositoryCustomization(.presented(.delegate(.save)))` to `affectsSidebarStructure` so the cache flushes on save instead of waiting for the next unrelated leaf tick. - Add `SidebarItemFeature.State.Lifecycle.isTerminating` and use it in the Active candidate filter so `.archiving` / `.deletingScript` / `.deleting` rows drop out alongside the existing wind-down case. `.pending` stays eligible because a pending row running a setup script is exactly what Active is meant to surface. - Replace `pinnedRows.first.flatMap` in `SidebarItemGroup.computeSlots` with `pinnedRows.first(where: isMainWorktree)` so a corrupted persisted `.pinned` with main at a non-zero index still routes to the main slot instead of double-rendering through `pinnedTail`. Matches the reducer's `orderedPinnedWorktreeIDs(in:)` any-position filter. - Narrow `Action.affectsSidebarStructure` for the `.sidebarItems` arm to the inner cases that actually mutate structure inputs (`lifecycleChanged`, `runningScriptStarted` / `Stopped`, `agentSnapshotChanged`, `terminalProjectionChanged`). Display-only per-leaf actions (diff stats, PR refresh, drag / focus / hint flags) skip the recompute entirely. - Mirror `nestWorktreesToggle`'s setter pattern in `groupPinnedRowsToggle` / `groupActiveRowsToggle` so toggling from the menu bar while the sidebar column is collapsed still dismisses the highlight onboarding card. The reducer handler keeps firing through `SidebarListView.onChange` for the sidebar-visible path. - Drop the unreachable duplicate-trap branch in the `slotByID` loop; `hotkeyOrder` is built from three disjoint sources. Strip the `(C10b)` / `(C9)` spec-slot parentheticals and the multi-line unreachable-case rationale on `.agent(.hidden)`. Trim the now-stale "default false in tests" gating comment. - Style sweep on newly-added lines: drop em dashes, condense the multi-paragraph `recomputeSidebarStructureIfChanged` docstring, and point AGENTS.md at the canonical TestStore mirror rules. Adds structure-level tests for the customization-title flow, the `.archiving` / `.deleting` exclusions, the `.pending` + setup-script inclusion, and the non-zero-index main-worktree scan.
Renders a 6pt orange dot next to the Pinned header and a 6pt blue dot next to the Active header (matching the running-script ping dot size without its animation). Uses the same SwiftUI semantic colors as the unread-notification dot in `SidebarItemView`.
Extract HighlightHoists / RepositorySectionsBuild / HotkeyOrdering helpers so the main entry point stays under SwiftLint's cyclomatic complexity and function-body limits. Rename the nested Section.ID enum to SectionID and the test-only `wt` binding to `feature` to clear the remaining identifier_name / type_name violations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RepositoriesFeature.Stateso per-leaf mutations (notifications, agent storms, running scripts) only invalidate their own row.pinWorktree/unpinWorktreeflow as git worktrees, including delete-script handling and command palette / deeplink reconciliation.Test plan
make test(TCA + sidebar structure + highlight ordering + grouping dismiss suites)make build-app