Skip to content

Nest sidebar worktrees by branch with onboarding card#324

Merged
sbertix merged 5 commits into
mainfrom
sbertix/collapsible-path-components
May 16, 2026
Merged

Nest sidebar worktrees by branch with onboarding card#324
sbertix merged 5 commits into
mainfrom
sbertix/collapsible-path-components

Conversation

@sbertix
Copy link
Copy Markdown
Collaborator

@sbertix sbertix commented May 16, 2026

Summary

  • Group sidebar worktrees by /-separated branch components into collapsible nested headers (e.g. feature/tools/api and feature/tools/web land under a single feature/tools group). Default ON, togglable from View → Nest Worktrees by Branch. Per-prefix collapse state persists in sidebar.json. Drag-to-reorder is suppressed while nesting is on; the original custom order is restored verbatim when nesting is toggled off.
  • Add a generic priority-based pinned bottom-of-sidebar card framework (SidebarCard + SidebarBottomCardView.Slot coordinator) and a low-priority NestedWorktreesOnboardingCardView that teaches the toggle location. The card auto-permadismisses when the user toggles nesting off via the View menu, regardless of whether the sidebar column is visible. Refactors the existing coding-agents card onto the new framework.
  • Hotkey worktree selection (Cmd+1..9, arrow nav, menu-bar Select Worktree submenu) now walks the same visible row order the sidebar renders: main → pinned-tail → pending → unpinned-tail, with the trie applied to the tail runs when nesting is on. Arrow nav from a row that's hidden behind a collapsed group lands on the nearest visible neighbor in the direction of travel.

Other changes

  • Per-leaf scoped reads for branchName + indicator aggregation in the sidebar, with a new Sidebar performance section in AGENTS.md codifying the rule.
  • Case-sensitive branch grouping (git refs are case-sensitive); collapsed-prefix set is pruned against the live branch list on every reconcile and lossy-decoded so a malformed payload can't nuke the sidebar layout.
  • Reveal in Sidebar / deeplink selection now expands any collapsed ancestor prefix so the row never lands on an invisible target.
  • Typed SharedReaderKey.sidebarNestWorktreesByBranch extension; new AGENTS.md rule banning top-level free functions in favor of static members on caseless enums.

Test plan

  • Toggle View → Nest Worktrees by Branch off and on, confirm custom drag order is preserved across the toggle.
  • Collapse a group header, switch to another worktree, return: state persisted across launches.
  • Hotkey Cmd+1..9 and arrow nav skip rows inside collapsed groups; arrow nav from a hidden selection lands on the nearest visible neighbor.
  • Reveal in Sidebar (hotkey + command palette + deeplink) auto-expands any collapsed ancestor prefix.
  • Onboarding card appears on first launch; dismissing via X or toggling nesting off (even with sidebar hidden) hides it permanently.
  • Coding-agents card priority still wins over the onboarding card.
  • Folder (non-git) repositories are unaffected.

sbertix added 5 commits May 16, 2026 11:44
Adds a View menu toggle ("Nest Worktrees by Branch", default on) that
visually nests sidebar worktrees by the / components of their branch
names. Headers collapse/expand with animation; collapsed headers
aggregate notification, running-script, and agent indicators from
their descendants. Collapse state persists per repo and per bucket in
sidebar.json. While nesting is on, rows sort alphabetically and drag
reorder is suppressed for affected buckets; toggling off restores the
custom order and collapse state survives.

Extracts the shared sidebar ping-dot views into their own file so the
leaf row and the group header share one source of truth, and fixes a
pre-existing hardcoded lineWidth in those types to use pixelLength.
…onboarding

Introduces a generic SidebarCard primitive plus a SidebarBottomCardView
coordinator that hosts the pinned bottom-of-sidebar slot one card at a
time. Refactors the existing coding-agents card onto the framework and
exposes a static resolveMode(...) so the coordinator can probe it
without rendering.

Adds a new low-priority onboarding card teaching the new branch-nesting
default: it shows when nesting is on, points at the menu location for
the toggle (no inline disable button, the friction is intentional),
and treats both the dismiss X and toggling nesting off via the View
menu as permanent dismissals so re-enabling nesting later doesn't
bring the prompt back.
Sidebar branch-nesting
- Scope branchName fan-out per-leaf in SidebarItemGroupView so trie
  rebuilds no longer re-fire on unrelated leaf ticks (notification,
  agent storm, running-script update).
- Auto-uncollapse ancestor prefixes on Reveal in Sidebar / deeplink
  selection so the row never lands behind a collapsed group header.
- Prune collapsedBranchPrefixes against the live branch set during
  sidebar reconcile so dead entries don't accumulate in sidebar.json
  across worktree rename / removal.
- Reject .archived bucket and unknown-repo IDs in
  branchNestExpansionChanged so stale UI / deeplinks can't write
  phantom collapse state.
- Wrap collapsedBranchPrefixes decode in try? so a malformed value
  drops just the field instead of nuking the whole sidebar layout.
- Add ancestorPrefixes(of:) helper plus tests covering case-sensitive
  grouping (Feature/x and feature/x stay distinct), reveal expansion,
  reducer guards, and malformed-payload decode.

Branch-nesting code organization
- Move buildRows / aggregateIndicators / Row / GroupIndicators into a
  caseless enum SidebarBranchNesting namespace.
- Aggregator view collects scoped leaf reads into LeafIndicatorSnapshot
  values and delegates to the pure aggregateIndicators(from:), so the
  tested algorithm is the one production runs.
- Move sidebarNestIndentStep into SidebarNestLayout.indentStep.

Bottom-card framework
- Drop SidebarCard.actions slot; unify dismiss API as
  onDismiss: (() -> Void)? so a non-functional X can't ship.
- Share cardRelevantSinceDate gating via SidebarCardRelevance.isDismissed.
- Nest ResolvedCard inside SidebarBottomCardView.Slot. Build
  transitionToken off case names instead of SkillAgent.rawValue and
  preconditionFailure on the unreachable .agent(.hidden) arm.
- Drop dead @Environment(\.backgroundProminence) on the indicator view.

Onboarding card
- Move the toggle-off permadismiss off SidebarBottomCardView.onChange
  and onto the SidebarCommands menu Toggle binding so it still fires
  when the sidebar column is hidden.
- Mention "toggle off to restore custom ordering" in the description.

Docs
- Add a Code Guidelines bullet to AGENTS.md banning top-level free
  functions in favor of static members on caseless enums / extensions.
orderedSidebarItemIDs (which feeds worktreeID(byOffset:), select-next /
select-previous, the worktree shortcut hints, and the menu-bar Select
Worktree submenu) was reading the raw drag-order out of
sidebarGrouping.bucketsByRepository. With branch nesting on, that order
diverges from what the sidebar actually renders: the visible list is
alphabetical and rows inside collapsed group headers are hidden, but
hotkeys still walked the underlying custom order including hidden rows.

Add @shared(.appStorage("sidebarNestWorktreesByBranch")) to
RepositoriesFeature.State so the reducer reads the same toggle the View
menu binds to, then rework orderedSidebarItemIDs to:

- Match the rendered visual order: main, pinned-tail, pending,
  unpinned-tail.
- When nesting is on for a git repo, run the pinned- and unpinned-tail
  runs through SidebarBranchNesting.buildRows and keep only the visible
  .leaf IDs, so hotkeys land on the same row the user sees and skip
  anything inside a collapsed group.
- Fall back to the raw custom drag order when nesting is off.

Add a reducer test that pins all three branches (nesting-on alphabetical
order, collapsed-group skip, nesting-off restores custom order).
Arrow navigation
- When the selected worktree is hidden behind a collapsed group,
  worktreeID(byOffset:) now anchors off the unfiltered ordered list and
  walks toward the nearest visible neighbor in the direction of travel
  instead of jumping to the top / bottom of the list. Adds a private
  ignoreCollapsedGroups: Bool flavor on orderedSidebarItemIDs that
  keeps the visual partition but skips the trie's collapse filtering.
- New test pins both forward and backward nearest-visible-neighbor
  resolution.

Hotkey path hardening
- visibleBranchNestingRowIDs (renamed branchNestingRowIDs to drop the
  redundant "visible") now builds the branchName lookup with
  Dictionary(_, uniquingKeysWith:) instead of uniqueKeysWithValues:
  so a transient duplicate row id during state transitions can't
  trap the arrow-key path.

Slot.transitionToken
- The unreachable .agent(.hidden) arm now returns a stable
  "agent:hidden" string instead of preconditionFailure. A future debug
  surface that constructs the variant directly no longer crashes the
  render path; identity stays distinct so .animation(_:value:) still
  fires correctly.

Code organization
- Move sidebarItemGroups(in:repositoryID:) onto SidebarItemGroup.slots
  and shortcutIndex(for:) onto SidebarShortcutIndex.build per the new
  AGENTS.md "no top-level free functions" rule. The file that adds the
  rule no longer ships its own offenders.
- Introduce a typed SharedReaderKey.sidebarNestWorktreesByBranch
  extension; all four read sites (State, View menu binding, sidebar
  view, bottom-card host) now go through the typed handle so the key
  string + default value can't drift.
- Inline the SidebarBranchNestingRowView construction at the three
  ForEach branches; drop the @ViewBuilder helper method per the
  project rule that SwiftUI subviews must be dedicated View structs.

Docstrings
- orderedSidebarItems vs orderedSidebarItemIDs now cross-reference
  each other and explain why the heavy flavor intentionally surfaces
  the raw curated order regardless of UI collapse state (command
  palette / multi-select consumers want that, not the visible
  alphabetical projection).
- pruneCollapsedBranchPrefixes docstring now narrows its claim and
  explains why a chain-collapsed single-link prefix is intentionally
  preserved (so a future sibling-branch addition pre-seeds the saved
  collapse state).

Tests
- Split the previous omnibus orderedSidebarItemIDs test into three
  focused tests (alphabetizes, skips collapsed groups, restores
  custom order when nesting is off) sharing a single setup helper.
- Add coverage for pending worktrees rendering between pinned-tail
  and unpinned-tail when nesting is on.
@sbertix sbertix enabled auto-merge (squash) May 16, 2026 11:52
@tuist
Copy link
Copy Markdown

tuist Bot commented May 16, 2026

🛠️ Tuist Run Report 🛠️

Builds 🔨

Scheme Status Duration Commit
supacode 1m 28s 4b86c096e

@sbertix sbertix merged commit 0a2548c into main May 16, 2026
2 checks passed
@sbertix sbertix deleted the sbertix/collapsible-path-components branch May 16, 2026 11:58
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