feat: voice and model pickers with sync-on-join (Phase 4)

- Voices.swift: 13 Azure voices, default en-US-Ava
- SettingsStore: persist voiceID/modelID, fallback to default voice
- VoiceSession: sync set_voice/set_model on agent_ready, selectVoice/selectModel, track models + model_changed
- SettingsView: voice and model picker sheets
- ControlChannel: publish via publish(data:) reliable data channel instead of sendText DataStream (agent listens on legacy data_received; DataStream text was dropped)
This commit is contained in:
snowspeeder committed 2026-08-30 07:50:45 -04:00
1 parent 5483e6ed48
commit ca2d26f171
8 files changed
+256 -14

No files matched your search

@@ -12,14 +12,16 @@ struct ConversationView: View {
if case .success(let value) = config {
_session = StateObject(wrappedValue: VoiceSession(
config: value,
tokenClient: TokenClient(config: value)
tokenClient: TokenClient(config: value),
settings: settings
))
} else {
let fallback = ServerConfig.parse("https://192.168.86.2:9178")
if case .success(let value) = fallback {
_session = StateObject(wrappedValue: VoiceSession(
config: value,
tokenClient: TokenClient(config: value)
tokenClient: TokenClient(config: value),
settings: settings
))
} else {
fatalError("Unreachable: default server URL is valid")
@@ -73,6 +75,11 @@ struct ConversationView: View {
.font(.caption)
.foregroundStyle(.secondary)
}
if session.state == .ready {
Text("\(Voices.display(for: settings.voiceID) ?? settings.voiceID) · \(session.selectedModel ?? settings.modelID ?? "—")")
.font(.caption)
.foregroundStyle(.secondary)
}
if session.state == .ready {
HStack(spacing: 16) {
Toggle(isOn: Binding(
@@ -168,7 +175,7 @@ struct ConversationView: View {
private var toolbarContent: some ToolbarContent {
ToolbarItem(placement: .topBarTrailing) {
NavigationLink {
SettingsView(settings: settings)
SettingsView(settings: settings, session: session)
} label: {
Image(systemName: "gearshape")
}