mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-01 22:25:45 +00:00
Summary: - Replace the legacy iOS shell with Pro Command, Chat, Agents, and Settings tabs. - Wire iOS chat/session/settings/diagnostics and realtime Talk flows through gateway-backed APIs. - Add gateway/session and shared chat coverage for the new iOS flow. Verification: - git diff --check - node scripts/run-vitest.mjs src/gateway/server.sessions.create.test.ts src/gateway/talk-realtime-relay.test.ts - swift test --filter ChatViewModelTests (apps/shared/OpenClawKit) - xcodebuild build for Nimrod's iPhone succeeded; install succeeded; launch was blocked because the phone was locked Known follow-up: - Preserve traceLevel in sessions.create parent runtime inheritance and keep the changelog credit in the follow-up patch.
101 lines
3.0 KiB
Swift
101 lines
3.0 KiB
Swift
import AVFAudio
|
|
import Foundation
|
|
import OpenClawKit
|
|
import Speech
|
|
|
|
extension TalkModeManager {
|
|
nonisolated static func requestMicrophonePermission() async -> Bool {
|
|
switch AVAudioApplication.shared.recordPermission {
|
|
case .granted:
|
|
return true
|
|
case .denied:
|
|
return false
|
|
case .undetermined:
|
|
return await self.requestPermissionWithTimeout { completion in
|
|
AVAudioApplication.requestRecordPermission(completionHandler: { ok in
|
|
completion(ok)
|
|
})
|
|
}
|
|
@unknown default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
nonisolated static func requestSpeechPermission() async -> Bool {
|
|
let status = SFSpeechRecognizer.authorizationStatus()
|
|
switch status {
|
|
case .authorized:
|
|
return true
|
|
case .denied, .restricted:
|
|
return false
|
|
case .notDetermined:
|
|
break
|
|
@unknown default:
|
|
return false
|
|
}
|
|
|
|
return await self.requestPermissionWithTimeout { completion in
|
|
SFSpeechRecognizer.requestAuthorization { authStatus in
|
|
completion(authStatus == .authorized)
|
|
}
|
|
}
|
|
}
|
|
|
|
private nonisolated static func requestPermissionWithTimeout(
|
|
_ operation: @escaping @Sendable (@escaping @Sendable (Bool) -> Void) -> Void) async -> Bool
|
|
{
|
|
do {
|
|
return try await AsyncTimeout.withTimeout(
|
|
seconds: 8,
|
|
onTimeout: { NSError(domain: "TalkMode", code: 6, userInfo: [
|
|
NSLocalizedDescriptionKey: "permission request timed out",
|
|
]) },
|
|
operation: {
|
|
await withCheckedContinuation(isolation: nil) { cont in
|
|
Task { @MainActor in
|
|
operation { ok in
|
|
cont.resume(returning: ok)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
static func permissionMessage(
|
|
kind: String,
|
|
status: AVAudioSession.RecordPermission) -> String
|
|
{
|
|
switch status {
|
|
case .denied:
|
|
return "\(kind) permission denied"
|
|
case .undetermined:
|
|
return "\(kind) permission not granted"
|
|
case .granted:
|
|
return "\(kind) permission denied"
|
|
@unknown default:
|
|
return "\(kind) permission denied"
|
|
}
|
|
}
|
|
|
|
static func permissionMessage(
|
|
kind: String,
|
|
status: SFSpeechRecognizerAuthorizationStatus) -> String
|
|
{
|
|
switch status {
|
|
case .denied:
|
|
return "\(kind) permission denied"
|
|
case .restricted:
|
|
return "\(kind) permission restricted"
|
|
case .notDetermined:
|
|
return "\(kind) permission not granted"
|
|
case .authorized:
|
|
return "\(kind) permission denied"
|
|
@unknown default:
|
|
return "\(kind) permission denied"
|
|
}
|
|
}
|
|
}
|