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.
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/CHANGELOG.md +11 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/PKG-INFO +1 -1
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/pyproject.toml +1 -1
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/config.py +12 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/session.py +1 -1
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/definitions.py +20 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/dispatch.py +3 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/handlers.py +29 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/__init__.py +1 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/openai.py +11 -2
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/worktrees.py +73 -0
- voice_vibecoder-2.26.0/tests/test_merge_branch.py +78 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/.gitignore +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/LICENSE +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/README.md +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/app.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/index.html +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio_macos.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/server.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/tests/test_audio_gating.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/tests/test_file_tree.py +0 -0
- {voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/tests/test_session_interrupts.py +0 -0
- {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.
|
|
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.
|
|
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"
|
|
@@ -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."
|
{voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
@@ -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,
|
{voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
@@ -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
|
-
|
|
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":
|
|
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
|
|
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.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/openclaw.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/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
|
{voice_vibecoder-2.25.0 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/gemini.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|