voice-vibecoder 2.17.6__tar.gz → 2.18.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.17.6 → voice_vibecoder-2.18.0}/CHANGELOG.md +8 -0
  2. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/server.py +36 -14
  6. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/session.py +6 -0
  7. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/.github/workflows/ci.yml +0 -0
  8. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/.github/workflows/release.yml +0 -0
  9. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/.gitignore +0 -0
  10. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/LICENSE +0 -0
  11. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/README.md +0 -0
  12. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/app.py +0 -0
  13. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/index.html +0 -0
  14. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/app_config.py +0 -0
  15. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/audio.py +0 -0
  16. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/cli_serve.py +0 -0
  17. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  18. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
  19. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  20. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
  21. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
  22. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/config.py +0 -0
  23. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/instances.py +0 -0
  24. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/tools/__init__.py +0 -0
  25. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
  26. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/tools/definitions.py +0 -0
  27. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/tools/dispatch.py +0 -0
  28. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
  29. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/tools/handlers.py +0 -0
  30. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/__init__.py +0 -0
  31. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
  32. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
  33. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
  34. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/panels.py +0 -0
  35. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/screen.py +0 -0
  36. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/setup.py +0 -0
  37. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/state.py +0 -0
  38. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/styles.py +0 -0
  39. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
  40. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/ui/wizard.py +0 -0
  41. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
  42. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
  43. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/voice_providers/openai.py +0 -0
  44. {voice_vibecoder-2.17.6 → voice_vibecoder-2.18.0}/src/voice_vibecoder/worktrees.py +0 -0
@@ -2,6 +2,14 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.18.0 (2026-02-23)
6
+
7
+ ### Features
8
+
9
+ - **server**: Text-only mode bypasses voice provider
10
+ ([`1b62fcf`](https://github.com/snokam/voice-vibecoder/commit/1b62fcfffa15ce44a911781761610c533a69bac8))
11
+
12
+
5
13
  ## v2.17.6 (2026-02-23)
6
14
 
7
15
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-vibecoder
3
- Version: 2.17.6
3
+ Version: 2.18.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.17.6"
7
+ version = "2.18.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.17.6"
6
+ __version__ = "2.18.0"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -132,18 +132,16 @@ class WebSocketBridge:
132
132
  on_ui_command=self._on_ui_command,
133
133
  )
134
134
 
135
+ # Set event loop so _send works even before voice provider connects
136
+ self._session._loop = asyncio.get_event_loop()
137
+
135
138
  # Load saved instances — panels appear dynamically as sessions/instances are created
136
139
  restored = self._session.registry.load_state()
137
140
 
138
- await self._session.connect()
139
- await self._session.configure_session()
140
- # Don't sync state here — the frontend sends sync_state after
141
- # connecting (and after auth), which triggers _sync_state_to_frontend.
142
- # Sending it here too would cause duplicate output lines.
141
+ # Don't connect voice provider yet — wait for sync_state with audio: true.
142
+ # Text-only clients skip the voice provider entirely.
143
143
  self._send({"type": "status", "status": "ready"})
144
144
 
145
- self._messages_task = asyncio.create_task(self._session.handle_messages())
146
-
147
145
  try:
148
146
  await self._receive_loop()
149
147
  finally:
@@ -156,6 +154,14 @@ class WebSocketBridge:
156
154
  if self._session.connected:
157
155
  await self._session.disconnect()
158
156
 
157
+ async def _connect_voice_provider(self) -> None:
158
+ """Lazily connect the voice provider when audio is needed."""
159
+ if self._session.connected:
160
+ return
161
+ await self._session.connect()
162
+ await self._session.configure_session()
163
+ self._messages_task = asyncio.create_task(self._session.handle_messages())
164
+
159
165
  async def _receive_loop(self) -> None:
160
166
  try:
161
167
  async for raw in self._ws:
@@ -167,32 +173,48 @@ class WebSocketBridge:
167
173
  msg_type = msg.get("type")
168
174
  if msg_type == "audio":
169
175
  data = msg.get("data", "")
170
- if data and self._session:
176
+ if data and self._session and self._session.connected:
171
177
  await self._session.send_audio(data)
172
178
  elif msg_type == "audio.commit":
173
- if self._session:
179
+ if self._session and self._session.connected:
174
180
  await self._session.commit_audio()
175
181
  elif msg_type == "text":
176
- # Text-only mode: inject user message directly
177
182
  text = msg.get("text", "") or msg.get("message", "")
178
183
  if text and self._session:
179
- # Transcript sent by session.inject_user_message
180
- await self._session.inject_user_message(text)
184
+ if self._session.connected:
185
+ # Voice mode: route through voice provider
186
+ await self._session.inject_user_message(text)
187
+ else:
188
+ # Text-only mode: route directly to agent
189
+ await self._handle_text_only(text)
181
190
  elif msg_type == "update_settings":
182
191
  await self._handle_settings_update(msg.get("settings", {}))
183
192
  elif msg_type == "sync_state":
184
- # Frontend requests full state sync from backend.
185
- # Opt-in to ui_command messages (file_tree, show_diff, etc.)
193
+ # Opt-in flags
186
194
  if msg.get("ui_commands"):
187
195
  self._ui_commands_enabled = True
188
196
  if msg.get("audio"):
189
197
  self._audio_enabled = True
198
+ # Connect voice provider now (lazy)
199
+ await self._connect_voice_provider()
190
200
  self._send({"type": "status", "status": "syncing"})
191
201
  await self._sync_state_to_frontend()
192
202
  self._send({"type": "status", "status": "ready"})
193
203
  except websockets.ConnectionClosed:
194
204
  pass
195
205
 
206
+ async def _handle_text_only(self, text: str) -> None:
207
+ """Route text directly to an agent session, bypassing the voice provider.
208
+
209
+ Used for text-only clients that don't need audio. The agent streams
210
+ output via on_agent_output and sends the final result as a transcript.
211
+ """
212
+ from voice_vibecoder.tools.agent_task import _handle_send_to_session
213
+
214
+ self._session.text_only = True
215
+ self._send({"type": "transcript", "role": "user", "text": text})
216
+ _handle_send_to_session(text)
217
+
196
218
  async def _sync_state_to_frontend(self) -> None:
197
219
  """Send full backend state to frontend - all instances with their conversation history."""
198
220
  if not self._session:
@@ -85,6 +85,7 @@ class RealtimeSession:
85
85
  self._on_ui_command = on_ui_command
86
86
  self._running = False
87
87
  self._assistant_transcript = ""
88
+ self.text_only = False
88
89
  self._response_pending = False
89
90
  self._fallback_task: asyncio.Task | None = None
90
91
  self._loop: asyncio.AbstractEventLoop | None = None
@@ -358,6 +359,11 @@ class RealtimeSession:
358
359
  # Show only the first line as summary — full result was already streamed
359
360
  first_line = result.strip().split("\n", 1)[0][:120]
360
361
  self._registry.on_output(instance_id, f"{prefix} {first_line}")
362
+ if self.text_only:
363
+ # Text-only mode: send result as transcript directly (no voice)
364
+ if self._on_transcript:
365
+ self._on_transcript("assistant", result)
366
+ return
361
367
  if self._loop and self._running:
362
368
  inst = self._registry.get_by_id(instance_id)
363
369
  display_key = inst.display_key if inst else "unknown"