import SwiftUI private struct ExecApprovalPromptDialogModifier: ViewModifier { @Environment(NodeAppModel.self) private var appModel: NodeAppModel @Environment(\.colorScheme) private var colorScheme func body(content: Content) -> some View { content .overlay { if let prompt = self.appModel.pendingExecApprovalPrompt { ZStack { Color.black.opacity(0.38) .ignoresSafeArea() ExecApprovalPromptCard( prompt: prompt, isResolving: self.appModel.pendingExecApprovalPromptResolving, errorText: self.appModel.pendingExecApprovalPromptErrorText, brighten: self.colorScheme == .light, onAllowOnce: { Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-once") } }, onAllowAlways: { Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-always") } }, onDeny: { Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "deny") } }, onCancel: { self.appModel.dismissPendingExecApprovalPrompt() }) .padding(.horizontal, 20) .frame(maxWidth: 460) .transition(.scale(scale: 0.98).combined(with: .opacity)) } .zIndex(1) } } .animation(.easeInOut(duration: 0.18), value: self.appModel.pendingExecApprovalPrompt?.id) } } private struct ExecApprovalPromptCard: View { let prompt: NodeAppModel.ExecApprovalPrompt let isResolving: Bool let errorText: String? let brighten: Bool let onAllowOnce: () -> Void let onAllowAlways: () -> Void let onDeny: () -> Void let onCancel: () -> Void var body: some View { VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 6) { Text("Exec approval required") .font(.headline) Text("OpenClaw opened from a notification. Review this exec request before continuing.") .font(.subheadline) .foregroundStyle(.secondary) } Text(self.prompt.commandText) .font(.system(size: 15, weight: .regular, design: .monospaced)) .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background(.black.opacity(0.14), in: RoundedRectangle(cornerRadius: 12, style: .continuous)) VStack(alignment: .leading, spacing: 8) { if let host = self.normalized(self.prompt.host) { ExecApprovalPromptMetadataRow(label: "Host", value: host) } if let nodeId = self.normalized(self.prompt.nodeId) { ExecApprovalPromptMetadataRow(label: "Node", value: nodeId) } if let agentId = self.normalized(self.prompt.agentId) { ExecApprovalPromptMetadataRow(label: "Agent", value: agentId) } if let expiresText = self.expiresText(self.prompt.expiresAtMs) { ExecApprovalPromptMetadataRow(label: "Expires", value: expiresText) } } if let errorText = self.normalized(self.errorText) { Text(errorText) .font(.footnote) .foregroundStyle(.red) } if self.isResolving { HStack(spacing: 8) { ProgressView() .progressViewStyle(.circular) Text("Resolving…") .font(.footnote) .foregroundStyle(.secondary) } } VStack(spacing: 10) { Button { self.onAllowOnce() } label: { Text("Allow Once") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .disabled(self.isResolving) if self.prompt.allowsAllowAlways { Button { self.onAllowAlways() } label: { Text("Allow Always") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .disabled(self.isResolving) } HStack(spacing: 10) { Button(role: .destructive) { self.onDeny() } label: { Text("Deny") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .disabled(self.isResolving) Button(role: .cancel) { self.onCancel() } label: { Text("Cancel") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) .disabled(self.isResolving) } } .controlSize(.large) .frame(maxWidth: .infinity) } .statusGlassCard(brighten: self.brighten, verticalPadding: 18, horizontalPadding: 18) } private func normalized(_ value: String?) -> String? { let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? nil : trimmed } private func expiresText(_ expiresAtMs: Int?) -> String? { guard let expiresAtMs else { return nil } let remainingSeconds = Int((Double(expiresAtMs) / 1000.0) - Date().timeIntervalSince1970) if remainingSeconds <= 0 { return "expired" } if remainingSeconds < 60 { return "under a minute" } if remainingSeconds < 3600 { let minutes = Int(ceil(Double(remainingSeconds) / 60.0)) return minutes == 1 ? "about 1 minute" : "about \(minutes) minutes" } let hours = Int(ceil(Double(remainingSeconds) / 3600.0)) return hours == 1 ? "about 1 hour" : "about \(hours) hours" } } private struct ExecApprovalPromptMetadataRow: View { let label: String let value: String var body: some View { VStack(alignment: .leading, spacing: 2) { Text(self.label) .font(.caption) .foregroundStyle(.secondary) Text(self.value) .font(.footnote) .textSelection(.enabled) } } } extension View { func execApprovalPromptDialog() -> some View { self.modifier(ExecApprovalPromptDialogModifier()) } }