mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
feat(ios): add background listening core toggle (#18261)
Co-authored-by: Mariano Belinky <mariano@mb-server-643.local>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user