native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #125 from jarvis-elevated/jarvis/issue-123-layout-restore-toast

Fix terminal layout restore safeguards

authored by

onevtail and committed by
GitHub
6eb283d5 62de4ec4

+63 -3
+1
supacode/Clients/Terminal/TerminalClient.swift
··· 42 42 case setupScriptConsumed(worktreeID: Worktree.ID) 43 43 case fontSizeChanged(Float32?) 44 44 case layoutRestored(selectedWorktreeID: Worktree.ID?) 45 + case layoutRestoreFailed(message: String) 45 46 } 46 47 } 47 48
+4
supacode/Features/App/Reducer/AppFeature.swift
··· 940 940 } 941 941 return .none 942 942 943 + case .terminalEvent(.layoutRestoreFailed(let message)): 944 + appLogger.warning("[LayoutRestore] layoutRestoreFailed: \(message)") 945 + return .send(.repositories(.showToast(.warning(message)))) 946 + 943 947 case .terminalEvent: 944 948 return .none 945 949 }
+2 -1
supacode/Features/Repositories/Reducer/RepositoriesFeature.swift
··· 271 271 enum StatusToast: Equatable { 272 272 case inProgress(String) 273 273 case success(String) 274 + case warning(String) 274 275 } 275 276 276 277 enum SnapshotPersistencePhase: Equatable { ··· 2035 2036 switch toast { 2036 2037 case .inProgress: 2037 2038 return .cancel(id: CancelID.toastAutoDismiss) 2038 - case .success: 2039 + case .success, .warning: 2039 2040 return .run { send in 2040 2041 try? await ContinuousClock().sleep(for: .seconds(2.5)) 2041 2042 await send(.dismissToast)
+10
supacode/Features/Repositories/Views/ToolbarStatusView.swift
··· 26 26 .foregroundStyle(.secondary) 27 27 } 28 28 .transition(.opacity) 29 + case .warning(let message): 30 + HStack(spacing: 6) { 31 + Image(systemName: "exclamationmark.triangle.fill") 32 + .foregroundStyle(.orange) 33 + .accessibilityHidden(true) 34 + Text(message) 35 + .font(.footnote) 36 + .foregroundStyle(.secondary) 37 + } 38 + .transition(.opacity) 29 39 case nil: 30 40 if let model = PullRequestStatusModel(pullRequest: pullRequest) { 31 41 PullRequestStatusButton(model: model)
+3 -1
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 3 3 import Sharing 4 4 5 5 private let terminalLogger = SupaLogger("Terminal") 6 + private let layoutRestoreFailureMessage = "Saved terminal layout was invalid and has been reset" 6 7 7 8 @MainActor 8 9 @Observable ··· 440 441 ) 441 442 emit(.layoutRestored(selectedWorktreeID: payload.selectedWorktreeID)) 442 443 } else { 443 - terminalLogger.info("[LayoutRestore] restore: clearing invalid snapshot") 444 + terminalLogger.warning("[LayoutRestore] restore: clearing invalid snapshot and emitting failure toast") 444 445 _ = await layoutPersistence.clearSnapshot() 446 + emit(.layoutRestoreFailed(message: layoutRestoreFailureMessage)) 445 447 } 446 448 } 447 449
+2 -1
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 1137 1137 guard let ratio = snapshotNode.ratio, ratio > 0, ratio < 1 else { 1138 1138 return nil 1139 1139 } 1140 + let clampedRatio = max(0.1, min(0.9, ratio)) 1140 1141 guard let children = snapshotNode.children, children.count == 2 else { 1141 1142 return nil 1142 1143 } ··· 1149 1150 return .split( 1150 1151 .init( 1151 1152 direction: splitDirection(from: direction), 1152 - ratio: ratio, 1153 + ratio: clampedRatio, 1153 1154 left: left, 1154 1155 right: right 1155 1156 )
+14
supacodeTests/AppFeatureTerminalLayoutRestoreTests.swift
··· 182 182 await store.receive(\.repositories.selectRepository) 183 183 } 184 184 185 + @Test(.dependencies) func layoutRestoreFailedEventShowsWarningToast() async { 186 + let store = TestStore(initialState: AppFeature.State()) { 187 + AppFeature() 188 + } 189 + store.exhaustivity = .off 190 + 191 + await store.send( 192 + .terminalEvent(.layoutRestoreFailed(message: "Saved terminal layout was invalid and has been reset")) 193 + ) 194 + await store.receive(\.repositories.showToast) { 195 + $0.repositories.statusToast = .warning("Saved terminal layout was invalid and has been reset") 196 + } 197 + } 198 + 185 199 @Test(.dependencies) func scenePhaseInactiveSavesLayoutSnapshot() async { 186 200 let sentCommands = LockIsolated<[TerminalClient.Command]>([]) 187 201 var settings = SettingsFeature.State()
+18
supacodeTests/TerminalLayoutSnapshotPayloadTests.swift
··· 153 153 #expect(TerminalLayoutSnapshotPayload.decodeValidated(from: data) == nil) 154 154 } 155 155 156 + @Test func decodeValidatedRoundTripsExtremeSplitRatioForRestoreClamping() throws { 157 + let payload = makePayload( 158 + splitRoot: .split( 159 + direction: .horizontal, 160 + ratio: 0.02, 161 + children: [ 162 + .leaf(surfaceID: "surface-1"), 163 + .leaf(surfaceID: "surface-2"), 164 + ] 165 + ) 166 + ) 167 + let data = try JSONEncoder().encode(payload) 168 + 169 + let decoded = TerminalLayoutSnapshotPayload.decodeValidated(from: data) 170 + #expect(decoded == payload) 171 + #expect(decoded?.worktrees.first?.tabs.first?.splitRoot.ratio == 0.02) 172 + } 173 + 156 174 @Test func decodeValidatedRejectsTypeMismatchInFields() { 157 175 let invalidJSON = #""" 158 176 {
+9
supacodeTests/WorktreeTerminalManagerTests.swift
··· 251 251 } 252 252 ) 253 253 ) 254 + let stream = manager.eventStream() 254 255 255 256 await manager.restoreLayoutSnapshot(from: []) 256 257 258 + let event = await nextEvent(stream) { event in 259 + if case .layoutRestoreFailed = event { 260 + return true 261 + } 262 + return false 263 + } 264 + 257 265 #expect(clearCount.value == 1) 266 + #expect(event == .layoutRestoreFailed(message: "Saved terminal layout was invalid and has been reset")) 258 267 } 259 268 260 269 @Test func persistLayoutSnapshotWithoutTabsClearsSnapshot() async {