Files
openclaw/apps/ios/Sources/Design/SettingsProTabSections.swift
Marvin Kaunda 5370f64cf0 improve: native iOS look with stock SwiftUI navigation, forms, chat, and talk visualizer (#99231)
* improve(ios): adopt native SwiftUI chrome across the app

* fix(ios): keep phone chat on the native navigation bar

* fix(ios): render licenses screen as native settings rows

* fix(ios): complete native interface polish

* fix(ios): preserve branded native interface

* test(ios): align gateway badge source guard

* fix(ios): center onboarding on iPad

* fix(ios): remove dead reskin symbols

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-03 18:17:09 -07:00

995 lines
37 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import OpenClawKit
import SwiftUI
/// iOS Settings-style icon: white glyph on a solid rounded-square, sized for a List row.
struct SettingsIcon: View {
let systemName: String
let color: Color
var body: some View {
Image(systemName: self.systemName)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 28, height: 28)
.background(RoundedRectangle(cornerRadius: 7, style: .continuous).fill(self.color))
}
}
private struct AppearanceSettingsRow: View {
@Environment(AppAppearanceModel.self) private var appearanceModel
private var preference: AppAppearancePreference {
self.appearanceModel.preference
}
var body: some View {
NavigationLink {
AppearanceSettingsScreen()
} label: {
self.rowLabel
}
.accessibilityIdentifier("settings-appearance-row")
.accessibilityLabel("Appearance")
.accessibilityValue(self.preference.label)
.accessibilityHint("Choose system, light, or dark appearance")
}
private var rowLabel: some View {
HStack(spacing: 12) {
ProIconBadge(
systemName: "circle.lefthalf.filled",
color: .secondary)
Text("Appearance")
.font(OpenClawType.subheadSemiBold)
Spacer(minLength: 8)
Text(self.preference.label)
.font(OpenClawType.subhead)
.foregroundStyle(.secondary)
}
}
}
private struct AppearanceSettingsScreen: View {
@Environment(AppAppearanceModel.self) private var appearanceModel
@Environment(\.dismiss) private var dismiss
var body: some View {
List {
Section {
ForEach(AppAppearancePreference.allCases) { preference in
Button {
self.select(preference)
} label: {
Label {
HStack {
Text(preference.label)
.font(OpenClawType.body)
Spacer()
if preference == self.appearanceModel.preference {
Image(systemName: "checkmark")
.foregroundStyle(OpenClawBrand.accent)
}
}
} icon: {
Image(systemName: preference.systemImage)
}
}
.foregroundStyle(.primary)
.accessibilityIdentifier("settings-appearance-\(preference.rawValue)")
.accessibilityValue(
preference == self.appearanceModel.preference ? "Selected" : "")
}
} footer: {
Text("System follows this devices appearance setting.")
.font(OpenClawType.footnote)
}
}
.navigationTitle("Appearance")
.navigationBarTitleDisplayMode(.inline)
}
private func select(_ preference: AppAppearancePreference) {
guard preference != self.appearanceModel.preference else { return }
self.dismiss()
Task { @MainActor in
// Changing the root scheme while an iPad split-view destination is active can
// leave that destination blank. Apply only after the native pop transition.
try? await Task.sleep(for: .milliseconds(500))
self.appearanceModel.select(preference)
}
}
}
extension SettingsProTab {
var appearanceRow: some View {
AppearanceSettingsRow()
}
var gatewaySection: some View {
Section("Gateway") {
NavigationLink(value: SettingsRoute.gateway) {
self.gatewayConnectionRow
}
LabeledContent("Address", value: self.gatewayAddress)
LabeledContent("Server", value: self.gatewayServer)
LabeledContent("Agents", value: "\(self.appModel.gatewayAgents.count)")
self.gatewayActions
}
}
var gatewayConnectionRow: some View {
LabeledContent {
Text(self.gatewayStatusDetail)
.font(OpenClawType.subhead)
.foregroundStyle(self.gatewayStatusColor)
} label: {
Text("Connection")
.font(OpenClawType.subheadSemiBold)
}
}
@ViewBuilder var settingsListSection: some View {
Section {
self.settingsListRow(
icon: "checkmark.shield.fill",
iconColor: self.pendingApproval == nil ? .green : .orange,
title: "Approvals",
route: .approvals,
badgeValue: self.pendingApproval == nil ? nil : "1")
self.settingsListRow(
icon: "person.2.fill",
iconColor: .blue,
title: "Permissions",
route: .permissions)
self.settingsListRow(
icon: "point.3.connected.trianglepath.dotted",
iconColor: .purple,
title: "Channels",
route: .channels)
self.settingsListRow(
icon: "waveform",
iconColor: .pink,
title: "Voice & Talk",
route: .voice)
}
Section {
self.appearanceRow
self.settingsListRow(
icon: "stethoscope",
iconColor: .teal,
title: "Diagnostics",
route: .diagnostics)
self.settingsListRow(
icon: "hand.raised.fill",
iconColor: .indigo,
title: "Privacy",
route: .privacy)
self.settingsListRow(
icon: "bell.fill",
iconColor: .red,
title: "Notifications",
route: .notifications)
self.settingsListRow(
icon: "info.circle.fill",
iconColor: .gray,
title: "About",
route: .about)
} header: {
Text("Device")
.font(OpenClawType.captionSemiBold)
.foregroundStyle(.secondary)
}
Section {
self.settingsListRow(
icon: "doc.text",
iconColor: .gray,
title: "Licenses",
route: .licenses)
.accessibilityIdentifier("settings-licenses-row")
}
}
func settingsListRow(
icon: String,
iconColor: Color,
title: String,
route: SettingsRoute,
badgeValue: String? = nil) -> some View
{
NavigationLink(value: route) {
Label {
Text(title)
.font(OpenClawType.subheadSemiBold)
} icon: {
SettingsIcon(systemName: icon, color: iconColor)
}
}
.badge(badgeValue.map { Text($0).font(OpenClawType.captionSemiBold) })
}
@ViewBuilder
func destination(for route: SettingsRoute) -> some View {
switch route {
case .channels:
SettingsChannelsDestination()
.navigationTitle(title(for: route))
.navigationBarTitleDisplayMode(.inline)
default:
List {
switch route {
case .gateway:
self.gatewayDestination
case .approvals:
self.approvalsDestination
case .permissions:
self.permissionsDestination
case .voice:
self.voiceDestination
case .diagnostics:
self.diagnosticsDestination
case .privacy:
self.privacyDestination
case .notifications:
self.notificationsDestination
case .about:
self.aboutDestination
case .licenses:
self.licensesDestination
case .channels:
EmptyView()
}
}
.font(OpenClawType.body)
.navigationTitle(title(for: route))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
if let headerLeadingAction {
ToolbarItem(placement: .topBarLeading) {
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
}
}
ToolbarItem(placement: .principal) {
Text(title(for: route))
.font(OpenClawType.headline)
.foregroundStyle(.primary)
}
}
}
}
var gatewayDestination: some View {
Group {
self.detailStatusCard(
icon: "antenna.radiowaves.left.and.right",
title: "Gateway",
detail: self.gatewayStatusDetail,
value: self.gatewayStatusValue,
color: self.gatewayStatusColor)
self.detailListCard {
self.detailRow("Address", value: self.gatewayAddress)
self.detailRow("Server", value: self.gatewayServer)
self.detailRow("Discovered", value: "\(self.gatewayController.gateways.count)")
self.detailRow("Default Agent", value: self.appModel.activeAgentName)
self.detailRow("Agents", value: "\(self.appModel.gatewayAgents.count)")
}
Section {
Button {
Task { await self.reconnectGateway() }
} label: {
Label("Reconnect", systemImage: "arrow.triangle.2.circlepath")
.font(OpenClawType.body)
}
.disabled(self.isReconnectingGateway || self.appModel.isAppleReviewDemoModeEnabled)
Button {
Task { await self.runDiagnostics() }
} label: {
Label("Diagnose", systemImage: "cross.case")
.font(OpenClawType.body)
}
.disabled(self.isRefreshingGateway)
}
self.manualGatewayCard
self.deviceIdentityCard
self.agentSelectionCard
self.gatewaySetupCard
self.discoveredGatewaysCard
self.gatewayAdvancedCard
}
.font(OpenClawType.body)
}
var approvalsDestination: some View {
Group {
self.detailStatusCard(
icon: "checkmark.shield.fill",
title: "Approvals",
detail: self.notificationsNeedAttention
? "Out-of-app approval alerts need notification permission."
: (self.pendingApproval == nil ? "No gateway actions are waiting for review." :
"Review the pending gateway action."),
value: self.notificationsNeedAttention
? "Alerts Off"
: (self.pendingApproval == nil ? "clear" : "1 waiting"),
color: self.notificationsNeedAttention ? OpenClawBrand.warn :
(self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn))
if self.notificationsNeedAttention {
self.approvalNotificationsWarningCard
}
self.approvalsReviewCard
}
}
var approvalNotificationsWarningCard: some View {
Section {
VStack(alignment: .leading, spacing: 4) {
Text("Notifications are off")
.font(OpenClawType.subheadSemiBold)
Text("Enable Notifications to receive approval alerts while OpenClaw is not open.")
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
if self.directRoute == nil {
Button {
self.openNotificationsRouteFromApprovals()
} label: {
Label("Open Notifications", systemImage: "bell.badge")
.font(OpenClawType.body)
}
}
}
}
@ViewBuilder
var approvalsReviewCard: some View {
if let pendingApproval {
Section {
ForEach(self.approvalItems, id: \.id) { item in
SettingsApprovalRow(item: item)
}
if let errorText = self.appModel.pendingExecApprovalPromptErrorText {
Text(errorText)
.font(OpenClawType.caption)
.foregroundStyle(OpenClawBrand.danger)
}
Button {
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-once") }
} label: {
Label("Allow", systemImage: "checkmark")
.font(OpenClawType.body)
}
.disabled(self.appModel.pendingExecApprovalPromptResolving)
if pendingApproval.allowsAllowAlways {
Button {
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-always") }
} label: {
Label("Always Allow", systemImage: "checkmark.shield")
.font(OpenClawType.body)
}
.disabled(self.appModel.pendingExecApprovalPromptResolving)
}
Button(role: .destructive) {
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "deny") }
} label: {
Label("Deny", systemImage: "xmark")
.font(OpenClawType.body)
}
.disabled(self.appModel.pendingExecApprovalPromptResolving)
}
} else {
Section {
Label {
VStack(alignment: .leading, spacing: 2) {
Text("No approvals waiting")
.font(OpenClawType.subheadSemiBold)
Text(self.approvalEmptyDetail)
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
}
} icon: {
Image(systemName: "checkmark.shield.fill")
.foregroundStyle(OpenClawBrand.ok)
}
}
}
}
var permissionsDestination: some View {
Group {
self.toggleCard(
title: "Camera",
isOn: self.$cameraEnabled)
self.locationModeCard
self.toggleCard(
title: "Keep Awake",
isOn: self.$preventSleep)
self.privacyAccessCard
}
}
var voiceDestination: some View {
Group {
self.detailStatusCard(
icon: "waveform",
title: "Voice & Talk",
detail: self.appModel.talkMode.gatewayTalkVoiceModeTitle,
value: self.voiceDetail,
color: self.talkEnabled || self.voiceWakeEnabled ? OpenClawBrand.accent : .secondary)
self.voiceFeatureCard
self.talkVoiceSettingsCard
self.shareSettingsCard
}
}
var diagnosticsDestination: some View {
Group {
self.detailStatusCard(
icon: "checklist.checked",
title: "Health Check",
detail: "Run app, permission, and gateway-adjacent checks without editing setup.",
value: self.diagnosticsHealthValue,
color: self.gatewayDiagnosticConnected ? OpenClawBrand.ok : OpenClawBrand.warn)
Section {
Button {
Task { await self.runDiagnostics() }
} label: {
Label("Run Diagnostics", systemImage: "cross.case")
.font(OpenClawType.body)
}
.disabled(self.isRefreshingGateway)
}
self.diagnosticChecksCard
self.detailListCard {
self.detailRow("Device", value: DeviceInfoHelper.deviceFamily())
self.detailRow("Platform", value: DeviceInfoHelper.platformStringForDisplay())
self.detailRow("App", value: DeviceInfoHelper.openClawVersionString())
self.detailRow("Model", value: DeviceInfoHelper.modelIdentifier())
}
self.diagnosticsAdvancedCard
}
}
var privacyDestination: some View {
Group {
self.detailStatusCard(
icon: "hand.raised",
title: "Privacy",
detail: "Control what device context OpenClaw can expose to the gateway.",
value: self.privacyDetail,
color: .secondary)
self.toggleCard(
title: "Camera Access",
isOn: self.$cameraEnabled)
self.locationModeCard
self.toggleCard(
title: "Background Listening",
isOn: self.$talkBackgroundEnabled)
self.privacyAccessCard
}
}
var notificationsDestination: some View {
Group {
self.detailStatusCard(
icon: "bell",
title: "Notifications",
detail: self.notificationStatusDetail,
value: self.notificationStatusText,
color: self.notificationStatus.color)
Section {
VStack(alignment: .leading, spacing: 12) {
Button {
self.handleNotificationAction()
} label: {
Label(
self.notificationActionText,
systemImage: self.notificationStatus.actionIcon)
.font(OpenClawType.captionSemiBold)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
.disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization)
Text(self.notificationStatusDetail)
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Divider()
HStack(alignment: .top, spacing: 10) {
Image(systemName: "network")
.font(OpenClawType.captionSemiBold)
.foregroundStyle(OpenClawBrand.accent)
.frame(width: 22, height: 22)
Text(self.notificationRelayDetail)
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
}
var gatewayActions: some View {
Group {
self.gatewayActionButton(
title: "Reconnect",
icon: "arrow.triangle.2.circlepath",
color: OpenClawBrand.accent,
isBusy: self.isReconnectingGateway,
isDisabled: self.appModel.isAppleReviewDemoModeEnabled)
{
Task { await self.reconnectGateway() }
}
self.gatewayActionButton(
title: "Diagnose",
icon: "cross.case",
color: OpenClawBrand.accent,
isBusy: self.isRefreshingGateway)
{
Task { await self.runDiagnostics() }
}
}
}
@ViewBuilder var licensesDestination: some View {
let documents = LicenseDocumentLoader.bundledDocuments()
if documents.isEmpty {
ContentUnavailableView(
"No Licenses Bundled",
systemImage: "doc.text",
description: Text("License files are not available in this build."))
.font(OpenClawType.body)
} else {
Section {
ForEach(documents) { document in
NavigationLink {
LicenseDocumentDetailView(document: document)
} label: {
Label {
Text(document.title)
.font(OpenClawType.subhead)
} icon: {
SettingsIcon(systemName: "doc.text", color: .gray)
}
}
}
} footer: {
Text("OpenClaw appreciates its partners in the open-source community.")
.font(OpenClawType.footnote)
}
.accessibilityIdentifier("settings-licenses-list")
}
}
/// Native inset-grouped action row (plain tinted text, no pill chrome).
func gatewayActionButton(
title: String,
icon: String,
color: Color,
isBusy: Bool,
isDisabled: Bool = false,
action: @escaping () -> Void) -> some View
{
Button(action: action) {
HStack {
Label(title, systemImage: icon)
.font(OpenClawType.body)
Spacer()
if isBusy {
ProgressView().controlSize(.small)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.foregroundStyle(color)
.disabled(isBusy || isDisabled)
.accessibilityLabel(title)
}
var aboutDestination: some View {
// Concise public details only; deep hardware identifiers live in Diagnostics.
detailListCard {
self.detailRow("OpenClaw app version", value: DeviceInfoHelper.openClawVersionString())
self.detailRow("Device", value: DeviceInfoHelper.deviceFamily())
self.detailRow("iOS", value: DeviceInfoHelper.iOSVersionStringForDisplay())
}
}
func toggleCard(title: String, isOn: Binding<Bool>) -> some View {
Section {
Toggle(isOn: isOn) {
Text(title)
.font(OpenClawType.body)
}
}
}
var locationModeCard: some View {
Section {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
SettingsIcon(
systemName: "location",
color: self.locationModeRaw == OpenClawLocationMode.off.rawValue ? .secondary : OpenClawBrand
.accent)
VStack(alignment: .leading, spacing: 3) {
Text("Location")
.font(OpenClawType.subheadSemiBold)
Text("Controls whether location can be shared with gateway tools.")
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer(minLength: 8)
if self.isChangingLocationMode {
ProgressView()
.controlSize(.small)
}
}
Picker("Location", selection: self.$locationModeRaw) {
Text("Off")
.font(OpenClawType.captionSemiBold)
.tag(OpenClawLocationMode.off.rawValue)
Text("While Using")
.font(OpenClawType.captionSemiBold)
.tag(OpenClawLocationMode.whileUsing.rawValue)
Text("Always")
.font(OpenClawType.captionSemiBold)
.tag(OpenClawLocationMode.always.rawValue)
}
.pickerStyle(.segmented)
.disabled(self.isChangingLocationMode)
Text(self.locationPermissionDetailText)
.font(OpenClawType.caption2)
.foregroundStyle(
self.locationPermissionSummary.needsAttention ? OpenClawBrand.warn : .secondary)
if let locationPermissionWarningText {
Text(locationPermissionWarningText)
.font(OpenClawType.caption2)
.foregroundStyle(OpenClawBrand.warn)
}
}
}
}
var agentSelectionCard: some View {
Section {
Picker("Default Agent", selection: self.$selectedAgentPickerId) {
Text("Default").font(OpenClawType.body).tag("")
let defaultId = (self.appModel.gatewayDefaultAgentId ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
ForEach(self.appModel.gatewayAgents.filter { $0.id != defaultId }, id: \.id) { agent in
let name = (agent.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
Text(name.isEmpty ? agent.id : name).font(OpenClawType.body).tag(agent.id)
}
}
.font(OpenClawType.body)
} footer: {
Text("Used for new Chat and Talk sessions.")
.font(OpenClawType.footnote)
}
}
var gatewaySetupCard: some View {
Section {
TextField("Paste setup code", text: self.$setupCode)
.font(OpenClawType.body)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
self.gatewayActionButton(
title: "Scan QR",
icon: "qrcode.viewfinder",
color: OpenClawBrand.accent,
isBusy: false,
isDisabled: self.connectingGatewayID != nil)
{
self.openGatewayQRScanner()
}
self.gatewayActionButton(
title: "Connect",
icon: "bolt.horizontal.circle",
color: OpenClawBrand.accent,
isBusy: self.connectingGatewayID == "manual",
isDisabled: !self.canApplyGatewaySetup || self.connectingGatewayID != nil)
{
Task { await self.applySetupCodeAndConnect() }
}
} header: {
Text("Setup Code")
.font(OpenClawType.subheadSemiBold)
} footer: {
if let warning = self.tailnetWarningText {
Text(warning).font(OpenClawType.footnote).foregroundStyle(OpenClawBrand.warn)
} else if let status = self.setupStatusLine {
Text(status)
.font(OpenClawType.footnote)
}
}
}
var discoveredGatewaysCard: some View {
Section("Discovered Gateways") {
if self.gatewayController.gateways.isEmpty {
Text("No gateways found yet. Use manual setup if Bonjour is blocked.")
.font(OpenClawType.subhead)
.foregroundStyle(.secondary)
} else {
ForEach(self.gatewayController.gateways) { gateway in
self.discoveredGatewayRow(gateway)
}
}
}
}
func discoveredGatewayRow(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 3) {
Text(verbatim: gateway.name)
.font(OpenClawType.subheadSemiBold)
Text(verbatim: self.gatewayDetailLines(gateway).joined(separator: ""))
.font(OpenClawType.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
Spacer(minLength: 8)
Button {
Task { await self.connect(gateway) }
} label: {
if self.connectingGatewayID == gateway.id {
ProgressView().controlSize(.small)
} else {
Text("Connect")
.font(OpenClawType.captionSemiBold)
}
}
.font(OpenClawType.captionSemiBold)
.buttonStyle(.bordered)
.disabled(self.connectingGatewayID != nil)
}
}
var manualGatewayCard: some View {
Section("Manual Gateway") {
Toggle(isOn: self.$manualGatewayEnabled) {
Text("Use Manual Gateway")
.font(OpenClawType.body)
}
TextField("Host", text: self.$manualGatewayHost)
.font(OpenClawType.body)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
TextField("Port", text: self.manualPortBinding)
.font(OpenClawType.body)
.keyboardType(.numberPad)
Toggle(isOn: self.$manualGatewayTLS) {
Text("Use TLS")
.font(OpenClawType.body)
}
self.gatewayActionButton(
title: "Connect Manual",
icon: "network",
color: OpenClawBrand.accent,
isBusy: self.connectingGatewayID == "manual",
isDisabled: self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|| !self.manualPortIsValid)
{
Task { await self.connectManual() }
}
}
}
var gatewayAdvancedCard: some View {
Section {
Toggle(isOn: self.$gatewayAutoConnect) {
Text("Auto-connect on launch")
.font(OpenClawType.body)
}
self.gatewaySecureField("Gateway Auth Token", text: self.$gatewayToken)
self.gatewaySecureField("Gateway Password", text: self.$gatewayPassword)
Button(role: .destructive) {
self.showResetOnboardingAlert = true
} label: {
Label("Reset Onboarding", systemImage: "arrow.counterclockwise")
.font(OpenClawType.body)
}
}
}
func gatewaySecureField(_ placeholder: String, text: Binding<String>) -> some View {
ZStack(alignment: .leading) {
SecureField("", text: text)
.font(OpenClawType.subhead)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.accessibilityLabel(placeholder)
if text.wrappedValue.isEmpty {
Text(placeholder)
.font(OpenClawType.subheadSemiBold)
.foregroundStyle(.tertiary)
.padding(.horizontal, 8)
.allowsHitTesting(false)
.accessibilityHidden(true)
}
}
.font(OpenClawType.subhead)
}
var voiceFeatureCard: some View {
Section {
self.settingsToggle("Voice Wake", isOn: self.$voiceWakeEnabled) { enabled in
self.appModel.setVoiceWakeEnabled(enabled)
}
self.settingsToggle("Talk Mode", isOn: self.$talkEnabled) { enabled in
guard !self.appModel.isAppleReviewDemoModeEnabled else {
self.talkEnabled = false
return
}
self.appModel.setTalkEnabled(enabled)
}
.disabled(self.appModel.isAppleReviewDemoModeEnabled)
Picker("Speech Language", selection: self.$talkSpeechLocale) {
ForEach(TalkSpeechLocale.supportedOptions()) { option in
Text(option.label).font(OpenClawType.body).tag(option.id)
}
}
.font(OpenClawType.body)
self.settingsToggle("Background Listening", isOn: self.$talkBackgroundEnabled)
self.settingsToggle("Speakerphone", isOn: self.talkSpeakerphoneBinding)
NavigationLink {
VoiceWakeWordsSettingsView()
} label: {
self.simpleSettingsRow(
title: "Wake Words",
value: VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords))
}
}
}
var talkVoiceSettingsCard: some View {
Group {
if self.gatewayConnected,
let issue = self.appModel.talkMode.gatewayTalkCurrentFallbackIssue
{
Section {
TalkRuntimeIssueBanner(
issue: issue,
onOpenSettings: nil,
onShowDetails: {
self.showTalkIssueDetails = true
})
}
}
Section("Voice") {
Picker("Provider", selection: self.talkProviderSelectionBinding) {
ForEach(TalkModeProviderSelection.allCases) { option in
Text(option.label).font(OpenClawType.body).tag(option.rawValue)
}
}
.font(OpenClawType.body)
if self.shouldShowRealtimeVoicePicker {
Picker("Realtime Voice", selection: self.talkRealtimeVoiceSelectionBinding) {
Text("Gateway Default").font(OpenClawType.body).tag("")
ForEach(TalkModeRealtimeVoiceSelection.voices, id: \.self) { voice in
Text(TalkModeRealtimeVoiceSelection.label(for: voice)).font(OpenClawType.body).tag(voice)
}
}
.font(OpenClawType.body)
}
self.detailRow("Voice Mode", value: self.appModel.talkMode.gatewayTalkVoiceModeTitle)
self.detailRow("Active Voice", value: self.gatewayTalkActiveVoiceDetail)
if let issue = self.gatewayTalkLastIssueDetail {
self.detailRow("Last Voice Issue", value: issue)
}
self.detailRow("Transport", value: self.appModel.talkMode.gatewayTalkTransportLabel)
self.detailRow("API Key", value: self.talkApiKeyStatus)
}
}
}
var shareSettingsCard: some View {
Section {
Toggle(isOn: self.$talkButtonEnabled) {
Text("Show Talk Control")
.font(OpenClawType.body)
}
TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
.font(OpenClawType.body)
.lineLimit(2...5)
.textInputAutocapitalization(.sentences)
Button {
Task { await self.appModel.runSharePipelineSelfTest() }
} label: {
Label("Run Share Self-Test", systemImage: "checkmark.seal")
.font(OpenClawType.body)
}
} footer: {
Text(self.appModel.lastShareEventText)
.font(OpenClawType.footnote)
}
}
var privacyAccessCard: some View {
Section {
PrivacyAccessSectionView()
}
}
var diagnosticsAdvancedCard: some View {
Section {
self.settingsToggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled) { enabled in
self.gatewayController.setDiscoveryDebugLoggingEnabled(enabled)
}
self.settingsToggle("Debug Screen Status", isOn: self.$canvasDebugStatusEnabled)
NavigationLink {
GatewayDiscoveryDebugLogView()
} label: {
self.simpleSettingsRow(title: "Discovery Logs", value: self.gatewayController.discoveryStatusText)
}
}
}
var deviceIdentityCard: some View {
Section("Device") {
TextField("Device Name", text: self.$displayName)
.font(OpenClawType.body)
self.detailRow("Instance ID", value: self.instanceId)
}
}
func settingsToggle(
_ title: String,
isOn: Binding<Bool>,
onChange: ((Bool) -> Void)? = nil) -> some View
{
Toggle(isOn: isOn) {
Text(title)
.font(OpenClawType.subhead)
}
.onChange(of: isOn.wrappedValue) { _, enabled in
onChange?(enabled)
}
}
func simpleSettingsRow(title: String, value: String) -> some View {
LabeledContent(title) {
Text(value)
.font(OpenClawType.subhead)
.lineLimit(1)
.truncationMode(.middle)
}
}
}