feat(ios): add background listening core toggle (#18261)

Co-authored-by: Mariano Belinky <mariano@mb-server-643.local>
This commit is contained in:
Mariano
2026-02-16 17:36:17 +00:00
committed by GitHub
parent ad27716d3f
commit b3859b488c
4 changed files with 25 additions and 6 deletions

View File

@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
- Cron/Gateway: separate per-job webhook delivery (`delivery.mode = "webhook"`) from announce delivery, enforce valid HTTP(S) webhook URLs, and keep a temporary legacy `notify + cron.webhook` fallback for stored jobs. (#17901) Thanks @advaitpaliwal.
- iOS/Talk: add a `Voice Directive Hint` toggle for Talk Mode prompts so users can disable ElevenLabs voice-switching instructions to save tokens when not needed. (#18250) Thanks @zeulewan.
- iOS/Talk: add a `Background Listening` toggle that keeps Talk Mode active while the app is backgrounded (off by default for battery safety). Thanks @zeulewan.
- Telegram/Agents: add inline button `style` support (`primary|success|danger`) across message tool schema, Telegram action parsing, send pipeline, and runtime prompt guidance. (#18241) Thanks @obviyus.
### Fixes

View File

@@ -114,6 +114,7 @@ final class NodeAppModel {
private var talkVoiceWakeSuspended = false
private var backgroundVoiceWakeSuspended = false
private var backgroundTalkSuspended = false
private var backgroundTalkKeptActive = false
private var backgroundedAt: Date?
private var reconnectAfterBackgroundArmed = false
@@ -269,15 +270,18 @@ final class NodeAppModel {
func setScenePhase(_ phase: ScenePhase) {
let keepTalkActive = UserDefaults.standard.bool(forKey: "talk.background.enabled")
switch phase {
case .background:
self.isBackgrounded = true
self.stopGatewayHealthMonitor()
self.backgroundedAt = Date()
self.reconnectAfterBackgroundArmed = true
// Be conservative: release the mic when the app backgrounds.
// Release voice wake mic in background.
self.backgroundVoiceWakeSuspended = self.voiceWake.suspendForExternalAudioCapture()
self.backgroundTalkSuspended = self.talkMode.suspendForBackground()
let shouldKeepTalkActive = keepTalkActive && self.talkMode.isEnabled
self.backgroundTalkKeptActive = shouldKeepTalkActive
self.backgroundTalkSuspended = self.talkMode.suspendForBackground(keepActive: shouldKeepTalkActive)
case .active, .inactive:
self.isBackgrounded = false
if self.operatorConnected {
@@ -289,8 +293,12 @@ final class NodeAppModel {
Task { [weak self] in
guard let self else { return }
let suspended = await MainActor.run { self.backgroundTalkSuspended }
await MainActor.run { self.backgroundTalkSuspended = false }
await self.talkMode.resumeAfterBackground(wasSuspended: suspended)
let keptActive = await MainActor.run { self.backgroundTalkKeptActive }
await MainActor.run {
self.backgroundTalkSuspended = false
self.backgroundTalkKeptActive = false
}
await self.talkMode.resumeAfterBackground(wasSuspended: suspended, wasKeptActive: keptActive)
}
}
if phase == .active, self.reconnectAfterBackgroundArmed {

View File

@@ -15,6 +15,7 @@ struct SettingsTab: View {
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
@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
@@ -256,6 +257,10 @@ struct SettingsTab: View {
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)

View File

@@ -219,8 +219,12 @@ final class TalkModeManager: NSObject {
/// Suspends microphone usage without disabling Talk Mode.
/// Used when the app backgrounds (or when we need to temporarily release the mic).
func suspendForBackground() -> Bool {
func suspendForBackground(keepActive: Bool = false) -> Bool {
guard self.isEnabled else { return false }
if keepActive {
self.statusText = self.isListening ? "Listening" : self.statusText
return false
}
let wasActive = self.isListening || self.isSpeaking || self.isPushToTalkActive
self.isListening = false
@@ -247,7 +251,8 @@ final class TalkModeManager: NSObject {
return wasActive
}
func resumeAfterBackground(wasSuspended: Bool) async {
func resumeAfterBackground(wasSuspended: Bool, wasKeptActive: Bool = false) async {
if wasKeptActive { return }
guard wasSuspended else { return }
guard self.isEnabled else { return }
await self.start()