voice-vibecoder 2.20.2__tar.gz → 2.21.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.20.2 → voice_vibecoder-2.21.0}/CHANGELOG.md +16 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/PKG-INFO +1 -1
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/pyproject.toml +1 -1
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/server.py +13 -10
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/session.py +98 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/.gitignore +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/LICENSE +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/README.md +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/app.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/index.html +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/config.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/worktrees.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.21.0 (2026-04-12)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **session**: Narrate background tool events to voice user
|
|
10
|
+
([`9c28a4f`](https://github.com/snokam/voice-vibecoder/commit/9c28a4f2f6a3f2bc6e6243f11a629ab0e369d7ee))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v2.20.3 (2026-02-23)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Skip transcript and instance replay for text-only clients
|
|
18
|
+
([`ee30512`](https://github.com/snokam/voice-vibecoder/commit/ee30512f1bebf7e8009d815a872d939aeffacf8b))
|
|
19
|
+
|
|
20
|
+
|
|
5
21
|
## v2.20.2 (2026-02-23)
|
|
6
22
|
|
|
7
23
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voice-vibecoder
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.21.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.21.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"
|
|
@@ -222,17 +222,20 @@ class WebSocketBridge:
|
|
|
222
222
|
if not self._session:
|
|
223
223
|
return
|
|
224
224
|
|
|
225
|
-
# Send transcript history (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
225
|
+
# Send transcript history (skip for text-only — they start fresh)
|
|
226
|
+
if not self._session.text_only:
|
|
227
|
+
transcripts = self._session.registry.get_transcripts()
|
|
228
|
+
logger.info(f"[SYNC_STATE] Sending {len(transcripts)} transcripts to frontend")
|
|
229
|
+
for entry in transcripts:
|
|
230
|
+
self._send({
|
|
231
|
+
"type": "transcript",
|
|
232
|
+
"role": entry["role"],
|
|
233
|
+
"text": entry["text"],
|
|
234
|
+
})
|
|
234
235
|
|
|
235
|
-
# Send instance status and history for
|
|
236
|
+
# Send instance status and history (skip for text-only)
|
|
237
|
+
if self._session.text_only:
|
|
238
|
+
return
|
|
236
239
|
for inst in self._session.registry.get_all():
|
|
237
240
|
self._send({
|
|
238
241
|
"type": "instance_status",
|
|
@@ -13,6 +13,7 @@ via the voice provider so ChatGPT can summarize it to the user.
|
|
|
13
13
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import logging
|
|
16
|
+
import time
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any, Callable
|
|
18
19
|
|
|
@@ -49,6 +50,70 @@ logger = logging.getLogger(__name__)
|
|
|
49
50
|
RECONNECT_MAX_ATTEMPTS = 5
|
|
50
51
|
RECONNECT_DELAYS = [2, 4, 8, 16, 30]
|
|
51
52
|
|
|
53
|
+
# Debounce between spoken tool-progress narrations. Prevents the model from
|
|
54
|
+
# interrupting itself mid-filler when many tool events fire in quick succession.
|
|
55
|
+
TOOL_NARRATION_DEBOUNCE_SEC = 6.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Markers used by code-provider runners to tag the kind of tool event:
|
|
59
|
+
# 🔧 tool invocation (tool_call)
|
|
60
|
+
# ✓ tool completed successfully (tool_result)
|
|
61
|
+
# ❌ tool failed (tool_result)
|
|
62
|
+
# ↳ streaming output preview, NOT a completion — skip narration
|
|
63
|
+
_TOOL_CALL_MARKER = "\U0001f527" # 🔧
|
|
64
|
+
_TOOL_OK_MARKER = "\u2713" # ✓
|
|
65
|
+
_TOOL_ERR_MARKER = "\u274c" # ❌
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _friendly_tool_hint(line: str) -> str | None:
|
|
69
|
+
"""Turn a registry tool-event line into a short natural-language hint.
|
|
70
|
+
|
|
71
|
+
Lines look like `"tool_call:🔧 jira_search({...})"` or
|
|
72
|
+
`"tool_result:✓ jira_search: 3 issues"`. Returns None for lines that
|
|
73
|
+
aren't recognized tool starts/completions (e.g. streaming previews)
|
|
74
|
+
so the caller skips narration.
|
|
75
|
+
"""
|
|
76
|
+
if not line or ":" not in line:
|
|
77
|
+
return None
|
|
78
|
+
category, rest = line.split(":", 1)
|
|
79
|
+
if category not in ("tool_call", "tool_result"):
|
|
80
|
+
return None
|
|
81
|
+
rest = rest.strip()
|
|
82
|
+
# Must be one of the known markers — otherwise it's a preview or
|
|
83
|
+
# something we don't want to narrate.
|
|
84
|
+
if category == "tool_call":
|
|
85
|
+
if not rest.startswith(_TOOL_CALL_MARKER):
|
|
86
|
+
return None
|
|
87
|
+
verb = "using"
|
|
88
|
+
else:
|
|
89
|
+
if rest.startswith(_TOOL_OK_MARKER):
|
|
90
|
+
verb = "finished"
|
|
91
|
+
elif rest.startswith(_TOOL_ERR_MARKER):
|
|
92
|
+
verb = "had trouble with"
|
|
93
|
+
else:
|
|
94
|
+
return None
|
|
95
|
+
rest = rest[1:].lstrip()
|
|
96
|
+
for sep in ("(", ":", " "):
|
|
97
|
+
if sep in rest:
|
|
98
|
+
rest = rest.split(sep, 1)[0]
|
|
99
|
+
raw = rest.strip()
|
|
100
|
+
if not raw:
|
|
101
|
+
return None
|
|
102
|
+
name = raw.split(".", 1)[-1].replace("_", " ").replace("-", " ").strip()
|
|
103
|
+
if not name:
|
|
104
|
+
return None
|
|
105
|
+
if verb == "using":
|
|
106
|
+
return f"using {name}"
|
|
107
|
+
if verb == "finished":
|
|
108
|
+
return f"finished {name}, preparing answer"
|
|
109
|
+
return f"had trouble with {name}, trying another way"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
_TOOL_NARRATION_PROMPT = (
|
|
113
|
+
"[agent background status: {hint}. Let the user know naturally in "
|
|
114
|
+
"whatever language they are speaking. Don't guess at the results.]"
|
|
115
|
+
)
|
|
116
|
+
|
|
52
117
|
|
|
53
118
|
class RealtimeSession:
|
|
54
119
|
"""Manages a single voice session with multi-instance support."""
|
|
@@ -89,6 +154,7 @@ class RealtimeSession:
|
|
|
89
154
|
self._response_pending = False
|
|
90
155
|
self._fallback_task: asyncio.Task | None = None
|
|
91
156
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
157
|
+
self._last_tool_narration: float = 0.0
|
|
92
158
|
|
|
93
159
|
# Create instance registry and wire callbacks
|
|
94
160
|
self._registry = InstanceRegistry(user_email=user_email, user_jwt=user_jwt)
|
|
@@ -322,6 +388,38 @@ class RealtimeSession:
|
|
|
322
388
|
def _on_registry_output(self, instance_id: str, line: str) -> None:
|
|
323
389
|
if self._on_agent_output:
|
|
324
390
|
self._on_agent_output(instance_id, line)
|
|
391
|
+
self._maybe_narrate_tool_event(line)
|
|
392
|
+
|
|
393
|
+
def _maybe_narrate_tool_event(self, line: str) -> None:
|
|
394
|
+
"""Speak a short filler phrase when a background tool event fires.
|
|
395
|
+
|
|
396
|
+
Without this, a caller hears 30–60s of silence while the agent runs
|
|
397
|
+
MCP tools. Debounced to TOOL_NARRATION_DEBOUNCE_SEC so many rapid
|
|
398
|
+
tool events coalesce into at most one spoken hint. Called from
|
|
399
|
+
background threads, so the injection is bounced to the session's
|
|
400
|
+
asyncio loop.
|
|
401
|
+
"""
|
|
402
|
+
if self.text_only or not self._running:
|
|
403
|
+
return
|
|
404
|
+
if not isinstance(line, str):
|
|
405
|
+
return
|
|
406
|
+
hint = _friendly_tool_hint(line)
|
|
407
|
+
if not hint:
|
|
408
|
+
return
|
|
409
|
+
now = time.monotonic()
|
|
410
|
+
if (now - self._last_tool_narration) < TOOL_NARRATION_DEBOUNCE_SEC:
|
|
411
|
+
return
|
|
412
|
+
loop = self._loop
|
|
413
|
+
if loop is None:
|
|
414
|
+
return
|
|
415
|
+
self._last_tool_narration = now
|
|
416
|
+
text = _TOOL_NARRATION_PROMPT.format(hint=hint)
|
|
417
|
+
try:
|
|
418
|
+
asyncio.run_coroutine_threadsafe(
|
|
419
|
+
self._provider.inject_message(text), loop,
|
|
420
|
+
)
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.warning("Tool narration inject failed: %s", e)
|
|
325
423
|
|
|
326
424
|
def _on_registry_status_change(self, instance_id: str, status: InstanceStatus) -> None:
|
|
327
425
|
if self._on_instance_status:
|
|
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.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/openclaw.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.20.2 → voice_vibecoder-2.21.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.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/gemini.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|