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.
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/CHANGELOG.md +16 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/PKG-INFO +1 -1
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/pyproject.toml +1 -1
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/openclaw.py +54 -28
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/styles.py +6 -1
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/.gitignore +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/LICENSE +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/README.md +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/app.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/index.html +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/config.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/server.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/session.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/dispatch.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
- {voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {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.
|
|
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.
|
|
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"
|
{voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/openclaw.py
RENAMED
|
@@ -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
|
|
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
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
#
|
|
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"
|
|
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}
|
|
286
|
-
|
|
310
|
+
on_output(AgentOutput(category="tool_result", content=f"✓ {preview}"))
|
|
311
|
+
|
|
287
312
|
elif stream == "thinking":
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
|
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:
|
|
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.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/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
|
|
File without changes
|
{voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/gemini.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.17.0 → voice_vibecoder-2.17.2}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|