voice-vibecoder 2.13.0__tar.gz → 2.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/CHANGELOG.md +3 -0
  2. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/server.py +77 -11
  6. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/session.py +16 -0
  7. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/.github/workflows/ci.yml +0 -0
  8. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/.github/workflows/release.yml +0 -0
  9. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/.gitignore +0 -0
  10. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/LICENSE +0 -0
  11. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/README.md +0 -0
  12. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/app.py +0 -0
  13. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/index.html +0 -0
  14. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/app_config.py +0 -0
  15. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/audio.py +0 -0
  16. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/cli_serve.py +0 -0
  17. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  18. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
  19. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  20. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
  21. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
  22. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/config.py +0 -0
  23. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/instances.py +0 -0
  24. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/tools/__init__.py +0 -0
  25. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
  26. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/tools/definitions.py +0 -0
  27. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
  28. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
  29. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/tools/handlers.py +0 -0
  30. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/__init__.py +0 -0
  31. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
  32. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
  33. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
  34. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/panels.py +0 -0
  35. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/screen.py +0 -0
  36. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/setup.py +0 -0
  37. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/state.py +0 -0
  38. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/styles.py +0 -0
  39. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
  40. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/ui/wizard.py +0 -0
  41. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
  42. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
  43. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
  44. {voice_vibecoder-2.13.0 → voice_vibecoder-2.14.0}/src/voice_vibecoder/worktrees.py +0 -0
@@ -2,6 +2,9 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.14.0 (2026-02-21)
6
+
7
+
5
8
  ## v2.13.0 (2026-02-21)
6
9
 
7
10
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-vibecoder
3
- Version: 2.13.0
3
+ Version: 2.14.0
4
4
  Summary: Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code
5
5
  Project-URL: Homepage, https://github.com/snokam/voice-vibecoder
6
6
  Project-URL: Repository, https://github.com/snokam/voice-vibecoder
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "voice-vibecoder"
7
- version = "2.13.0"
7
+ version = "2.14.0"
8
8
  description = "Olaf The Vibecoder — a voice-controlled coding assistant using OpenAI Realtime API + Claude Code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  A voice-controlled coding assistant using OpenAI Realtime API + Claude Code.
4
4
  """
5
5
 
6
- __version__ = "2.13.0"
6
+ __version__ = "2.14.0"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -9,12 +9,14 @@ Protocol (client -> server):
9
9
  {"type": "sync_state"} — request full state sync from backend
10
10
  {"type": "audio", "data": "<base64 PCM16 24kHz>"}
11
11
  {"type": "audio.commit"}
12
+ {"type": "update_settings", "settings": {...}} — partial settings update, reconnects provider
12
13
 
13
14
  Protocol (server -> client):
14
15
  {"type": "auth_ok"}
15
16
  {"type": "audio", "data": "<base64>"}
16
17
  {"type": "transcript", "role": "user"|"assistant", "text": "..."}
17
- {"type": "status", "status": "ready"|"listening"|"processing"|"responding"}
18
+ {"type": "status", "status": "ready"|"listening"|"processing"|"responding"|"reconnecting"}
19
+ {"type": "settings_updated", "settings": {...}}
18
20
  {"type": "agent_output", "instanceId": "...", "line": "..."}
19
21
  {"type": "instance_status", "instanceId": "...", "status": "...", "branch": "..."}
20
22
  {"type": "tool_call", "name": "...", "arguments": {...}}
@@ -29,6 +31,7 @@ import asyncio
29
31
  import json
30
32
  import logging
31
33
  import os
34
+ from dataclasses import asdict, fields
32
35
  from pathlib import Path
33
36
  from typing import Any
34
37
 
@@ -49,6 +52,34 @@ logger = logging.getLogger(__name__)
49
52
  AUTH_TIMEOUT_SECONDS = 10
50
53
 
51
54
 
55
+ def _merge_settings(settings: RealtimeSettings, incoming: dict[str, Any]) -> None:
56
+ """Merge a partial settings dict into an existing RealtimeSettings.
57
+
58
+ Top-level scalar keys (provider, language, etc.) are replaced directly.
59
+ Nested provider dicts (openai, azure, gemini) are merged at the field level
60
+ so the frontend can send e.g. ``{"gemini": {"model": "..."}}`` without
61
+ needing to include every field.
62
+ """
63
+ from voice_vibecoder.config import OpenAIConfig, AzureConfig, GeminiConfig
64
+
65
+ _PROVIDER_ATTRS = {
66
+ "openai": OpenAIConfig,
67
+ "azure": AzureConfig,
68
+ "gemini": GeminiConfig,
69
+ }
70
+
71
+ for key, value in incoming.items():
72
+ if key in _PROVIDER_ATTRS and isinstance(value, dict):
73
+ # Merge sub-keys into the existing dataclass
74
+ cfg = getattr(settings, key)
75
+ valid_fields = {f.name for f in fields(cfg)}
76
+ for sub_key, sub_value in value.items():
77
+ if sub_key in valid_fields:
78
+ setattr(cfg, sub_key, sub_value)
79
+ elif hasattr(settings, key):
80
+ setattr(settings, key, value)
81
+
82
+
52
83
  class WebSocketBridge:
53
84
  """One bridge per browser connection. Thin adapter between WS frames and RealtimeSession."""
54
85
 
@@ -68,6 +99,7 @@ class WebSocketBridge:
68
99
  self._voice_config = voice_config
69
100
  self._user_email = user_email
70
101
  self._user_jwt = user_jwt
102
+ self._messages_task: asyncio.Task | None = None
71
103
 
72
104
  async def run(self) -> None:
73
105
  provider = create_provider(self._settings)
@@ -99,21 +131,17 @@ class WebSocketBridge:
99
131
  # Sending it here too would cause duplicate output lines.
100
132
  self._send({"type": "status", "status": "ready"})
101
133
 
102
- receive_task = asyncio.create_task(self._receive_loop())
103
- messages_task = asyncio.create_task(self._session.handle_messages())
134
+ self._messages_task = asyncio.create_task(self._session.handle_messages())
104
135
 
105
136
  try:
106
- done, pending = await asyncio.wait(
107
- [receive_task, messages_task],
108
- return_when=asyncio.FIRST_COMPLETED,
109
- )
110
- for task in pending:
111
- task.cancel()
137
+ await self._receive_loop()
138
+ finally:
139
+ if self._messages_task:
140
+ self._messages_task.cancel()
112
141
  try:
113
- await task
142
+ await self._messages_task
114
143
  except (asyncio.CancelledError, Exception):
115
144
  pass
116
- finally:
117
145
  if self._session.connected:
118
146
  await self._session.disconnect()
119
147
 
@@ -139,6 +167,8 @@ class WebSocketBridge:
139
167
  if text and self._session:
140
168
  # Transcript sent by session.inject_user_message
141
169
  await self._session.inject_user_message(text)
170
+ elif msg_type == "update_settings":
171
+ await self._handle_settings_update(msg.get("settings", {}))
142
172
  elif msg_type == "sync_state":
143
173
  # Frontend requests full state sync from backend
144
174
  self._send({"type": "status", "status": "syncing"})
@@ -180,6 +210,42 @@ class WebSocketBridge:
180
210
  "line": f"text:{inst.output_buffer}",
181
211
  })
182
212
 
213
+ async def _handle_settings_update(self, incoming: dict[str, Any]) -> None:
214
+ """Apply a partial settings update, save to disk, and reconnect the provider."""
215
+ if not incoming:
216
+ self._send({"type": "error", "message": "Empty settings update"})
217
+ return
218
+
219
+ _merge_settings(self._settings, incoming)
220
+ self._settings.save()
221
+ logger.info("Settings updated: %s", list(incoming.keys()))
222
+
223
+ if self._session:
224
+ self._send({"type": "status", "status": "reconnecting"})
225
+ try:
226
+ # Stop the current message listener before swapping providers
227
+ if self._messages_task:
228
+ self._messages_task.cancel()
229
+ try:
230
+ await self._messages_task
231
+ except (asyncio.CancelledError, Exception):
232
+ pass
233
+
234
+ await self._session.update_provider(self._settings)
235
+
236
+ # Restart the message listener with the new provider
237
+ self._messages_task = asyncio.create_task(
238
+ self._session.handle_messages()
239
+ )
240
+ self._send({"type": "status", "status": "ready"})
241
+ self._send({
242
+ "type": "settings_updated",
243
+ "settings": self._settings._to_dict(),
244
+ })
245
+ except Exception as e:
246
+ logger.error("Failed to reconnect after settings update: %s", e, exc_info=True)
247
+ self._send({"type": "error", "message": f"Reconnect failed: {e}"})
248
+
183
249
  # -- Callbacks (called from RealtimeSession, possibly from background threads) --
184
250
 
185
251
  def _send(self, msg: dict[str, Any]) -> None:
@@ -128,6 +128,22 @@ class RealtimeSession:
128
128
  if self._on_status:
129
129
  self._on_status("connected")
130
130
 
131
+ async def update_provider(self, new_settings: RealtimeSettings) -> None:
132
+ """Swap the voice provider and reconnect with new settings.
133
+
134
+ Preserves the InstanceRegistry, callbacks, and all agent instances.
135
+ Only the voice provider connection is torn down and recreated.
136
+ """
137
+ self._settings = new_settings
138
+ # Disconnect old provider
139
+ await self._provider.disconnect()
140
+ # Create new provider
141
+ from voice_vibecoder.voice_providers import create_provider
142
+ self._provider = create_provider(new_settings)
143
+ # Connect and reconfigure
144
+ await self._provider.connect()
145
+ await self.configure_session()
146
+
131
147
  async def configure_session(self) -> None:
132
148
  from voice_vibecoder.tools.agent_task import warm_mcp_cache
133
149
  from voice_vibecoder.tools.handlers import get_active_instances_text, get_startup_context