diff --git a/supacode/App/AppShortcuts.swift b/supacode/App/AppShortcuts.swift index f38d164c..53c3e0a4 100644 --- a/supacode/App/AppShortcuts.swift +++ b/supacode/App/AppShortcuts.swift @@ -238,12 +238,8 @@ enum AppShortcuts { static let selectPreviousWorktree = AppShortcut( keyEquivalent: .upArrow, ghosttyKeyName: "arrow_up", modifiers: [.command, .control] ) - static let worktreeHistoryBack = AppShortcut( - keyEquivalent: .leftArrow, ghosttyKeyName: "arrow_left", modifiers: [.command, .option, .control] - ) - static let worktreeHistoryForward = AppShortcut( - keyEquivalent: .rightArrow, ghosttyKeyName: "arrow_right", modifiers: [.command, .option, .control] - ) + static let worktreeHistoryBack = AppShortcut(key: "[", modifiers: [.command, .option]) + static let worktreeHistoryForward = AppShortcut(key: "]", modifiers: [.command, .option]) static let selectWorktree1 = AppShortcut(key: "1", modifiers: [.control]) static let selectWorktree2 = AppShortcut(key: "2", modifiers: [.control]) static let selectWorktree3 = AppShortcut(key: "3", modifiers: [.control]) diff --git a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift index 41f47d6a..31a25156 100644 --- a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift +++ b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift @@ -304,7 +304,7 @@ struct RepositoriesFeature { case markWorktreeClosed(Worktree.ID) case setSidebarSelectedWorktreeIDs(Set) case selectRepository(Repository.ID?) - case selectWorktree(Worktree.ID?, focusTerminal: Bool = false) + case selectWorktree(Worktree.ID?, focusTerminal: Bool = false, recordHistory: Bool = true) case selectNextWorktree case selectPreviousWorktree case worktreeHistoryBack @@ -870,10 +870,10 @@ struct RepositoriesFeature { } return .send(.delegate(.selectedWorktreeChanged(state.selectedTerminalWorktree))) - case .selectWorktree(let worktreeID, let focusTerminal): + case .selectWorktree(let worktreeID, let focusTerminal, let recordHistory): let selectWtToken = repositoriesLogger.beginInterval("reducer.selectWorktree") defer { repositoriesLogger.endInterval(selectWtToken) } - setSingleWorktreeSelection(worktreeID, state: &state, recordHistory: true) + setSingleWorktreeSelection(worktreeID, state: &state, recordHistory: recordHistory) if focusTerminal, let worktreeID { state.pendingTerminalFocusWorktreeIDs.insert(worktreeID) } @@ -1553,11 +1553,13 @@ extension RepositoriesFeature.State { } var canNavigateWorktreeHistoryBackward: Bool { - canNavigateWorktreeHistory(stack: worktreeHistoryBackStack) + guard canUseWorktreeHistory else { return false } + return canNavigateWorktreeHistory(stack: worktreeHistoryBackStack) } var canNavigateWorktreeHistoryForward: Bool { - canNavigateWorktreeHistory(stack: worktreeHistoryForwardStack) + guard canUseWorktreeHistory else { return false } + return canNavigateWorktreeHistory(stack: worktreeHistoryForwardStack) } var selectedRepositoryID: Repository.ID? { @@ -1633,6 +1635,10 @@ extension RepositoriesFeature.State { isShelfActive } + private var canUseWorktreeHistory: Bool { + !isShowingShelf && !isShowingCanvas + } + var archivedWorktreeIDSet: Set { Set(archivedWorktrees.map(\.id)) } @@ -2435,7 +2441,7 @@ func shelfBookSelectionEffect( ) -> Effect { switch book.kind { case .worktree: - return .send(.selectWorktree(book.id, focusTerminal: true)) + return .send(.selectWorktree(book.id, focusTerminal: true, recordHistory: false)) case .plainFolder: return .send(.selectRepository(book.repositoryID)) } @@ -2484,6 +2490,7 @@ private func navigateWorktreeHistory( direction: WorktreeHistoryDirection, state: inout RepositoriesFeature.State ) -> Effect { + guard !state.isShowingShelf, !state.isShowingCanvas else { return .none } guard let currentID = state.selectedWorktreeID else { return .none } var sourceStack = direction == .backward @@ -2516,6 +2523,7 @@ private func recordWorktreeHistoryTransition( to nextID: Worktree.ID?, state: inout RepositoriesFeature.State ) { + guard !state.isShowingShelf, !state.isShowingCanvas else { return } guard let previousID, let nextID, previousID != nextID else { return } guard isSelectionValid(previousID, state: state), isSelectionValid(nextID, state: state) else { return } pushWorktreeHistoryID(previousID, onto: &state.worktreeHistoryBackStack) diff --git a/supacodeTests/KeybindingSchemaTests.swift b/supacodeTests/KeybindingSchemaTests.swift index 7399cdbb..452d5959 100644 --- a/supacodeTests/KeybindingSchemaTests.swift +++ b/supacodeTests/KeybindingSchemaTests.swift @@ -56,8 +56,12 @@ struct KeybindingSchemaTests { @Test func worktreeHistoryShortcutsDoNotConflictWithShelfBookNavigation() { #expect(AppShortcuts.worktreeHistoryBack != AppShortcuts.selectPreviousShelfBook) #expect(AppShortcuts.worktreeHistoryForward != AppShortcuts.selectNextShelfBook) - #expect(AppShortcuts.worktreeHistoryBack.display == "⌘⌥⌃←") - #expect(AppShortcuts.worktreeHistoryForward.display == "⌘⌥⌃→") + #expect(AppShortcuts.worktreeHistoryBack != AppShortcuts.selectPreviousTerminalPane) + #expect(AppShortcuts.worktreeHistoryForward != AppShortcuts.selectNextTerminalPane) + #expect(AppShortcuts.worktreeHistoryBack != AppShortcuts.selectPreviousTerminalTab) + #expect(AppShortcuts.worktreeHistoryForward != AppShortcuts.selectNextTerminalTab) + #expect(AppShortcuts.worktreeHistoryBack.display == "⌘⌥[") + #expect(AppShortcuts.worktreeHistoryForward.display == "⌘⌥]") } @Test func resolverAppliesUserOverrideOverMigratedOverride() { diff --git a/supacodeTests/ShelfFeatureTests.swift b/supacodeTests/ShelfFeatureTests.swift index a835bf11..89f12e1e 100644 --- a/supacodeTests/ShelfFeatureTests.swift +++ b/supacodeTests/ShelfFeatureTests.swift @@ -442,6 +442,41 @@ struct ShelfFeatureTests { #expect(sentCommands.value == [.performBindingAction(wt1, action: "previous_tab")]) } + @Test(.dependencies) func worktreeHistoryIsUnavailableWhileShelfIsActive() async { + let fixture = threeWorktreeFixture() + var state = RepositoriesFeature.State(repositories: [fixture.repo]) + state.selection = .worktree(fixture.worktrees[1].id) + state.isShelfActive = true + state.worktreeHistoryBackStack = [fixture.worktrees[0].id] + state.worktreeHistoryForwardStack = [fixture.worktrees[2].id] + #expect(!state.canNavigateWorktreeHistoryBackward) + #expect(!state.canNavigateWorktreeHistoryForward) + let store = TestStore(initialState: state) { + RepositoriesFeature() + } + + await store.send(.worktreeHistoryBack) + await store.send(.worktreeHistoryForward) + await store.finish() + } + + @Test(.dependencies) func worktreeHistoryIsUnavailableWhileCanvasIsActive() async { + let fixture = threeWorktreeFixture() + var state = RepositoriesFeature.State(repositories: [fixture.repo]) + state.selection = .canvas + state.worktreeHistoryBackStack = [fixture.worktrees[0].id] + state.worktreeHistoryForwardStack = [fixture.worktrees[2].id] + #expect(!state.canNavigateWorktreeHistoryBackward) + #expect(!state.canNavigateWorktreeHistoryForward) + let store = TestStore(initialState: state) { + RepositoriesFeature() + } + + await store.send(.worktreeHistoryBack) + await store.send(.worktreeHistoryForward) + await store.finish() + } + @Test(.dependencies) func selectNextWorktreeOutsideShelfStillCyclesWorktrees() async { let rootURL = URL(fileURLWithPath: "/tmp/repo") let wt1 = Worktree( @@ -477,6 +512,7 @@ struct ShelfFeatureTests { $0.selection = .worktree(wt2.id) $0.sidebarSelectedWorktreeIDs = [wt2.id] $0.openedWorktreeIDs = [wt2.id] + $0.worktreeHistoryBackStack = [wt1.id] } await store.receive(\.delegate.selectedWorktreeChanged) await store.finish()