native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #120 from onevcat/fix/layout-restore-bugs

Fix terminal layout restore for plain folders and clear-then-quit

authored by

Wei Wang and committed by
GitHub
d0bac0b1 dd183cd9

+92 -1
+1
supacode/App/supacodeApp.swift
··· 53 53 54 54 func applicationWillTerminate(_ notification: Notification) { 55 55 guard appStore?.state.settings.restoreTerminalLayoutOnLaunch == true else { return } 56 + guard appStore?.state.suppressLayoutSaveUntilRelaunch != true else { return } 56 57 terminalManager?.persistLayoutSnapshotSync() 57 58 } 58 59
+9 -1
supacode/Features/App/Reducer/AppFeature.swift
··· 51 51 var notificationIndicatorCount: Int = 0 52 52 var lastKnownSystemNotificationsEnabled: Bool 53 53 var launchRestoreMode: LaunchRestoreMode 54 + var suppressLayoutSaveUntilRelaunch = false 54 55 @Presents var alert: AlertState<Alert>? 55 56 56 57 init( ··· 189 190 ) 190 191 case .inactive, .background: 191 192 var effects: [Effect<Action>] = [.cancel(id: CancelID.periodicRefresh)] 192 - if state.settings.restoreTerminalLayoutOnLaunch { 193 + if state.settings.restoreTerminalLayoutOnLaunch, !state.suppressLayoutSaveUntilRelaunch { 193 194 appLogger.info("[LayoutRestore] scenePhase=\(String(describing: phase)), saving layout snapshot") 194 195 effects.append(.run { _ in await terminalClient.send(.saveLayoutSnapshot) }) 195 196 } ··· 469 470 470 471 case .settings(.delegate(.terminalLayoutSnapshotCleared(let success))): 471 472 if success { 473 + state.suppressLayoutSaveUntilRelaunch = true 472 474 return .send(.repositories(.showToast(.success("Saved terminal layout cleared")))) 473 475 } 474 476 return .send( ··· 928 930 case .terminalEvent(.layoutRestored(let selectedWorktreeID)): 929 931 appLogger.info("[LayoutRestore] layoutRestored: selectedWorktreeID=\(selectedWorktreeID ?? "nil")") 930 932 if let selectedWorktreeID { 933 + // Plain folders use .repository selection, not .worktree 934 + if let repo = state.repositories.repositories[id: selectedWorktreeID], 935 + repo.kind == .plain 936 + { 937 + return .send(.repositories(.selectRepository(selectedWorktreeID))) 938 + } 931 939 return .send(.repositories(.selectWorktree(selectedWorktreeID))) 932 940 } 933 941 return .none
+82
supacodeTests/AppFeatureTerminalLayoutRestoreTests.swift
··· 129 129 await store.receive(\.repositories.selectWorktree) 130 130 } 131 131 132 + @Test(.dependencies) func layoutRestoredEventSelectsRepositoryForPlainFolder() async { 133 + let plainRepo = makePlainRepository() 134 + let repositoriesState = RepositoriesFeature.State(repositories: [plainRepo]) 135 + let store = TestStore( 136 + initialState: AppFeature.State(repositories: repositoriesState) 137 + ) { 138 + AppFeature() 139 + } 140 + store.exhaustivity = .off 141 + 142 + await store.send(.terminalEvent(.layoutRestored(selectedWorktreeID: plainRepo.id))) 143 + await store.receive(\.repositories.selectRepository) 144 + } 145 + 132 146 @Test(.dependencies) func scenePhaseInactiveSavesLayoutSnapshot() async { 133 147 let sentCommands = LockIsolated<[TerminalClient.Command]>([]) 134 148 var settings = SettingsFeature.State() ··· 164 178 165 179 #expect(!sentCommands.value.contains(.saveLayoutSnapshot)) 166 180 } 181 + 182 + @Test(.dependencies) func clearLayoutSuppressesSaveOnScenePhaseInactive() async { 183 + let sentCommands = LockIsolated<[TerminalClient.Command]>([]) 184 + var settings = SettingsFeature.State() 185 + settings.restoreTerminalLayoutOnLaunch = true 186 + let store = TestStore(initialState: AppFeature.State(settings: settings)) { 187 + AppFeature() 188 + } withDependencies: { 189 + $0.terminalClient.send = { command in 190 + sentCommands.withValue { $0.append(command) } 191 + } 192 + $0.terminalLayoutPersistence.clearSnapshot = { true } 193 + } 194 + store.exhaustivity = .off 195 + 196 + // Clear the layout 197 + await store.send(.settings(.delegate(.terminalLayoutSnapshotCleared(success: true)))) { 198 + $0.suppressLayoutSaveUntilRelaunch = true 199 + } 200 + await store.finish() 201 + 202 + sentCommands.withValue { $0.removeAll() } 203 + 204 + // Scene phase inactive should NOT save because layout was cleared 205 + await store.send(.scenePhaseChanged(.inactive)) 206 + await store.finish() 207 + 208 + #expect(!sentCommands.value.contains(.saveLayoutSnapshot)) 209 + } 210 + 211 + @Test(.dependencies) func suppressLayoutSavePersistsAcrossMultipleScenePhaseChanges() async { 212 + let sentCommands = LockIsolated<[TerminalClient.Command]>([]) 213 + var settings = SettingsFeature.State() 214 + settings.restoreTerminalLayoutOnLaunch = true 215 + let store = TestStore(initialState: AppFeature.State(settings: settings)) { 216 + AppFeature() 217 + } withDependencies: { 218 + $0.terminalClient.send = { command in 219 + sentCommands.withValue { $0.append(command) } 220 + } 221 + $0.terminalLayoutPersistence.clearSnapshot = { true } 222 + } 223 + store.exhaustivity = .off 224 + 225 + // Clear the layout 226 + await store.send(.settings(.delegate(.terminalLayoutSnapshotCleared(success: true)))) { 227 + $0.suppressLayoutSaveUntilRelaunch = true 228 + } 229 + await store.finish() 230 + 231 + // Multiple inactive/active cycles should all skip saving 232 + for _ in 0..<3 { 233 + sentCommands.withValue { $0.removeAll() } 234 + await store.send(.scenePhaseChanged(.inactive)) 235 + await store.finish() 236 + #expect(!sentCommands.value.contains(.saveLayoutSnapshot)) 237 + } 238 + } 167 239 } 168 240 169 241 private func makeWorktree() -> Worktree { ··· 184 256 worktrees: IdentifiedArray(uniqueElements: worktrees) 185 257 ) 186 258 } 259 + 260 + private func makePlainRepository() -> Repository { 261 + Repository( 262 + id: "/tmp/plain-folder", 263 + rootURL: URL(fileURLWithPath: "/tmp/plain-folder"), 264 + name: "plain-folder", 265 + kind: .plain, 266 + worktrees: IdentifiedArray() 267 + ) 268 + }