voice-vibecoder 2.13.0__tar.gz → 2.15.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.
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/CHANGELOG.md +6 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/PKG-INFO +1 -1
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/pyproject.toml +1 -1
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/server.py +83 -11
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/session.py +16 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/.gitignore +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/LICENSE +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/README.md +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/app.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/index.html +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/config.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/worktrees.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voice-vibecoder
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.15.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.
|
|
7
|
+
version = "2.15.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"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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,48 @@ class WebSocketBridge:
|
|
|
180
210
|
"line": f"text:{inst.output_buffer}",
|
|
181
211
|
})
|
|
182
212
|
|
|
213
|
+
# Send current settings so the frontend can render controls
|
|
214
|
+
self._send({
|
|
215
|
+
"type": "settings_updated",
|
|
216
|
+
"settings": self._settings._to_dict(),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
async def _handle_settings_update(self, incoming: dict[str, Any]) -> None:
|
|
220
|
+
"""Apply a partial settings update, save to disk, and reconnect the provider."""
|
|
221
|
+
if not incoming:
|
|
222
|
+
self._send({"type": "error", "message": "Empty settings update"})
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
_merge_settings(self._settings, incoming)
|
|
226
|
+
self._settings.save()
|
|
227
|
+
logger.info("Settings updated: %s", list(incoming.keys()))
|
|
228
|
+
|
|
229
|
+
if self._session:
|
|
230
|
+
self._send({"type": "status", "status": "reconnecting"})
|
|
231
|
+
try:
|
|
232
|
+
# Stop the current message listener before swapping providers
|
|
233
|
+
if self._messages_task:
|
|
234
|
+
self._messages_task.cancel()
|
|
235
|
+
try:
|
|
236
|
+
await self._messages_task
|
|
237
|
+
except (asyncio.CancelledError, Exception):
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
await self._session.update_provider(self._settings)
|
|
241
|
+
|
|
242
|
+
# Restart the message listener with the new provider
|
|
243
|
+
self._messages_task = asyncio.create_task(
|
|
244
|
+
self._session.handle_messages()
|
|
245
|
+
)
|
|
246
|
+
self._send({"type": "status", "status": "ready"})
|
|
247
|
+
self._send({
|
|
248
|
+
"type": "settings_updated",
|
|
249
|
+
"settings": self._settings._to_dict(),
|
|
250
|
+
})
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.error("Failed to reconnect after settings update: %s", e, exc_info=True)
|
|
253
|
+
self._send({"type": "error", "message": f"Reconnect failed: {e}"})
|
|
254
|
+
|
|
183
255
|
# -- Callbacks (called from RealtimeSession, possibly from background threads) --
|
|
184
256
|
|
|
185
257
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/openclaw.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/code_providers/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/voice_providers/gemini.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.13.0 → voice_vibecoder-2.15.0}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|