voice-vibecoder 2.17.0__tar.gz → 2.17.2__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.0 → voice_vibecoder-2.17.2}/CHANGELOG.md +16 -0
  2. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/openclaw.py +54 -28
  6. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/styles.py +6 -1
  7. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/.github/workflows/ci.yml +0 -0
  8. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/.github/workflows/release.yml +0 -0
  9. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/.gitignore +0 -0
  10. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/LICENSE +0 -0
  11. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/README.md +0 -0
  12. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/app.py +0 -0
  13. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/index.html +0 -0
  14. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/app_config.py +0 -0
  15. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/audio.py +0 -0
  16. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/cli_serve.py +0 -0
  17. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  18. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/claude.py +0 -0
  19. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  20. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/registry.py +0 -0
  21. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/config.py +0 -0
  22. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/instances.py +0 -0
  23. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/server.py +0 -0
  24. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/session.py +0 -0
  25. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/__init__.py +0 -0
  26. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/agent_task.py +0 -0
  27. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/definitions.py +0 -0
  28. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/dispatch.py +0 -0
  29. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/file_tree.py +0 -0
  30. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/handlers.py +0 -0
  31. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/__init__.py +0 -0
  32. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/callbacks.py +0 -0
  33. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/diff_view.py +0 -0
  34. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/help_modal.py +0 -0
  35. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/panels.py +0 -0
  36. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/screen.py +0 -0
  37. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/setup.py +0 -0
  38. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/state.py +0 -0
  39. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/update_modal.py +0 -0
  40. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/wizard.py +0 -0
  41. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
  42. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
  43. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/openai.py +0 -0
  44. {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/worktrees.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.17.2 (2026-02-22)
6
+
7
+ ### Bug Fixes
8
+
9
+ - **openclaw**: Properly split buffered text into lines on flush
10
+ ([`4c65bc6`](https://github.com/snokam/voice-vibecoder/commit/4c65bc693054e03e58df552d1d8a6df195b4d32f))
11
+
12
+
13
+ ## v2.17.1 (2026-02-22)
14
+
15
+ ### Bug Fixes
16
+
17
+ - **openclaw**: Buffer streaming tokens into lines and show connection once
18
+ ([`e2892b8`](https://github.com/snokam/voice-vibecoder/commit/e2892b89100de3ec28e813a8d5dc16506c0b746e))
19
+
20
+
5
21
  ## v2.17.0 (2026-02-22)
6
22
 
7
23
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-vibecoder
3
- Version: 2.17.0
3
+ Version: 2.17.2
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.0"
7
+ version = "2.17.2"
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.0"
6
+ __version__ = "2.17.2"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -113,6 +113,7 @@ class OpenClawRunner:
113
113
  self._ws = None
114
114
 
115
115
  self._device_id, self._private_key, self._device_token = _load_device_keys(self._state_dir)
116
+ self._first_connect = True
116
117
 
117
118
  async def _connect_websocket(self) -> Any:
118
119
  """Connect to OpenClaw Gateway via WebSocket."""
@@ -243,57 +244,81 @@ class OpenClawRunner:
243
244
  }
244
245
 
245
246
  await ws.send(json.dumps(chat_req))
246
-
247
+
247
248
  result_text = ""
248
-
249
+ prev_text_len = 0 # Track streamed text length for delta extraction
250
+ line_buffer = "" # Buffer tokens until we have a complete line
251
+
252
+ def _flush_lines(buf: str, final: bool = False) -> str:
253
+ """Emit complete lines from buffer, return the remaining partial line.
254
+
255
+ When *final* is True, also emit whatever remains in the buffer.
256
+ """
257
+ while "\n" in buf:
258
+ line, buf = buf.split("\n", 1)
259
+ line = line.strip()
260
+ if line and on_output:
261
+ on_output(AgentOutput(category="text", content=line))
262
+ if final:
263
+ remainder = buf.strip()
264
+ if remainder and on_output:
265
+ on_output(AgentOutput(category="text", content=remainder))
266
+ return ""
267
+ return buf
268
+
249
269
  while True:
250
270
  msg = await asyncio.wait_for(ws.recv(), timeout=300.0)
251
271
  data = json.loads(msg)
252
-
272
+
253
273
  event = data.get("event")
254
274
  payload = data.get("payload", {})
255
-
275
+
256
276
  if event == "agent":
257
277
  stream = payload.get("stream")
258
278
  agent_data = payload.get("data", {})
259
-
279
+
260
280
  if stream == "lifecycle":
261
281
  phase = agent_data.get("phase")
262
282
  if phase == "start" and on_output:
263
- on_output(AgentOutput(category="text", content="🤔 Thinking...\n"))
264
-
283
+ on_output(AgentOutput(category="text", content="🤔 Thinking..."))
284
+
265
285
  elif stream == "assistant" and "text" in agent_data:
266
286
  text = agent_data["text"]
267
287
  if text:
268
288
  result_text = text
269
- # Send entire accumulated text with stream: prefix
270
- # Frontend will replace the previous stream line
271
- if on_output:
272
- on_output(AgentOutput(category="stream", content=text))
273
-
289
+ # Extract only the new delta (API sends full text each time)
290
+ delta = text[prev_text_len:]
291
+ prev_text_len = len(text)
292
+ if delta:
293
+ line_buffer += delta
294
+ line_buffer = _flush_lines(line_buffer)
295
+
274
296
  elif stream == "tool":
275
- # Tool execution
297
+ # Flush any buffered text before showing tool output
298
+ line_buffer = _flush_lines(line_buffer, final=True)
299
+ # Reset stream position so next assistant text starts fresh
300
+ prev_text_len = len(result_text)
276
301
  name = agent_data.get("name", "")
277
302
  status = agent_data.get("status", "")
278
303
  if name and on_output:
279
304
  if status == "started":
280
- on_output(AgentOutput(category="tool_call", content=f"\n🔧 [{name}] "))
305
+ on_output(AgentOutput(category="tool_call", content=f"🔧 [{name}]"))
281
306
  elif status == "completed":
282
307
  result = agent_data.get("result", "")
283
308
  if result:
284
309
  preview = str(result)[:200]
285
- on_output(AgentOutput(category="tool_result", content=f"✓ {preview}...\n"))
286
-
310
+ on_output(AgentOutput(category="tool_result", content=f"✓ {preview}"))
311
+
287
312
  elif stream == "thinking":
288
- # Reasoning/thinking
289
- text = agent_data.get("text", "")
290
- delta = agent_data.get("delta", "")
291
- if delta and on_output:
292
- on_output(AgentOutput(category="thinking", content=delta))
313
+ # Thinking deltas — suppress (the "🤔 Thinking..."
314
+ # lifecycle message already indicates the agent is working)
315
+ pass
293
316
 
294
317
  elif event == "chat":
295
318
  state = payload.get("state")
296
319
  if state == "final":
320
+ # Flush remaining buffered text
321
+ line_buffer = _flush_lines(line_buffer, final=True)
297
322
  # Get final message
298
323
  msg_content = payload.get("message", {})
299
324
  if isinstance(msg_content, dict) and "content" in msg_content:
@@ -372,24 +397,25 @@ class OpenClawRunner:
372
397
  """Run a coding task via OpenClaw."""
373
398
  self._task = asyncio.current_task()
374
399
  effective_session = session_id or self._session_id
375
-
376
- if on_output:
400
+
401
+ if self._first_connect and on_output:
377
402
  on_output(AgentOutput(category="text", content="🦞 Kobler til...\n"))
378
-
403
+
379
404
  ws = await self._connect_websocket()
380
405
  if ws:
381
406
  try:
382
- if on_output:
383
- on_output(AgentOutput(category="text", content="🦞 Tilkoblet! Streamer...\n"))
407
+ if self._first_connect and on_output:
408
+ on_output(AgentOutput(category="text", content="🦞 Tilkoblet!\n"))
409
+ self._first_connect = False
384
410
  result = await self._run_via_websocket(ws, message, cwd, effective_session, on_output)
385
411
  if result:
386
412
  return result
387
413
  finally:
388
414
  await ws.close()
389
-
415
+
390
416
  if on_output:
391
417
  on_output(AgentOutput(category="text", content="WebSocket unavailable, using CLI...\n"))
392
-
418
+
393
419
  return await self._run_via_cli(message, cwd, effective_session, on_output)
394
420
 
395
421
  def cancel(self) -> bool:
@@ -118,6 +118,8 @@ _CATEGORY_STYLES = {
118
118
  "file_edit": "green",
119
119
  "bash": "cyan",
120
120
  "text": "",
121
+ "stream": "",
122
+ "thinking": "dim",
121
123
  "user_prompt": "bold blue",
122
124
  "separator": "dim",
123
125
  "question": "bold magenta",
@@ -156,7 +158,7 @@ def _format_output_line(category: str | None, text: str, agent_type: str = "clau
156
158
  if category == "result_error":
157
159
  return f"[bold red]✗[/bold red] {text}"
158
160
 
159
- if category == "text":
161
+ if category in ("text", "stream"):
160
162
  # Agent narration — check for [DONE] / [ERROR] prefixes
161
163
  stripped = text.strip()
162
164
  if stripped.startswith("[DONE]"):
@@ -166,6 +168,9 @@ def _format_output_line(category: str | None, text: str, agent_type: str = "clau
166
168
  badge = f"[bold {agent_color} on #1a3a3a] {agent_info.label} [/bold {agent_color} on #1a3a3a] " if show_badge else ""
167
169
  return f"{badge}[{agent_color}]{text}[/{agent_color}]"
168
170
 
171
+ if category == "thinking":
172
+ return f"[dim]{text}[/dim]"
173
+
169
174
  if category == "tool_call":
170
175
  # MCP tools get tag-style badge: \[Jira] Search → tag + description
171
176
  if text.startswith("\\[") and "]" in text: