feat: share to openclaw ios app (#19424)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 0a7ab8589a
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-17 20:08:50 +00:00
committed by GitHub
parent 81c5c02e53
commit bfc9736366
19 changed files with 1300 additions and 108 deletions

View File

@@ -6,6 +6,12 @@ import SwiftUI
import UIKit
struct SettingsTab: View {
private struct FeatureHelp: Identifiable {
let id = UUID()
let title: String
let message: String
}
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
@Environment(VoiceWakeManager.self) private var voiceWake: VoiceWakeManager
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
@@ -19,7 +25,6 @@ struct SettingsTab: View {
@AppStorage("talk.voiceDirectiveHint.enabled") private var talkVoiceDirectiveHintEnabled: Bool = true
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
@AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = OpenClawLocationMode.off.rawValue
@AppStorage("location.preciseEnabled") private var locationPreciseEnabled: Bool = true
@AppStorage("screen.preventSleep") private var preventSleep: Bool = true
@AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
@AppStorage("gateway.lastDiscoveredStableID") private var lastDiscoveredGatewayStableID: String = ""
@@ -37,11 +42,10 @@ struct SettingsTab: View {
@AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false
@State private var connectingGatewayID: String?
@State private var localIPAddress: String?
@State private var lastLocationModeRaw: String = OpenClawLocationMode.off.rawValue
@State private var gatewayToken: String = ""
@State private var gatewayPassword: String = ""
@State private var talkElevenLabsApiKey: String = ""
@State private var defaultShareInstruction: String = ""
@AppStorage("gateway.setupCode") private var setupCode: String = ""
@State private var setupStatusText: String?
@State private var manualGatewayPortText: String = ""
@@ -49,6 +53,7 @@ struct SettingsTab: View {
@State private var selectedAgentPickerId: String = ""
@State private var showResetOnboardingAlert: Bool = false
@State private var activeFeatureHelp: FeatureHelp?
@State private var suppressCredentialPersist: Bool = false
private let gatewayLogger = Logger(subsystem: "ai.openclaw.ios", category: "GatewaySettings")
@@ -243,30 +248,22 @@ struct SettingsTab: View {
Section("Device") {
DisclosureGroup("Features") {
Toggle("Voice Wake", isOn: self.$voiceWakeEnabled)
.onChange(of: self.voiceWakeEnabled) { _, newValue in
self.featureToggle(
"Voice Wake",
isOn: self.$voiceWakeEnabled,
help: "Enables wake-word activation to start a hands-free session.") { newValue in
self.appModel.setVoiceWakeEnabled(newValue)
}
Toggle("Talk Mode", isOn: self.$talkEnabled)
.onChange(of: self.talkEnabled) { _, newValue in
self.featureToggle(
"Talk Mode",
isOn: self.$talkEnabled,
help: "Enables voice conversation mode with your connected OpenClaw agent.") { newValue in
self.appModel.setTalkEnabled(newValue)
}
SecureField("Talk ElevenLabs API Key (optional)", text: self.$talkElevenLabsApiKey)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Text("Use this local override when gateway config redacts talk.apiKey for mobile clients.")
.font(.footnote)
.foregroundStyle(.secondary)
Toggle("Background Listening", isOn: self.$talkBackgroundEnabled)
Text("Keep listening when the app is in the background. Uses more battery.")
.font(.footnote)
.foregroundStyle(.secondary)
Toggle("Voice Directive Hint", isOn: self.$talkVoiceDirectiveHintEnabled)
Text("Include ElevenLabs voice switching instructions in the Talk Mode prompt. Disable to save tokens.")
.font(.footnote)
.foregroundStyle(.secondary)
// Keep this separate so users can hide the side bubble without disabling Talk Mode.
Toggle("Show Talk Button", isOn: self.$talkButtonEnabled)
self.featureToggle(
"Background Listening",
isOn: self.$talkBackgroundEnabled,
help: "Keeps listening while the app is backgrounded. Uses more battery.")
NavigationLink {
VoiceWakeWordsSettingsView()
@@ -276,29 +273,78 @@ struct SettingsTab: View {
value: VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords))
}
Toggle("Allow Camera", isOn: self.$cameraEnabled)
Text("Allows the gateway to request photos or short video clips (foreground only).")
.font(.footnote)
.foregroundStyle(.secondary)
self.featureToggle(
"Allow Camera",
isOn: self.$cameraEnabled,
help: "Allows the gateway to request photos or short video clips while OpenClaw is foregrounded.")
HStack(spacing: 8) {
Text("Location Access")
Spacer()
Button {
self.activeFeatureHelp = FeatureHelp(
title: "Location Access",
message: "Controls location permissions for OpenClaw. Off disables location tools, While Using enables foreground location, and Always enables background location.")
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("Location Access info")
}
Picker("Location Access", selection: self.$locationEnabledModeRaw) {
Text("Off").tag(OpenClawLocationMode.off.rawValue)
Text("While Using").tag(OpenClawLocationMode.whileUsing.rawValue)
Text("Always").tag(OpenClawLocationMode.always.rawValue)
}
.labelsHidden()
.pickerStyle(.segmented)
Toggle("Precise Location", isOn: self.$locationPreciseEnabled)
.disabled(self.locationMode == .off)
self.featureToggle(
"Prevent Sleep",
isOn: self.$preventSleep,
help: "Keeps the screen awake while OpenClaw is open.")
Text("Always requires system permission and may prompt to open Settings.")
.font(.footnote)
.foregroundStyle(.secondary)
DisclosureGroup("Advanced") {
self.featureToggle(
"Voice Directive Hint",
isOn: self.$talkVoiceDirectiveHintEnabled,
help: "Adds voice-switching instructions to Talk prompts. Disable to reduce prompt size.")
self.featureToggle(
"Show Talk Button",
isOn: self.$talkButtonEnabled,
help: "Shows the floating Talk button in the main interface.")
TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
.lineLimit(2 ... 6)
.textInputAutocapitalization(.sentences)
HStack(spacing: 8) {
Text("Default Share Instruction")
.font(.footnote)
.foregroundStyle(.secondary)
Spacer()
Button {
self.activeFeatureHelp = FeatureHelp(
title: "Default Share Instruction",
message: "Appends this instruction when sharing content into OpenClaw from iOS.")
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("Default Share Instruction info")
}
Toggle("Prevent Sleep", isOn: self.$preventSleep)
Text("Keeps the screen awake while OpenClaw is open.")
.font(.footnote)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 8) {
Button {
Task { await self.appModel.runSharePipelineSelfTest() }
} label: {
Label("Run Share Self-Test", systemImage: "checkmark.seal")
}
Text(self.appModel.lastShareEventText)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
DisclosureGroup("Device Info") {
@@ -306,19 +352,11 @@ struct SettingsTab: View {
Text(self.instanceId)
.font(.footnote)
.foregroundStyle(.secondary)
LabeledContent("IP", value: self.localIPAddress ?? "")
.contextMenu {
if let ip = self.localIPAddress {
Button {
UIPasteboard.general.string = ip
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
}
}
.lineLimit(1)
.truncationMode(.middle)
LabeledContent("Device", value: self.deviceFamily())
LabeledContent("Platform", value: self.platformString())
LabeledContent("Version", value: self.appVersion())
LabeledContent("Model", value: self.modelIdentifier())
LabeledContent("OpenClaw", value: self.openClawVersionString())
}
}
}
@@ -342,8 +380,13 @@ struct SettingsTab: View {
Text(
"This will disconnect, clear saved gateway connection + credentials, and reopen the onboarding wizard.")
}
.alert(item: self.$activeFeatureHelp) { help in
Alert(
title: Text(help.title),
message: Text(help.message),
dismissButton: .default(Text("OK")))
}
.onAppear {
self.localIPAddress = NetworkInterfaces.primaryIPv4Address()
self.lastLocationModeRaw = self.locationEnabledModeRaw
self.syncManualPortText()
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -351,7 +394,8 @@ struct SettingsTab: View {
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
}
self.talkElevenLabsApiKey = GatewaySettingsStore.loadTalkElevenLabsApiKey() ?? ""
self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
self.appModel.refreshLastShareEventFromRelay()
// Keep setup front-and-center when disconnected; keep things compact once connected.
self.gatewayExpanded = !self.isGatewayConnected
self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
@@ -384,8 +428,8 @@ struct SettingsTab: View {
guard !instanceId.isEmpty else { return }
GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId)
}
.onChange(of: self.talkElevenLabsApiKey) { _, newValue in
GatewaySettingsStore.saveTalkElevenLabsApiKey(newValue)
.onChange(of: self.defaultShareInstruction) { _, newValue in
ShareToAgentSettings.saveDefaultInstruction(newValue)
}
.onChange(of: self.manualGatewayPort) { _, _ in
self.syncManualPortText()
@@ -518,14 +562,6 @@ struct SettingsTab: View {
return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
}
private var locationMode: OpenClawLocationMode {
OpenClawLocationMode(rawValue: self.locationEnabledModeRaw) ?? .off
}
private func appVersion() -> String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
}
private func deviceFamily() -> String {
switch UIDevice.current.userInterfaceIdiom {
case .pad:
@@ -537,14 +573,36 @@ struct SettingsTab: View {
}
}
private func modelIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in
String(bytes: ptr.prefix { $0 != 0 }, encoding: .utf8)
private func openClawVersionString() -> String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
let trimmedBuild = build.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedBuild.isEmpty || trimmedBuild == version {
return version
}
return "\(version) (\(trimmedBuild))"
}
private func featureToggle(
_ title: String,
isOn: Binding<Bool>,
help: String,
onChange: ((Bool) -> Void)? = nil
) -> some View {
HStack(spacing: 8) {
Toggle(title, isOn: isOn)
Button {
self.activeFeatureHelp = FeatureHelp(title: title, message: help)
} label: {
Image(systemName: "info.circle")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.accessibilityLabel("\(title) info")
}
.onChange(of: isOn.wrappedValue) { _, newValue in
onChange?(newValue)
}
let trimmed = machine?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? "unknown" : trimmed
}
private func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async {