voice-vibecoder 2.25.0__tar.gz → 2.26.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 (50) hide show
  1. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/CHANGELOG.md +11 -0
  2. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/config.py +12 -0
  6. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/session.py +1 -1
  7. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/definitions.py +20 -0
  8. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/dispatch.py +3 -0
  9. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/handlers.py +29 -0
  10. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/__init__.py +1 -0
  11. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/openai.py +11 -2
  12. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/worktrees.py +73 -0
  13. voice_vibecoder-2.26.0/tests/test_merge_branch.py +78 -0
  14. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/.github/workflows/ci.yml +0 -0
  15. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/.github/workflows/release.yml +0 -0
  16. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/.gitignore +0 -0
  17. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/LICENSE +0 -0
  18. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/README.md +0 -0
  19. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/app.py +0 -0
  20. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/index.html +0 -0
  21. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/app_config.py +0 -0
  22. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio.py +0 -0
  23. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio_macos.py +0 -0
  24. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/cli_serve.py +0 -0
  25. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  26. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
  27. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  28. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
  29. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
  30. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/instances.py +0 -0
  31. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/server.py +0 -0
  32. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/__init__.py +0 -0
  33. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
  34. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
  35. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/__init__.py +0 -0
  36. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
  37. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
  38. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
  39. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/panels.py +0 -0
  40. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/screen.py +0 -0
  41. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/setup.py +0 -0
  42. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/state.py +0 -0
  43. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/styles.py +0 -0
  44. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
  45. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/wizard.py +0 -0
  46. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
  47. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/tests/test_audio_gating.py +0 -0
  48. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/tests/test_file_tree.py +0 -0
  49. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/tests/test_session_interrupts.py +0 -0
  50. {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/tests/test_startup_context.py +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v2.26.0 (2026-07-13)
6
+
7
+ ### Features
8
+
9
+ - **providers**: Pass the configured language to transcription
10
+ ([`cc06dcb`](https://github.com/snokam/voice-vibecoder/commit/cc06dcb45e9844827cfa2792b88ee3561f8c6a38))
11
+
12
+ - **tools**: Merge a branch into main by voice
13
+ ([`8fbddd6`](https://github.com/snokam/voice-vibecoder/commit/8fbddd6da288634e4ab3a80da9d86cba9c1d1e8f))
14
+
15
+
5
16
  ## v2.25.0 (2026-07-13)
6
17
 
7
18
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-vibecoder
3
- Version: 2.25.0
3
+ Version: 2.26.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.25.0"
7
+ version = "2.26.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.25.0"
6
+ __version__ = "2.26.0"
7
7
 
8
8
  from voice_vibecoder.app_config import VoiceConfig
9
9
  from voice_vibecoder.ui.screen import VoiceCodingScreen
@@ -77,6 +77,10 @@ Avbryt/reset:
77
77
  - "start på nytt", "reset" → reset_agent
78
78
  - remove_branch_instance / remove_session for å fjerne instanser.
79
79
 
80
+ Merge:
81
+ - "merge det", "ta det inn i main" → merge_branch. Ved konflikter: foreslå å be \
82
+ agenten merge main inn i sin branch først.
83
+
80
84
  Visning:
81
85
  - "vis diff" → show_diff
82
86
  - "vis output", "gå tilbake" → show_output
@@ -132,6 +136,10 @@ Cancel/reset:
132
136
  - "start fresh", "reset" → reset_agent
133
137
  - remove_branch_instance / remove_session to remove instances.
134
138
 
139
+ Merge:
140
+ - "merge it", "bring it into main" → merge_branch. On conflicts: suggest asking \
141
+ the agent to merge main into its branch first.
142
+
135
143
  Views:
136
144
  - "show diff" → show_diff
137
145
  - "show output", "go back" → show_output
@@ -187,6 +195,10 @@ Avbryt/reset:
187
195
  - "börja om", "reset" → reset_agent
188
196
  - remove_branch_instance / remove_session för att ta bort instanser.
189
197
 
198
+ Merge:
199
+ - "merga det", "ta in det i main" → merge_branch. Vid konflikter: föreslå att \
200
+ agenten mergar main in i sin branch först.
201
+
190
202
  Visning:
191
203
  - "visa diff" → show_diff
192
204
  - "visa output", "gå tillbaka" → show_output
@@ -608,7 +608,7 @@ class RealtimeSession:
608
608
  # -- Function call handling --
609
609
 
610
610
  # Tools that might do slow I/O (git subprocess, diff generation)
611
- _SLOW_TOOLS = {"show_diff", "list_branches", "delete_worktree"}
611
+ _SLOW_TOOLS = {"show_diff", "list_branches", "delete_worktree", "merge_branch"}
612
612
 
613
613
  async def _handle_function_call(self, call_id: str, name: str, arguments: dict) -> None:
614
614
  if self._on_tool_call:
@@ -332,6 +332,26 @@ _CORE_TOOLS: list[dict[str, Any]] = [
332
332
  "required": ["session_name"],
333
333
  },
334
334
  },
335
+ {
336
+ "type": "function",
337
+ "name": "merge_branch",
338
+ "description": (
339
+ "Merge a branch's work into the main branch. Commits any pending "
340
+ "work in the branch's worktree first. Use when the user approves "
341
+ "the changes and wants them merged (e.g. 'merge it', 'ta det inn "
342
+ "i main'). Reports conflicts without merging if any."
343
+ ),
344
+ "parameters": {
345
+ "type": "object",
346
+ "properties": {
347
+ "branch": {
348
+ "type": "string",
349
+ "description": "The branch to merge into main. Fuzzy-matched.",
350
+ },
351
+ },
352
+ "required": ["branch"],
353
+ },
354
+ },
335
355
  {
336
356
  "type": "function",
337
357
  "name": "delete_worktree",
@@ -199,6 +199,7 @@ def handle_tool_call(name: str, arguments: dict[str, Any]) -> str:
199
199
  _handle_delete_worktree,
200
200
  _handle_get_agent_status,
201
201
  _handle_list_branches,
202
+ _handle_merge_branch,
202
203
  _handle_remove_branch_instance,
203
204
  _handle_remove_session,
204
205
  _handle_reset_agent,
@@ -228,6 +229,8 @@ def handle_tool_call(name: str, arguments: dict[str, Any]) -> str:
228
229
  )
229
230
  elif name == "create_branch_instance":
230
231
  return _handle_create_branch_instance(arguments.get("branch", ""))
232
+ elif name == "merge_branch":
233
+ return _handle_merge_branch(arguments.get("branch", ""))
231
234
  elif name == "get_agent_status":
232
235
  return _handle_get_agent_status(arguments.get("branch"), arguments.get("session_name"))
233
236
  elif name == "cancel_agent":
@@ -495,6 +495,35 @@ def _handle_remove_session(session_name: str) -> str:
495
495
  return f"Removed session '{instance.display_name}'."
496
496
 
497
497
 
498
+ def _handle_merge_branch(branch: str) -> str:
499
+ if not branch.strip():
500
+ return "Error: branch name is required."
501
+ if not _dispatch._repo_root:
502
+ return "Error: repo root not configured."
503
+
504
+ from voice_vibecoder.tools.dispatch import _fuzzy_match_branch
505
+ from voice_vibecoder.worktrees import list_worktrees, merge_branch
506
+
507
+ worktrees = list_worktrees(_dispatch._repo_root)
508
+ wt_branches = [w.branch for w in worktrees if w.branch and not w.is_main]
509
+ resolved = _fuzzy_match_branch(branch, wt_branches) or branch
510
+
511
+ result = merge_branch(resolved, _dispatch._repo_root)
512
+ if result.merged:
513
+ return (
514
+ f"{result.message} The worktree is still there if more work is "
515
+ "needed — say so if it should be cleaned up (delete_worktree)."
516
+ )
517
+ if result.conflict_files:
518
+ files = ", ".join(result.conflict_files[:5])
519
+ return (
520
+ f"{result.message} Conflicting files: {files}. "
521
+ "Suggest sending the agent a request to resolve the conflicts by "
522
+ "merging main into its branch first."
523
+ )
524
+ return result.message
525
+
526
+
498
527
  def _handle_delete_worktree(branch: str, delete_branch: bool = False) -> str:
499
528
  if not branch.strip():
500
529
  return "Error: branch name is required."
@@ -144,6 +144,7 @@ def create_provider(settings) -> VoiceProvider:
144
144
  ws_url=settings.ws_url,
145
145
  ws_headers=settings.ws_headers,
146
146
  transcription_model="whisper-1" if is_azure else "gpt-4o-mini-transcribe",
147
+ transcription_language=settings.language,
147
148
  # Azure realtime is the GA API (/openai/v1/realtime); it uses the nested
148
149
  # session schema and response.output_audio.* events.
149
150
  ga=is_azure,
@@ -41,11 +41,13 @@ class OpenAIVoiceProvider:
41
41
  ws_url: str,
42
42
  ws_headers: dict[str, str],
43
43
  transcription_model: str = "gpt-4o-mini-transcribe",
44
+ transcription_language: str = "",
44
45
  ga: bool = False,
45
46
  ) -> None:
46
47
  self._ws_url = ws_url
47
48
  self._ws_headers = ws_headers
48
49
  self._transcription_model = transcription_model
50
+ self._transcription_language = transcription_language
49
51
  # GA realtime API (Azure /openai/v1/realtime): nested `audio` session
50
52
  # schema + response.output_audio.* events. Preview otherwise.
51
53
  self._ga = ga
@@ -76,7 +78,10 @@ class OpenAIVoiceProvider:
76
78
  "turn_detection": self._turn_detection(vad),
77
79
  }
78
80
  if self._transcription_model:
79
- audio_input["transcription"] = {"model": self._transcription_model}
81
+ transcription = {"model": self._transcription_model}
82
+ if self._transcription_language:
83
+ transcription["language"] = self._transcription_language
84
+ audio_input["transcription"] = transcription
80
85
  session: dict[str, Any] = {
81
86
  "type": "realtime",
82
87
  "output_modalities": ["audio"],
@@ -99,7 +104,11 @@ class OpenAIVoiceProvider:
99
104
  "voice": voice,
100
105
  "input_audio_format": "pcm16",
101
106
  "output_audio_format": "pcm16",
102
- "input_audio_transcription": {"model": self._transcription_model},
107
+ "input_audio_transcription": (
108
+ {"model": self._transcription_model, "language": self._transcription_language}
109
+ if self._transcription_language
110
+ else {"model": self._transcription_model}
111
+ ),
103
112
  "tools": tools,
104
113
  "turn_detection": self._turn_detection(vad),
105
114
  }
@@ -22,6 +22,12 @@ def configure_paths(worktrees_dir: Path) -> None:
22
22
  WORKTREES_DIR = worktrees_dir
23
23
 
24
24
 
25
+ def _git(args: list[str], cwd: str) -> subprocess.CompletedProcess:
26
+ return subprocess.run(
27
+ ["git", *args], cwd=cwd, capture_output=True, text=True, timeout=30,
28
+ )
29
+
30
+
25
31
  @dataclass
26
32
  class WorktreeInfo:
27
33
  path: Path
@@ -191,6 +197,73 @@ def remove_worktree(branch: str, repo_root: Path, delete_branch: bool = False) -
191
197
  )
192
198
 
193
199
 
200
+ @dataclass
201
+ class MergeResult:
202
+ merged: bool
203
+ message: str
204
+ conflict_files: list[str] | None = None
205
+
206
+
207
+ def merge_branch(branch: str, repo_root: Path) -> MergeResult:
208
+ """Merge a worktree branch into the branch checked out at repo_root.
209
+
210
+ Commits any uncommitted work in the branch's worktree first (agents
211
+ often leave files uncommitted), then merges. On conflict the merge is
212
+ aborted and the conflicting files are reported.
213
+ """
214
+ worktrees = list_worktrees(repo_root)
215
+ wt = next((w for w in worktrees if w.branch == branch), None)
216
+ if not wt:
217
+ return MergeResult(False, f"No worktree found for branch '{branch}'.")
218
+ if wt.is_main:
219
+ return MergeResult(False, "That is the main worktree — nothing to merge.")
220
+ if not wt.path.exists():
221
+ return MergeResult(False, f"Worktree directory for '{branch}' is missing.")
222
+
223
+ status = _git(["status", "--porcelain"], str(wt.path))
224
+ if status.returncode == 0 and status.stdout.strip():
225
+ _git(["add", "-A"], str(wt.path))
226
+ commit = _git(
227
+ ["commit", "-m", f"chore: voice-agent work on {branch}"], str(wt.path)
228
+ )
229
+ if commit.returncode != 0:
230
+ return MergeResult(
231
+ False, f"Could not commit pending work on '{branch}': "
232
+ f"{(commit.stderr or commit.stdout).strip()[:200]}"
233
+ )
234
+
235
+ target = get_current_branch(repo_root) or "main"
236
+ merge = _git(
237
+ ["merge", "--no-ff", branch, "-m", f"Merge branch '{branch}'"],
238
+ str(repo_root),
239
+ )
240
+ if merge.returncode != 0:
241
+ conflicts = _git(
242
+ ["diff", "--name-only", "--diff-filter=U"], str(repo_root)
243
+ )
244
+ conflict_files = [
245
+ f for f in conflicts.stdout.strip().splitlines() if f.strip()
246
+ ] if conflicts.returncode == 0 else []
247
+ _git(["merge", "--abort"], str(repo_root))
248
+ if conflict_files:
249
+ return MergeResult(
250
+ False,
251
+ f"Merge of '{branch}' into {target} has conflicts — aborted.",
252
+ conflict_files,
253
+ )
254
+ return MergeResult(
255
+ False,
256
+ f"Merge of '{branch}' failed: {(merge.stderr or merge.stdout).strip()[:200]}",
257
+ )
258
+
259
+ stat = _git(["diff", "--stat", "HEAD~1", "HEAD"], str(repo_root))
260
+ summary = stat.stdout.strip().splitlines()[-1].strip() if stat.returncode == 0 and stat.stdout.strip() else ""
261
+ msg = f"Merged '{branch}' into {target}."
262
+ if summary:
263
+ msg += f" {summary}."
264
+ return MergeResult(True, msg)
265
+
266
+
194
267
  def resolve_worktree_path(branch: str, repo_root: Path) -> Path:
195
268
  """Ensure a worktree exists for the branch.
196
269
 
@@ -0,0 +1,78 @@
1
+ """Voice-driven merge: merge a worktree branch into main, handle conflicts."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from voice_vibecoder import worktrees
9
+ from voice_vibecoder.worktrees import ensure_worktree, merge_branch
10
+
11
+
12
+ def run(cwd: Path, *args: str) -> subprocess.CompletedProcess:
13
+ return subprocess.run(
14
+ args, cwd=cwd, capture_output=True, text=True, check=True
15
+ )
16
+
17
+
18
+ @pytest.fixture
19
+ def repo(tmp_path):
20
+ repo = tmp_path / "repo"
21
+ repo.mkdir()
22
+ run(repo, "git", "init", "-q", "-b", "main")
23
+ run(repo, "git", "config", "user.email", "t@t")
24
+ run(repo, "git", "config", "user.name", "t")
25
+ (repo / "README.md").write_text("hello\n")
26
+ run(repo, "git", "add", "README.md")
27
+ run(repo, "git", "commit", "-q", "-m", "init")
28
+ worktrees.configure_paths(tmp_path)
29
+ worktrees.invalidate_worktree_cache()
30
+ yield repo
31
+ worktrees.invalidate_worktree_cache()
32
+
33
+
34
+ def test_merge_committed_work(repo):
35
+ wt = ensure_worktree("poem", repo)
36
+ (wt / "dikt.txt").write_text("roser er røde\n")
37
+ run(wt, "git", "add", "dikt.txt")
38
+ run(wt, "git", "commit", "-q", "-m", "add poem")
39
+ worktrees.invalidate_worktree_cache()
40
+
41
+ result = merge_branch("poem", repo)
42
+ assert result.merged, result.message
43
+ assert (repo / "dikt.txt").exists()
44
+
45
+
46
+ def test_merge_commits_pending_work_first(repo):
47
+ wt = ensure_worktree("wip", repo)
48
+ (wt / "uncommitted.txt").write_text("agents forget to commit\n")
49
+ worktrees.invalidate_worktree_cache()
50
+
51
+ result = merge_branch("wip", repo)
52
+ assert result.merged, result.message
53
+ assert (repo / "uncommitted.txt").exists()
54
+
55
+
56
+ def test_merge_conflict_aborts_and_reports(repo):
57
+ wt = ensure_worktree("clash", repo)
58
+ (wt / "README.md").write_text("branch version\n")
59
+ run(wt, "git", "add", "README.md")
60
+ run(wt, "git", "commit", "-q", "-m", "branch edit")
61
+ (repo / "README.md").write_text("main version\n")
62
+ run(repo, "git", "add", "README.md")
63
+ run(repo, "git", "commit", "-q", "-m", "main edit")
64
+ worktrees.invalidate_worktree_cache()
65
+
66
+ result = merge_branch("clash", repo)
67
+ assert not result.merged
68
+ assert result.conflict_files == ["README.md"]
69
+ # main is left clean — merge was aborted
70
+ status = run(repo, "git", "status", "--porcelain").stdout.strip()
71
+ assert status == ""
72
+ assert (repo / "README.md").read_text() == "main version\n"
73
+
74
+
75
+ def test_merge_unknown_branch(repo):
76
+ result = merge_branch("nope", repo)
77
+ assert not result.merged
78
+ assert "No worktree" in result.message