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.
Files changed (44) hide show
  1. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/CHANGELOG.md +16 -0
  2. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/server.py +13 -10
  6. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/session.py +98 -0
  7. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/.github/workflows/ci.yml +0 -0
  8. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/.github/workflows/release.yml +0 -0
  9. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/.gitignore +0 -0
  10. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/LICENSE +0 -0
  11. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/README.md +0 -0
  12. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/app.py +0 -0
  13. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/index.html +0 -0
  14. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/app_config.py +0 -0
  15. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/audio.py +0 -0
  16. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/cli_serve.py +0 -0
  17. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  18. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
  19. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  20. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
  21. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
  22. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/config.py +0 -0
  23. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/instances.py +0 -0
  24. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/__init__.py +0 -0
  25. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
  26. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/definitions.py +0 -0
  27. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
  28. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
  29. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/tools/handlers.py +0 -0
  30. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/__init__.py +0 -0
  31. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
  32. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
  33. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
  34. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/panels.py +0 -0
  35. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/screen.py +0 -0
  36. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/setup.py +0 -0
  37. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/state.py +0 -0
  38. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/styles.py +0 -0
  39. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
  40. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/ui/wizard.py +0 -0
  41. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
  42. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
  43. {voice_vibecoder-2.20.2 → voice_vibecoder-2.21.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
  44. {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.20.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.20.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"
@@ -3,7 +3,7 @@
3
3
  A voice-controlled coding assistant using OpenAI Realtime API + Claude Code.
4
4
  """
5
5
 
6
- __version__ = "2.20.2"
6
+ __version__ = "2.21.0"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -222,17 +222,20 @@ class WebSocketBridge:
222
222
  if not self._session:
223
223
  return
224
224
 
225
- # Send transcript history (voice conversation)
226
- transcripts = self._session.registry.get_transcripts()
227
- logger.info(f"[SYNC_STATE] Sending {len(transcripts)} transcripts to frontend")
228
- for entry in transcripts:
229
- self._send({
230
- "type": "transcript",
231
- "role": entry["role"],
232
- "text": entry["text"],
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 all instances
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: