// // ConfirmCenter.swift // Poche // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // import Foundation import Observation /// The only gate to side effects. Tools enqueue proposals here; nothing is /// written until the user confirms on the card. There is no code path that /// writes without crossing this layer — no expert mode, no preference to /// disable it, no exception (CLAUDE.md §3). @MainActor @Observable final class ConfirmCenter { private(set) var pending: [PendingAction] = [] private let executor: ActionExecutor var onExecuted: ((String) -> Void)? var onDismissed: ((String) -> Void)? init(executor: ActionExecutor) { self.executor = executor } /// Called by tools. Registers the proposal; the UI shows the card. func propose(_ action: PendingAction) { pending.append(action) } /// Called by the confirmation card only — this is the user's decision. func confirm(_ action: PendingAction) async { guard let index = pending.firstIndex(where: { $0.id == action.id }) else { return } pending[index].status = .executing do { let summary = try await executor.execute(action.payload) pending.removeAll { $0.id == action.id } onExecuted?(summary) } catch { if let idx = pending.firstIndex(where: { $0.id == action.id }) { pending[idx].status = .failed(userMessage(for: error)) } } } func dismiss(_ action: PendingAction) { guard pending.contains(where: { $0.id == action.id }) else { return } pending.removeAll { $0.id == action.id } onDismissed?(action.summary) } private func userMessage(for error: any Error) -> String { if let bridgeError = error as? BridgeError { return bridgeError.userMessage } return "L'action n'a pas pu être exécutée. Réessaie." } }