feat: LiveKit voice session, control channel, transcripts (Phase 2)

- VoiceSession: token->connect with fresh-room retry on timeout, agent
  discovery, agent_ready wait, mic enable/mute, text send over lk.chat
- ControlChannel: outbound set_voice/set_model/get_models/set_mode/
  media_state; inbound agent_ready/models/model_changed/task_event
- TranscriptHandler: transcription segments + transcript topic finals,
  partial/final dedupe per role
- ConversationView/OrbView: status orb, mic/text-mode toggles, composer
- ServerConfig: plain ws:// signaling for private IPs (self-signed TLS
  not injectable into SDK WebSocket); Info.plist local networking
This commit is contained in:
snowspeeder committed 2026-08-30 07:26:58 -04:00
1 parent 378b8cb36f
commit 4986b6d7dc
14 files changed
+900 -50

No files matched your search

+17 -2
View File
@@ -33,10 +33,25 @@ struct ServerConfig: Equatable {
c.path = ""
c.fragment = nil
guard let base = c.url else { return .failure(.invalidURL(raw)) }
let portSuffix = components.port.map { ":\($0)" } ?? ""
let livekit = URL(string: "wss://\(host)\(portSuffix)/livekit")!
// LAN deployment: LiveKit is reachable directly on :7880 (plain ws).
// Real domains use wss via same origin. The SDK derives /rtc from the base.
let livekit: URL
if Self.isPrivateIP(host) {
livekit = URL(string: "ws://\(host):7880")!
} else {
let portSuffix = components.port.map { ":\($0)" } ?? ""
livekit = URL(string: "wss://\(host)\(portSuffix)/livekit")!
}
return .success(ServerConfig(baseURL: base, livekitURL: livekit))
}
static func isPrivateIP(_ host: String) -> Bool {
let parts = host.split(separator: ".").compactMap { UInt8($0) }
guard parts.count == 4 else { return false }
return parts[0] == 10
|| (parts[0] == 172 && (16...31).contains(parts[1]))
|| (parts[0] == 192 && parts[1] == 168)
}
var host: String { baseURL.host ?? "" }
}