// // GitSyncConflictSheet.swift // Confab // // Recovery UI for Git Sync's "untracked working tree files" pull failure // // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 // Copyright (C) 2026 Rune Olsen // // This file is part of Confab. // // Confab is licensed under the PolyForm Noncommercial License 1.0.0. // You may use, study, modify, and share it for any noncommercial // purpose. Commercial use — including selling Confab or any part of // it, standalone or bundled into another product or service — // requires a separate commercial license from the copyright holder. // // See the LICENSE file or // for // the full license text. For commercial licensing, contact Rune // Olsen via . import SwiftUI struct GitSyncConflictSheet: View { let pending: GitSyncService.PendingGitConflict /// Performs the automatic fix; nil return means success. let onFixForMe: () async -> String? /// Swaps this sheet for GitSyncManualFixSheet's in-app step-by-step instructions. let onFixMyself: () -> Void let onDismiss: () -> Void private enum RecoveryState: Equatable { case idle case fixing case succeeded case failed(String) } @State private var recoveryState: RecoveryState = .idle var body: some View { VStack(alignment: .leading, spacing: 20) { // Header HStack(spacing: 12) { Image(systemName: "exclamationmark.arrow.triangle.2.circlepath") .font(.title2) .foregroundStyle(.orange) VStack(alignment: .leading, spacing: 2) { Text("Sync Ran Into a Conflict") .font(.system(size: 17, weight: .semibold)) Text("Confab syncs in the background automatically, and just hit a file that collided with a leftover local copy of itself") .font(.system(size: 13)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } Spacer() } // Conflicting files VStack(alignment: .leading, spacing: 6) { Text("AFFECTED FILES") .font(.system(size: 11, weight: .medium)) .foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 4) { ForEach(pending.files, id: \.self) { file in Text(file) .font(.system(size: 13, design: .monospaced)) } } .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) .padding(12) .background(Color.secondary.opacity(0.08)) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.secondary.opacity(0.2), lineWidth: 1) ) } Text("These are Confab's own sync bookkeeping files, not your conversations — nothing you've written is at risk either way.") .font(.system(size: 12)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) statusBanner if !pending.canAutoFix { HStack(alignment: .top, spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.orange) .font(.system(size: 13)) .padding(.top, 1) Text("One or more of these files aren't ones Confab recognizes as safe to remove automatically — please fix this one yourself.") .font(.system(size: 12)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } .padding(10) .background(Color.orange.opacity(0.08)) .clipShape(RoundedRectangle(cornerRadius: 8)) } // Buttons HStack(spacing: 8) { Button("Fix It Myself") { onFixMyself() } .buttonStyle(.bordered) .keyboardShortcut(.escape, modifiers: []) Spacer() if recoveryState == .succeeded { Button("Done") { onDismiss() } .buttonStyle(.borderedProminent) .keyboardShortcut(.return, modifiers: []) } else { Button("Fix It For Me") { recoveryState = .fixing Task { if let errorMessage = await onFixForMe() { recoveryState = .failed(errorMessage) } else { recoveryState = .succeeded } } } .buttonStyle(.borderedProminent) .disabled(!pending.canAutoFix || recoveryState == .fixing) .keyboardShortcut(.return, modifiers: []) } } } .padding(24) .frame(width: 480) } @ViewBuilder private var statusBanner: some View { switch recoveryState { case .idle: EmptyView() case .fixing: HStack(spacing: 8) { ProgressView().controlSize(.small) Text("Fixing...") .font(.system(size: 13)) .foregroundStyle(.secondary) } case .succeeded: HStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text("Fixed — your conversations are back in sync.") .font(.system(size: 13)) .foregroundStyle(.secondary) } case .failed(let message): HStack(alignment: .top, spacing: 8) { Image(systemName: "xmark.circle.fill") .foregroundStyle(.red) .font(.system(size: 13)) .padding(.top, 1) Text("Still couldn't sync: \(message)") .font(.system(size: 12)) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } } } }