Add ⌘P worktree switcher (VS Code-style) with worktree MRU and direct-beats-fuzzy ranking#608
Merged
Merged
Conversation
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.
Collaborator
|
Thank you 🙇♂️ I'll try to review this asap. Could you resolve the conflicts too while we're at it, please?
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 😅 |
…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
enabled auto-merge (squash)
July 12, 2026 17:52
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.
Closes #592
What
Adds a ⌘P worktree switcher (VS Code-style quick-open) and moves the existing Command Palette to ⌘⇧P.
Why this binding
Both are rebindable or disablable in Settings → Keyboard Shortcuts → General, so this only changes the defaults.
Behavior
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
directSubtitleMatchOutranksScatteredTitleMatchandcontiguousSubstringOutranksScatteredSubsequence.Tests
CommandPaletteFeatureTests+RepositoriesFeatureWorktreeMRUTests— switcher ordering/flagging, defaultIndex seeding, sidebar-path MRU, and the ranking tiers. This PR's CI runsmake 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).