Skip to content

Add ⌘P worktree switcher (VS Code-style) with worktree MRU and direct-beats-fuzzy ranking#608

Merged
sbertix merged 8 commits into
supabitapp:mainfrom
COCPORN:pr/project-switcher
Jul 12, 2026
Merged

Add ⌘P worktree switcher (VS Code-style) with worktree MRU and direct-beats-fuzzy ranking#608
sbertix merged 8 commits into
supabitapp:mainfrom
COCPORN:pr/project-switcher

Conversation

@COCPORN

@COCPORN COCPORN commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Closes #592

What

Adds a ⌘P worktree switcher (VS Code-style quick-open) and moves the existing Command Palette to ⌘⇧P.

Why this binding

  • ⌘⇧P → Command Palette — exact VS Code match.
  • ⌘P → fast navigation — VS Code's ⌘P jumps between files; here it jumps between worktrees.

Both are rebindable or disablable in Settings → Keyboard Shortcuts → General, so this only changes the defaults.

Behavior

  • ⌘P opens a fuzzy switcher of worktrees, MRU-ordered. The current worktree is shown, but the default selection lands on the previous one — ⌘P then Enter is a Cmd+Tab-style toggle. Typing snaps to the top fuzzy match.
  • Rows read as a hierarchy: worktree name as the title, repo name as a quieter subtitle. The scorer matches both, so a query hits either the worktree or the project name.
  • MRU is recorded on both navigation paths (sidebar clicks and hotkeys/palette), falling back to main/first.
  • Cancelling the palette refocuses the current terminal.

Ranking — fuzzy search fixed

The fuzzy search that misbehaved in the earlier iteration of this PR is fixed. The shared scorer now guarantees direct beats fuzzy: a contiguous prefix/substring hit (including a clean repo-subtitle hit) always outranks a scattered subsequence hit — tiers are clamped so per-character bonuses can't bleed across a tier boundary. Recency (MRU) is lifted above match-spread so equal-quality matches resolve to the most-recently-used row. Pinned by directSubtitleMatchOutranksScatteredTitleMatch and contiguousSubstringOutranksScatteredSubsequence.

Tests

CommandPaletteFeatureTests + RepositoriesFeatureWorktreeMRUTests — switcher ordering/flagging, defaultIndex seeding, sidebar-path MRU, and the ranking tiers. This PR's CI runs make test.

Note on the extra commits

This branch also carries two fork-only CI commits (Skip Tuist auth on forks, Add fork-build CI…) that let the fork build an installable artifact where local builds are impossible. Happy to split them out if you'd rather keep this PR switcher-only.

Disclosure

Implemented with AI assistance (Claude Code). Scorer ranking logic verified locally; the full build + test suite runs via this PR's CI.


Supersedes #370 (that thread got tangled after a force-push and won't reopen cleanly).

Adds a worktree switcher on ⌘P and moves the existing Command Palette to ⌘⇧P,
aligning with VS Code (⌘⇧P = Command Palette; ⌘P = fast navigation). Both
shortcuts remain user-rebindable via shortcut overrides.

Reworked from the original project switcher per the PR supabitapp#370 review: Supacode's
core use case is multi-worktree-per-repo, where a project-level switcher
collapses the whole history down to one entry per repo and loses the worktree
you actually had open. Worktrees are the primary rows instead.

- ⌘P opens a fuzzy switcher of every worktree, ordered most-recently-used
  (RepositoriesFeature.State.worktreeMRU). The current worktree is rendered (so
  you see where you are) but flagged `isCurrentWorktree` so the default
  selection lands on the *previous* worktree — ⌘P then Enter is a Cmd+Tab-style
  toggle. Typing fuzzy-matches the combined `repo / worktree` title, so a query
  hits either the project name or the worktree name.
- worktreeMRU is recorded on BOTH navigation paths — setSingleWorktreeSelection
  (hotkeys/palette) and reduceSelectionChangedEffect (sidebar clicks) — so it
  tracks however you navigate. Single-worktree repos behave exactly as the
  original project switcher did.
- New PaletteMode (.commands / .worktreeSwitcher). The switcher emits
  `selectWorktree` directly (no project→worktree resolution step), so activation
  lands focus in the chosen terminal; cancelling refocuses the current terminal.

Covered by CommandPaletteFeatureTests (switcher ordering/flagging, MRU sort,
combined title, mode dispatch) and RepositoriesFeatureWorktreeMRUTests (MRU
recording on both nav paths).

Implemented with AI assistance (Claude Code).
The worktree switcher records every concrete selection into the new
`worktreeMRU` state on both nav paths (setSingleWorktreeSelection and
reduceSelectionChangedEffect). TestStore checks state exhaustively, so the 27
selection/history tests that drive those paths now see an unasserted change.
Assert the new MRU head in each — selecting worktree W prepends W; nil/archived
selections leave it untouched (those tests already pass and are unchanged).

Behavior change is intentional (the switcher's whole point), so the tests follow.
Two bits of feedback from running the build:

- The query placeholder was hardcoded "Search for actions or branches…" — stale
  now that ⌘P is worktree-only (actions live on ⌘⇧P). Make it mode-aware:
  "Go to worktree…" for the switcher, the actions wording for the command palette.
- Switcher rows rendered one flat `repo / worktree` string at a single weight, so
  the worktree name was hard to pick out from the repo. Split them: worktree name
  is the title, repo is the secondary subtitle (VS Code "filename big, path small").
  The fuzzy scorer matches title and subtitle, so a query still hits either name.
  Folder rows keep the repo name alone (no redundant subtitle).

The ⌘⇧P command palette is unchanged (still the flat `repo / worktree` rows
alongside actions).
The shared CommandPaletteFuzzyScorer had two flaws the worktree switcher
surfaced:

- No tier between "prefix" and "scattered fuzzy", so a contiguous substring
  hit and a scattered subsequence hit landed in the same band — and the
  per-character position-0/separator bonuses could push a scattered match
  past a mid-word direct match. Added a contiguous-substring tier (title) and
  a direct (prefix/substring) tier for the subtitle, with every in-tier score
  clamped so it can never bleed into the tier above. Net: a direct match —
  including a clean repo (subtitle) hit — always outranks a scattered fuzzy
  one. This also undoes the regression from splitting switcher rows into
  worktree-title + repo-subtitle, where a fuzzy worktree hit buried an exact
  repo hit (subtitle matches previously scored in a no-threshold low band).

- Recency (MRU) was a near-last tiebreaker, below match-spread. Lifted it
  above matchDistance so once match quality (the score tier) is equal, the
  most-recently-used row wins — the right default for a navigation surface.

Pinned with directSubtitleMatchOutranksScatteredTitleMatch and
contiguousSubstringOutranksScatteredSubsequence (both fail on the old scorer).
The `setup-macos` Tuist auth step was gated to skip only cross-fork PRs, so an
intra-fork PR (or a push to a fork's main) still tried `tuist auth login` and
failed with `notAuthenticated` / "No projects linked to the repository" — the
fork isn't linked in the Tuist dashboard. That blocked the whole build job
before lint/build/test ran.

Tuist auth is OIDC against the canonical upstream project and only buys the
remote cache; `tuist generate` (and thus the build) works without it — the
cross-fork-PR path already relies on this. Gate auth on the upstream repo so
forks (and cross-fork PRs) skip it and can run the full build+test job.
Local builds are impossible on this Mac (Zig/libSystem), and test.yml builds but
never uploads the app, so a fork has no way to get a runnable build. Add a
`make package-app` target (zips the Debug build via the same -showBuildSettings
path resolution as run-app/install-dev-build) and a `fork-build.yml` workflow on
feat/** | fix/** | exp/** that builds, packages, and uploads `supacode-<sha>`.
No tests, no signing — just a downloadable .app.zip for local testing.
@sbertix

sbertix commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Thank you 🙇‍♂️ I'll try to review this asap. Could you resolve the conflicts too while we're at it, please?

This branch also carries two fork-only CI commits (Skip Tuist auth on forks, Add fork-build CI…) that let the fork build an installable artifact where local builds are impossible. Happy to split them out if you'd rather keep this PR switcher-only.

Yeah, tbh I wouldn’t add those at all, not just here. For one, because it’s probably better left to the “internals” of forks themselves, so it can be tweaked and tailored to a specific “taste.” And also because the implication of “loosening” local verification even more is pretty scary to me 😅

sbertix added 2 commits July 12, 2026 17:49
…worktree-switcher-vs-code-style-wi

# Conflicts:
#	supacode/App/ContentView.swift
#	supacode/Features/CommandPalette/Views/CommandPaletteOverlayView.swift
#	supacode/Features/Repositories/Views/ToolbarStatusView.swift
#	supacode/Features/Repositories/Views/WorktreeDetailView.swift
…MRU fixes

Render the worktree switcher in the glass command-palette panel and keep ⌘⇧P
listing actions only, since worktree navigation is the switcher's job.

Style each switcher row like the sidebar: tint the worktree title and repo
subtitle from the sidebar section and row colors, show a distinct wifi badge
for a remote host, resolve folder rows to their custom name (falling back to
the row's own name) with no subtitle, and draw the sidebar's leading glyph
(branch or pull-request icon, folder, or missing) plus its CI check badge at
the sidebar's resting opacity. A pull request whose head branch no longer
matches the worktree falls back to the branch glyph.

Promote a direct subtitle match above a scattered title match in the fuzzy
scorer so an exact repo-name hit outranks a fuzzy worktree-name hit.

Keep the worktree MRU correct: prune it in the shared removeWorktree primitive
so every delete path drops the entry, keep transient pending-creation ids out
of it, and cap it at the history-stack limit, so a reused path can't inherit a
removed worktree's rank.

Pass the active mode to the panel's keyboard-navigation filter so arrow keys
and ⌘1-⌘5 drive the switcher rows, and force the command palette mode for
Ghostty's toggle action so it never reopens the last-used surface.

@sbertix sbertix left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 🙇‍♂️

@sbertix
sbertix enabled auto-merge (squash) July 12, 2026 17:52
@sbertix
sbertix merged commit c3bdb8e into supabitapp:main Jul 12, 2026
3 checks passed
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.

Recent picker for worktrees and tabs (JetBrains "Recent Files"-style, recency-ordered switcher)

3 participants