From 9323fc147a8dda2c853a72bb42981e381c783ad2 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 7 Jun 2026 00:51:52 +0900 Subject: [PATCH] =?UTF-8?q?Add=20Expand/Restore=20shortcut=20(=E2=8C=A5?= =?UTF-8?q?=E2=8C=98E)=20and=20palette=20commands=20for=20canvas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keyboard: bind Expand / Restore Canvas Card to ⌥⌘E (a single toggle — there's only ever one expanded card). ⌥⌘E joins the existing canvas shortcuts ⌥⌘A/R/G and avoids the system ⌥⌘M (Minimize All). CanvasView handles the key and toggles the focused (primary) card. Command palette: surface Expand/Restore, Arrange, Organize, and Select All in the palette while in Canvas mode (the latter three weren't there before). Since these are CanvasView's view-local actions (not reducer state), route them through a one-shot reducer→view trigger modeled on CanvasFocusRequest: a new CanvasCommandRequest + RepositoriesFeature state (request/consume), AppFeature maps the palette delegates to .requestCanvasCommand, and CanvasView observes pendingCanvasCommandRequest, runs the command, and reports the id back to clear it. Wiring: new Kind/Delegate cases, viewDelegateAction mapping, palette item rows (icon/title/help/category), AppShortcuts CommandID + default shortcut + recordable command entry. Tests cover the palette items (present in Canvas mode, absent otherwise) and the request/consume reducer logic. --- supacode/App/AppShortcuts.swift | 8 +++ .../Features/App/Reducer/AppFeature.swift | 12 ++++ .../Canvas/Models/CanvasFocusRequest.swift | 16 +++++ .../Features/Canvas/Views/CanvasView.swift | 47 ++++++++++++++ .../CommandPalette/CommandPaletteItem.swift | 12 ++++ .../Reducer/CommandPaletteFeature.swift | 64 +++++++++++++++++++ .../Views/CommandPaletteOverlayView.swift | 24 ++++++- .../Reducer/RepositoriesFeature.swift | 18 ++++++ .../Views/WorktreeDetailView.swift | 4 ++ .../CommandPaletteFeatureTests.swift | 27 +++++++- supacodeTests/RepositoriesFeatureTests.swift | 31 +++++++++ 11 files changed, 259 insertions(+), 4 deletions(-) diff --git a/supacode/App/AppShortcuts.swift b/supacode/App/AppShortcuts.swift index a802ab4f..77bb1665 100644 --- a/supacode/App/AppShortcuts.swift +++ b/supacode/App/AppShortcuts.swift @@ -142,6 +142,7 @@ enum AppShortcuts { static let selectAllCanvasCards = "select_all_canvas_cards" static let arrangeCanvasCards = "arrange_canvas_cards" static let organizeCanvasCards = "organize_canvas_cards" + static let expandCanvasCard = "expand_canvas_card" static let selectPreviousTerminalTab = "select_previous_terminal_tab" static let selectNextTerminalTab = "select_next_terminal_tab" static let selectPreviousTerminalPane = "select_previous_terminal_pane" @@ -290,6 +291,7 @@ enum AppShortcuts { static let selectAllCanvasCards = AppShortcut(key: "a", modifiers: [.command, .option]) static let arrangeCanvasCards = AppShortcut(key: "r", modifiers: [.command, .option]) static let organizeCanvasCards = AppShortcut(key: "g", modifiers: [.command, .option]) + static let expandCanvasCard = AppShortcut(key: "e", modifiers: [.command, .option]) static let worktreeSelection: [AppShortcut] = [ selectWorktree1, selectWorktree2, @@ -767,6 +769,12 @@ enum AppShortcuts { scope: .localInteraction, shortcut: organizeCanvasCards ), + .init( + id: CommandID.expandCanvasCard, + title: "Expand / Restore Canvas Card", + scope: .localInteraction, + shortcut: expandCanvasCard + ), ] static func userOverrideConflicts( diff --git a/supacode/Features/App/Reducer/AppFeature.swift b/supacode/Features/App/Reducer/AppFeature.swift index 565b5976..5b85ec15 100644 --- a/supacode/Features/App/Reducer/AppFeature.swift +++ b/supacode/Features/App/Reducer/AppFeature.swift @@ -1207,6 +1207,18 @@ struct AppFeature { case .commandPalette(.delegate(.toggleCanvas)): return .send(.repositories(.toggleCanvas)) + case .commandPalette(.delegate(.expandCanvasCard)): + return .send(.repositories(.requestCanvasCommand(.toggleExpand))) + + case .commandPalette(.delegate(.arrangeCanvasCards)): + return .send(.repositories(.requestCanvasCommand(.arrange))) + + case .commandPalette(.delegate(.organizeCanvasCards)): + return .send(.repositories(.requestCanvasCommand(.organize))) + + case .commandPalette(.delegate(.selectAllCanvasCards)): + return .send(.repositories(.requestCanvasCommand(.selectAll))) + case .commandPalette(.delegate(.toggleShelf)): return .send(.repositories(.toggleShelf)) diff --git a/supacode/Features/Canvas/Models/CanvasFocusRequest.swift b/supacode/Features/Canvas/Models/CanvasFocusRequest.swift index ade498c1..465a60d6 100644 --- a/supacode/Features/Canvas/Models/CanvasFocusRequest.swift +++ b/supacode/Features/Canvas/Models/CanvasFocusRequest.swift @@ -15,6 +15,22 @@ struct CanvasFocusCandidate: Equatable, Sendable { let tabID: TerminalTabID } +/// A reducer-driven request to run one of CanvasView's view-local commands +/// (which live in CanvasView's `@State`, not the reducer) — e.g. triggered from +/// the command palette. CanvasView observes it, runs the command, and reports +/// the id back to clear it (the same one-shot pattern as `CanvasFocusRequest`). +struct CanvasCommandRequest: Equatable, Sendable { + enum Command: Equatable, Sendable { + case toggleExpand + case arrange + case organize + case selectAll + } + + let id: Int + let command: Command +} + enum CanvasFocusResolver { static func resolve( request: CanvasFocusRequest, diff --git a/supacode/Features/Canvas/Views/CanvasView.swift b/supacode/Features/Canvas/Views/CanvasView.swift index d4183929..486bffef 100644 --- a/supacode/Features/Canvas/Views/CanvasView.swift +++ b/supacode/Features/Canvas/Views/CanvasView.swift @@ -13,8 +13,12 @@ struct CanvasView: View { /// per-frame canvas hot path. var repositoryCustomTitles: [Repository.ID: String] = [:] var focusRequest: CanvasFocusRequest? + /// A one-shot, reducer-driven request to run a view-local canvas command + /// (expand/arrange/organize/select-all), e.g. from the command palette. + var commandRequest: CanvasCommandRequest? var onFocusedWorktreeChanged: (Worktree.ID?) -> Void = { _ in } var onFocusRequestConsumed: (Int) -> Void = { _ in } + var onCommandConsumed: (Int) -> Void = { _ in } /// Reports whether a card is currently expanded in place, so the parent can /// give the window toolbar a matching scrim (it can't be covered from here). var onExpandedChange: (Bool) -> Void = { _ in } @@ -82,6 +86,10 @@ struct CanvasView: View { for: AppShortcuts.CommandID.organizeCanvasCards, in: resolvedKeybindings ) + let expandCanvasShortcut = AppShortcuts.resolvedShortcut( + for: AppShortcuts.CommandID.expandCanvasCard, + in: resolvedKeybindings + ) let _ = configReloadCounter CanvasScrollContainer( offset: $canvasOffset, @@ -202,9 +210,21 @@ struct CanvasView: View { organizeCardsWithFit() return .handled } + .onKeyPress( + expandCanvasShortcut?.keyEquivalent ?? AppShortcuts.expandCanvasCard.keyEquivalent, + phases: .down + ) { keyPress in + guard let shortcut = expandCanvasShortcut else { return .ignored } + guard keyPress.modifiers == shortcut.modifiers else { return .ignored } + toggleExpandFocusedCard() + return .handled + } .onChange(of: expandedTabID) { _, newValue in onExpandedChange(newValue != nil) } + .onChange(of: commandRequest) { _, newRequest in + fulfillCommandRequest(newRequest) + } .task { activateCanvas() } .onReceive(NotificationCenter.default.publisher(for: .ghosttyRuntimeConfigDidChange)) { _ in configReloadCounter &+= 1 @@ -856,6 +876,33 @@ struct CanvasView: View { } } + /// Toggle expand/restore for the focused (primary) card. Used by the keyboard + /// shortcut and the command palette, which target whichever card is focused. + private func toggleExpandFocusedCard() { + if expandedTabID != nil { + collapseExpand() + } else if let tabID = selectionState.primaryTabID { + expandCard(tabID, states: terminalManager.activeWorktreeStates) + } + } + + /// Run a reducer-driven canvas command (from the command palette) and clear + /// the one-shot request. + private func fulfillCommandRequest(_ request: CanvasCommandRequest?) { + guard let request else { return } + switch request.command { + case .toggleExpand: + toggleExpandFocusedCard() + case .arrange: + arrangeCardsWithFit() + case .organize: + organizeCardsWithFit() + case .selectAll: + selectAllCards() + } + onCommandConsumed(request.id) + } + /// Expand a card in place: raise it to the top, then flip `expandedTabID` /// inside one `withAnimation` so the card's size, center, and scale all /// interpolate together from its in-canvas frame to the full viewport at diff --git a/supacode/Features/CommandPalette/CommandPaletteItem.swift b/supacode/Features/CommandPalette/CommandPaletteItem.swift index dc178ecc..163313d5 100644 --- a/supacode/Features/CommandPalette/CommandPaletteItem.swift +++ b/supacode/Features/CommandPalette/CommandPaletteItem.swift @@ -64,6 +64,10 @@ struct CommandPaletteItem: Identifiable, Equatable { case toggleLeftSidebar case toggleActiveAgentsPanel case toggleCanvas + case expandCanvasCard + case arrangeCanvasCards + case organizeCanvasCards + case selectAllCanvasCards case toggleShelf case showDiff case revealInFinder @@ -120,6 +124,14 @@ struct CommandPaletteItem: Identifiable, Equatable { return AppShortcuts.CommandID.toggleActiveAgentsPanel case .toggleCanvas: return AppShortcuts.CommandID.toggleCanvas + case .expandCanvasCard: + return AppShortcuts.CommandID.expandCanvasCard + case .arrangeCanvasCards: + return AppShortcuts.CommandID.arrangeCanvasCards + case .organizeCanvasCards: + return AppShortcuts.CommandID.organizeCanvasCards + case .selectAllCanvasCards: + return AppShortcuts.CommandID.selectAllCanvasCards case .toggleShelf: return AppShortcuts.CommandID.toggleShelf case .showDiff: diff --git a/supacode/Features/CommandPalette/Reducer/CommandPaletteFeature.swift b/supacode/Features/CommandPalette/Reducer/CommandPaletteFeature.swift index 4cc069dd..6ff01eb0 100644 --- a/supacode/Features/CommandPalette/Reducer/CommandPaletteFeature.swift +++ b/supacode/Features/CommandPalette/Reducer/CommandPaletteFeature.swift @@ -54,6 +54,10 @@ struct CommandPaletteFeature { case toggleLeftSidebar case toggleActiveAgentsPanel case toggleCanvas + case expandCanvasCard + case arrangeCanvasCards + case organizeCanvasCards + case selectAllCanvasCards case toggleShelf case showDiff case revealInFinder @@ -224,6 +228,9 @@ struct CommandPaletteFeature { repositories.repositories.isEmpty || repositories.repositories.contains { $0.capabilities.supportsWorktrees } var items = globalCommandItems(showsNewWorktreeAction: showsNewWorktreeAction) + if repositories.isShowingCanvas { + items.append(contentsOf: canvasCommandItems()) + } let worktreeActionTargetID = actionTargetWorktreeID ?? repositories.selectedWorktreeID if repositories.selectedWorktreeID != nil { items.append( @@ -580,6 +587,39 @@ private func viewToggleCommandItems() -> [CommandPaletteItem] { ] } +private func canvasCommandItems() -> [CommandPaletteItem] { + [ + .appShortcut( + id: CommandPaletteItemID.globalExpandCanvasCard, + title: "Expand / Restore Canvas Card", + category: .view, + kind: .expandCanvasCard, + keywords: ["canvas", "expand", "restore", "focus", "fullscreen", "card"] + ), + .appShortcut( + id: CommandPaletteItemID.globalArrangeCanvasCards, + title: "Arrange Canvas Cards", + category: .view, + kind: .arrangeCanvasCards, + keywords: ["canvas", "arrange", "layout", "pack", "fit"] + ), + .appShortcut( + id: CommandPaletteItemID.globalOrganizeCanvasCards, + title: "Organize Canvas Cards", + category: .view, + kind: .organizeCanvasCards, + keywords: ["canvas", "organize", "grid", "tidy", "uniform"] + ), + .appShortcut( + id: CommandPaletteItemID.globalSelectAllCanvasCards, + title: "Select All Canvas Cards", + category: .view, + kind: .selectAllCanvasCards, + keywords: ["canvas", "select all", "broadcast"] + ), + ] +} + private func selectedCodeHostItems( from repositories: RepositoriesFeature.State ) -> [CommandPaletteItem] { @@ -858,6 +898,10 @@ private enum CommandPaletteItemID { static let globalToggleLeftSidebar = "global.toggle-left-sidebar" static let globalToggleActiveAgentsPanel = "global.toggle-active-agents-panel" static let globalToggleCanvas = "global.toggle-canvas" + static let globalExpandCanvasCard = "global.expand-canvas-card" + static let globalArrangeCanvasCards = "global.arrange-canvas-cards" + static let globalOrganizeCanvasCards = "global.organize-canvas-cards" + static let globalSelectAllCanvasCards = "global.select-all-canvas-cards" static let globalToggleShelf = "global.toggle-shelf" static let globalShowDiff = "global.show-diff" static let globalRevealInFinder = "global.reveal-in-finder" @@ -890,6 +934,10 @@ private enum CommandPaletteItemID { globalToggleLeftSidebar, globalToggleActiveAgentsPanel, globalToggleCanvas, + globalExpandCanvasCard, + globalArrangeCanvasCards, + globalOrganizeCanvasCards, + globalSelectAllCanvasCards, globalToggleShelf, globalShowDiff, globalRevealInFinder, @@ -1021,6 +1069,10 @@ private func delegateAction(for kind: CommandPaletteItem.Kind) -> CommandPalette .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, + .expandCanvasCard, + .arrangeCanvasCards, + .organizeCanvasCards, + .selectAllCanvasCards, .toggleShelf, .showDiff, .revealInFinder, @@ -1089,6 +1141,14 @@ private func viewDelegateAction(for kind: CommandPaletteItem.Kind) -> CommandPal return .toggleActiveAgentsPanel case .toggleCanvas: return .toggleCanvas + case .expandCanvasCard: + return .expandCanvasCard + case .arrangeCanvasCards: + return .arrangeCanvasCards + case .organizeCanvasCards: + return .organizeCanvasCards + case .selectAllCanvasCards: + return .selectAllCanvasCards case .toggleShelf: return .toggleShelf case .showDiff: @@ -1133,6 +1193,10 @@ private func pullRequestDelegateAction( .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, + .expandCanvasCard, + .arrangeCanvasCards, + .organizeCanvasCards, + .selectAllCanvasCards, .toggleShelf, .showDiff, .revealInFinder, diff --git a/supacode/Features/CommandPalette/Views/CommandPaletteOverlayView.swift b/supacode/Features/CommandPalette/Views/CommandPaletteOverlayView.swift index 0e26fdab..d92a9921 100644 --- a/supacode/Features/CommandPalette/Views/CommandPaletteOverlayView.swift +++ b/supacode/Features/CommandPalette/Views/CommandPaletteOverlayView.swift @@ -526,7 +526,9 @@ private struct CommandPaletteRowView: View { .copyFailingJobURL, .copyCiFailureLogs, .rerunFailedJobs, .openFailingCheckDetails, .worktreeSelect, .changeFocusedTabIcon, - .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, .toggleShelf, .showDiff, + .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, + .expandCanvasCard, .arrangeCanvasCards, .organizeCanvasCards, .selectAllCanvasCards, + .toggleShelf, .showDiff, .revealInFinder, .copyPath, .revealInSidebar, .runScript, .stopRunScript, .togglePinWorktree, .renameBranch, .openRepositorySettings, .runCustomCommand: @@ -586,6 +588,14 @@ private struct CommandPaletteRowView: View { return "person.crop.rectangle.stack" case .toggleCanvas: return "square.grid.2x2" + case .expandCanvasCard: + return "arrow.up.left.and.arrow.down.right" + case .arrangeCanvasCards: + return "rectangle.3.group" + case .organizeCanvasCards: + return "square.grid.2x2" + case .selectAllCanvasCards: + return "checkmark.rectangle.stack" case .toggleShelf: return "books.vertical" case .showDiff: @@ -629,7 +639,9 @@ private struct CommandPaletteRowView: View { .copyFailingJobURL, .copyCiFailureLogs, .rerunFailedJobs, .openFailingCheckDetails, .changeFocusedTabIcon, - .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, .toggleShelf, .showDiff, + .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, + .expandCanvasCard, .arrangeCanvasCards, .organizeCanvasCards, .selectAllCanvasCards, + .toggleShelf, .showDiff, .revealInFinder, .copyPath, .revealInSidebar, .runScript, .stopRunScript, .togglePinWorktree, .renameBranch, .openRepositorySettings, @@ -754,6 +766,14 @@ private struct CommandPaletteRowView: View { base = "Toggle Active Agents Panel" case .toggleCanvas: base = "Toggle Canvas" + case .expandCanvasCard: + base = "Expand / Restore Canvas Card" + case .arrangeCanvasCards: + base = "Arrange Canvas Cards" + case .organizeCanvasCards: + base = "Organize Canvas Cards" + case .selectAllCanvasCards: + base = "Select All Canvas Cards" case .toggleShelf: base = "Toggle Shelf" case .showDiff: diff --git a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift index 13ff2ce0..9644c358 100644 --- a/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift +++ b/supacode/Features/Repositories/Reducer/RepositoriesFeature.swift @@ -277,6 +277,8 @@ struct RepositoriesFeature { var showActiveAgentTabTitles = false var nextCanvasFocusRequestID = 0 var pendingCanvasFocusRequest: CanvasFocusRequest? + var nextCanvasCommandRequestID = 0 + var pendingCanvasCommandRequest: CanvasCommandRequest? var activeAgents = ActiveAgentsFeature.State() @Shared(.appStorage("sidebarCollapsedRepositoryIDs")) var collapsedRepositoryIDs: [Repository.ID] = [] @Presents var worktreeCreationPrompt: WorktreeCreationPromptFeature.State? @@ -352,6 +354,8 @@ struct RepositoriesFeature { case selectNextWorktree case selectPreviousWorktree case consumeCanvasFocusRequest(Int) + case requestCanvasCommand(CanvasCommandRequest.Command) + case consumeCanvasCommandRequest(Int) case worktreeHistoryBack case worktreeHistoryForward case revealSelectedWorktreeInSidebar @@ -1042,6 +1046,20 @@ struct RepositoriesFeature { } return .none + case .requestCanvasCommand(let command): + state.nextCanvasCommandRequestID += 1 + state.pendingCanvasCommandRequest = CanvasCommandRequest( + id: state.nextCanvasCommandRequestID, + command: command + ) + return .none + + case .consumeCanvasCommandRequest(let id): + if state.pendingCanvasCommandRequest?.id == id { + state.pendingCanvasCommandRequest = nil + } + return .none + case .worktreeHistoryBack: return navigateWorktreeHistory(direction: .backward, state: &state) diff --git a/supacode/Features/Repositories/Views/WorktreeDetailView.swift b/supacode/Features/Repositories/Views/WorktreeDetailView.swift index 5fffb089..e272dd22 100644 --- a/supacode/Features/Repositories/Views/WorktreeDetailView.swift +++ b/supacode/Features/Repositories/Views/WorktreeDetailView.swift @@ -399,12 +399,16 @@ struct WorktreeDetailView: View { terminalManager: terminalManager, repositoryCustomTitles: repositories.repositoryCustomTitles, focusRequest: repositories.pendingCanvasFocusRequest, + commandRequest: repositories.pendingCanvasCommandRequest, onFocusedWorktreeChanged: { worktreeID in store.send(.canvasFocusedWorktreeChanged(worktreeID)) }, onFocusRequestConsumed: { requestID in store.send(.repositories(.consumeCanvasFocusRequest(requestID))) }, + onCommandConsumed: { requestID in + store.send(.repositories(.consumeCanvasCommandRequest(requestID))) + }, onExpandedChange: { expanded in isCanvasCardExpanded = expanded } diff --git a/supacodeTests/CommandPaletteFeatureTests.swift b/supacodeTests/CommandPaletteFeatureTests.swift index 1d6255ce..cd3f46f5 100644 --- a/supacodeTests/CommandPaletteFeatureTests.swift +++ b/supacodeTests/CommandPaletteFeatureTests.swift @@ -46,6 +46,25 @@ struct CommandPaletteFeatureTests { #expect(items.contains { $0.id == "global.show-diff" }) } + @Test func commandPaletteItems_includesCanvasCommandsInCanvasMode() { + var state = RepositoriesFeature.State() + state.selection = .canvas + + let ids = CommandPaletteFeature.commandPaletteItems(from: state).map(\.id) + #expect(ids.contains("global.expand-canvas-card")) + #expect(ids.contains("global.arrange-canvas-cards")) + #expect(ids.contains("global.organize-canvas-cards")) + #expect(ids.contains("global.select-all-canvas-cards")) + } + + @Test func commandPaletteItems_omitsCanvasCommandsOutsideCanvas() { + let ids = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()).map(\.id) + #expect(!ids.contains("global.expand-canvas-card")) + #expect(!ids.contains("global.arrange-canvas-cards")) + #expect(!ids.contains("global.organize-canvas-cards")) + #expect(!ids.contains("global.select-all-canvas-cards")) + } + @Test func commandPaletteItems_omitsShowDiffWithoutSelectedWorktree() { let items = CommandPaletteFeature.commandPaletteItems(from: RepositoriesFeature.State()) #expect(!items.contains { $0.id == "global.show-diff" }) @@ -1701,7 +1720,9 @@ private func testCategory(for kind: CommandPaletteItem.Kind) -> CommandPaletteIt return .pullRequest case .ghosttyCommand: return .terminal - case .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, .toggleShelf, .showDiff: + case .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, + .expandCanvasCard, .arrangeCanvasCards, .organizeCanvasCards, .selectAllCanvasCards, + .toggleShelf, .showDiff: return .view #if DEBUG case .debugTestToast, .debugSimulateUpdateFound, .debugLightDockNotificationDot: @@ -1716,7 +1737,9 @@ private func testDefaultSuggestion(for kind: CommandPaletteItem.Kind) -> Bool { .newWorktree, .refreshWorktrees, .viewArchivedWorktrees, .jumpToLatestUnread, .openPullRequest, .markPullRequestReady, .mergePullRequest, .closePullRequest, .copyFailingJobURL, .copyCiFailureLogs, .rerunFailedJobs, .openFailingCheckDetails, - .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, .toggleShelf, .showDiff, + .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, + .expandCanvasCard, .arrangeCanvasCards, .organizeCanvasCards, .selectAllCanvasCards, + .toggleShelf, .showDiff, .revealInFinder, .copyPath, .revealInSidebar, .runScript, .stopRunScript, .togglePinWorktree, .renameBranch, .openRepositorySettings: diff --git a/supacodeTests/RepositoriesFeatureTests.swift b/supacodeTests/RepositoriesFeatureTests.swift index 21981b49..6a38aeae 100644 --- a/supacodeTests/RepositoriesFeatureTests.swift +++ b/supacodeTests/RepositoriesFeatureTests.swift @@ -11,6 +11,37 @@ import Testing @MainActor struct RepositoriesFeatureTests { + @Test func requestCanvasCommandSetsPendingRequestWithIncrementingID() async { + let store = TestStore(initialState: RepositoriesFeature.State()) { + RepositoriesFeature() + } + + await store.send(.requestCanvasCommand(.toggleExpand)) { + $0.nextCanvasCommandRequestID = 1 + $0.pendingCanvasCommandRequest = CanvasCommandRequest(id: 1, command: .toggleExpand) + } + await store.send(.requestCanvasCommand(.arrange)) { + $0.nextCanvasCommandRequestID = 2 + $0.pendingCanvasCommandRequest = CanvasCommandRequest(id: 2, command: .arrange) + } + } + + @Test func consumeCanvasCommandRequestClearsOnlyMatchingID() async { + var initialState = RepositoriesFeature.State() + initialState.nextCanvasCommandRequestID = 5 + initialState.pendingCanvasCommandRequest = CanvasCommandRequest(id: 5, command: .organize) + let store = TestStore(initialState: initialState) { + RepositoriesFeature() + } + + // A stale id is ignored. + await store.send(.consumeCanvasCommandRequest(4)) + // The matching id clears the request. + await store.send(.consumeCanvasCommandRequest(5)) { + $0.pendingCanvasCommandRequest = nil + } + } + @Test func refreshWorktreesSetsRefreshingStateUntilLoadCompletes() async { let worktree = makeWorktree(id: "/tmp/repo/main", name: "main") let repository = makeRepository(id: "/tmp/repo", worktrees: [worktree]) -- 2.51.2