diff --git a/apps/ios/README.md b/apps/ios/README.md
index cc7f5cba2..2d0dd056f 100644
--- a/apps/ios/README.md
+++ b/apps/ios/README.md
@@ -1,4 +1,4 @@
-# ClawdisNode (iOS)
+# ClawdisKit (iOS)
Internal-only SwiftUI app scaffold.
diff --git a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift
index 79f5b6d67..cf47b88a4 100644
--- a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift
+++ b/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift
@@ -5,9 +5,10 @@ import Network
@MainActor
final class BridgeDiscoveryModel: ObservableObject {
struct DiscoveredBridge: Identifiable, Equatable {
- var id: String { self.debugID }
+ var id: String { self.stableID }
var name: String
var endpoint: NWEndpoint
+ var stableID: String
var debugID: String
}
@@ -54,7 +55,8 @@ final class BridgeDiscoveryModel: ObservableObject {
return DiscoveredBridge(
name: decodedName,
endpoint: result.endpoint,
- debugID: Self.prettyEndpointDebugID(result.endpoint))
+ stableID: BridgeEndpointID.stableID(result.endpoint),
+ debugID: BridgeEndpointID.prettyDescription(result.endpoint))
default:
return nil
}
@@ -73,8 +75,4 @@ final class BridgeDiscoveryModel: ObservableObject {
self.bridges = []
self.statusText = "Stopped"
}
-
- private static func prettyEndpointDebugID(_ endpoint: NWEndpoint) -> String {
- BonjourEscapes.decode(String(describing: endpoint))
- }
}
diff --git a/apps/ios/Sources/Bridge/BridgeEndpointID.swift b/apps/ios/Sources/Bridge/BridgeEndpointID.swift
new file mode 100644
index 000000000..e5662f433
--- /dev/null
+++ b/apps/ios/Sources/Bridge/BridgeEndpointID.swift
@@ -0,0 +1,19 @@
+import ClawdisKit
+import Foundation
+import Network
+
+enum BridgeEndpointID {
+ static func stableID(_ endpoint: NWEndpoint) -> String {
+ switch endpoint {
+ case let .service(name, type, domain, _):
+ // Keep this stable across encode/decode differences; use raw service tuple.
+ "\(type)|\(domain)|\(name)"
+ default:
+ String(describing: endpoint)
+ }
+ }
+
+ static func prettyDescription(_ endpoint: NWEndpoint) -> String {
+ BonjourEscapes.decode(String(describing: endpoint))
+ }
+}
diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist
index e525cf7b4..05d3889f5 100644
--- a/apps/ios/Sources/Info.plist
+++ b/apps/ios/Sources/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- Clawdis Node
+ ClawdisKit
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -25,11 +25,11 @@
_clawdis-bridge._tcp
NSLocalNetworkUsageDescription
- Clawdis Node discovers and connects to your Clawdis bridge on the local network.
+ ClawdisKit discovers and connects to your Clawdis bridge on the local network.
NSMicrophoneUsageDescription
- Clawdis Node needs microphone access for voice wake.
+ ClawdisKit needs microphone access for voice wake.
NSSpeechRecognitionUsageDescription
- Clawdis Node uses on-device speech recognition for voice wake.
+ ClawdisKit uses on-device speech recognition for voice wake.
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift
index 0c5227f2c..e86f1c677 100644
--- a/apps/ios/Sources/Model/NodeAppModel.swift
+++ b/apps/ios/Sources/Model/NodeAppModel.swift
@@ -9,7 +9,7 @@ final class NodeAppModel: ObservableObject {
@Published var bridgeStatusText: String = "Not connected"
@Published var bridgeServerName: String?
@Published var bridgeRemoteAddress: String?
- @Published var connectedBridgeDebugID: String?
+ @Published var connectedBridgeID: String?
private let bridge = BridgeSession()
private var bridgeTask: Task?
@@ -58,7 +58,7 @@ final class NodeAppModel: ObservableObject {
self.bridgeStatusText = "Connecting…"
self.bridgeServerName = nil
self.bridgeRemoteAddress = nil
- self.connectedBridgeDebugID = BonjourEscapes.decode(String(describing: endpoint))
+ self.connectedBridgeID = BridgeEndpointID.stableID(endpoint)
self.bridgeTask = Task {
do {
@@ -96,14 +96,14 @@ final class NodeAppModel: ObservableObject {
self.bridgeStatusText = "Disconnected"
self.bridgeServerName = nil
self.bridgeRemoteAddress = nil
- self.connectedBridgeDebugID = nil
+ self.connectedBridgeID = nil
}
} catch {
await MainActor.run {
self.bridgeStatusText = "Bridge error: \(error.localizedDescription)"
self.bridgeServerName = nil
self.bridgeRemoteAddress = nil
- self.connectedBridgeDebugID = nil
+ self.connectedBridgeID = nil
}
}
}
@@ -116,7 +116,7 @@ final class NodeAppModel: ObservableObject {
self.bridgeStatusText = "Disconnected"
self.bridgeServerName = nil
self.bridgeRemoteAddress = nil
- self.connectedBridgeDebugID = nil
+ self.connectedBridgeID = nil
}
func sendVoiceTranscript(text: String, sessionKey: String?) async throws {
diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift
index 3b19b2332..1a07f731c 100644
--- a/apps/ios/Sources/Settings/SettingsTab.swift
+++ b/apps/ios/Sources/Settings/SettingsTab.swift
@@ -98,9 +98,9 @@ struct SettingsTab: View {
Text("No bridges found yet.")
.foregroundStyle(.secondary)
} else {
- let connectedID = self.appModel.connectedBridgeDebugID
+ let connectedID = self.appModel.connectedBridgeID
let rows = self.discovery.bridges.filter { bridge in
- let isConnected = bridge.debugID == connectedID
+ let isConnected = bridge.stableID == connectedID
switch showing {
case .all:
return true
@@ -117,10 +117,6 @@ struct SettingsTab: View {
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(bridge.name)
- Text(bridge.debugID)
- .font(.caption2)
- .foregroundStyle(.secondary)
- .lineLimit(1)
}
Spacer()
diff --git a/apps/ios/Sources/Voice/VoiceWakeManager.swift b/apps/ios/Sources/Voice/VoiceWakeManager.swift
index aacb5bba6..91871ca22 100644
--- a/apps/ios/Sources/Voice/VoiceWakeManager.swift
+++ b/apps/ios/Sources/Voice/VoiceWakeManager.swift
@@ -2,6 +2,13 @@ import AVFAudio
import Foundation
import Speech
+private func makeAudioTapEnqueueCallback(queue: AudioBufferQueue) -> AVAudioNodeTapBlock {
+ { buffer, _ in
+ // This callback is invoked on a realtime audio thread/queue. Keep it tiny and nonisolated.
+ queue.enqueueCopy(of: buffer)
+ }
+}
+
private final class AudioBufferQueue: @unchecked Sendable {
private let lock = NSLock()
private var buffers: [AVAudioPCMBuffer] = []
@@ -41,7 +48,7 @@ extension AVAudioPCMBuffer {
let channels = Int(format.channelCount)
let frames = Int(frameLength)
for ch in 0..