Skip to content

Highlight relevant sidebar rows with Pinned / Active sections#328

Merged
sbertix merged 8 commits into
mainfrom
sbertix/active-drawer
May 17, 2026
Merged

Highlight relevant sidebar rows with Pinned / Active sections#328
sbertix merged 8 commits into
mainfrom
sbertix/active-drawer

Conversation

@sbertix
Copy link
Copy Markdown
Collaborator

@sbertix sbertix commented May 17, 2026

Summary

  • Add View menu toggles to hoist Pinned and Active rows into dedicated sidebar sections, with an onboarding card and steady color dots beside each section header.
  • Cache the derived sidebar structure on RepositoriesFeature.State so per-leaf mutations (notifications, agent storms, running scripts) only invalidate their own row.
  • Make folder (non-git) repositories pinnable through the same pinWorktree / unpinWorktree flow as git worktrees, including delete-script handling and command palette / deeplink reconciliation.
  • Respect the visible sidebar order (highlight hoists, branch nesting, alphabetical sort) for ⌃1..⌃0 hotkeys and arrow navigation.

Test plan

  • make test (TCA + sidebar structure + highlight ordering + grouping dismiss suites)
  • make build-app
  • Toggle Group Pinned / Group Active independently from View menu; verify sections appear only when populated and the onboarding card auto-dismisses when both are off
  • Pin a folder repo, restart, confirm it stays pinned and is not double-rendered
  • With nesting enabled, verify ⌃1..⌃0 numbering matches the visible row order
  • Run a script on a hoisted row and confirm only that row re-renders

sbertix added 7 commits May 17, 2026 13:16
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`.
@sbertix sbertix enabled auto-merge (squash) May 17, 2026 12:57
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.
@tuist
Copy link
Copy Markdown

tuist Bot commented May 17, 2026

🛠️ Tuist Run Report 🛠️

Builds 🔨

Scheme Status Duration Commit
supacode 1m 45s ae8acc2dc

@sbertix sbertix merged commit 0b66caf into main May 17, 2026
2 checks passed
@sbertix sbertix deleted the sbertix/active-drawer branch May 17, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant