voice-vibecoder 2.24.1__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.24.1 → voice_vibecoder-2.26.0}/CHANGELOG.md +22 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/PKG-INFO +1 -1
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/pyproject.toml +1 -1
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/config.py +18 -3
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/session.py +1 -1
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/definitions.py +20 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/dispatch.py +3 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/handlers.py +43 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/__init__.py +1 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/openai.py +11 -2
- {voice_vibecoder-2.24.1 → 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.26.0/tests/test_startup_context.py +28 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/.gitignore +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/LICENSE +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/README.md +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/app.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/index.html +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/app_config.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio_macos.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/server.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/panels.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/tests/test_audio_gating.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/tests/test_file_tree.py +0 -0
- {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/tests/test_session_interrupts.py +0 -0
|
@@ -2,6 +2,28 @@
|
|
|
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
|
+
|
|
16
|
+
## v2.25.0 (2026-07-13)
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
- **session**: Resume conversations with recent history in context
|
|
21
|
+
([`a4a60b2`](https://github.com/snokam/voice-vibecoder/commit/a4a60b214c68e028111d873fc6c1d33f47d93967))
|
|
22
|
+
|
|
23
|
+
- **voice**: Follow the user's language instead of locking to one
|
|
24
|
+
([`b828007`](https://github.com/snokam/voice-vibecoder/commit/b82800721340f96aaaa29ada12276c0466eeca2a))
|
|
25
|
+
|
|
26
|
+
|
|
5
27
|
## v2.24.1 (2026-07-13)
|
|
6
28
|
|
|
7
29
|
### Bug Fixes
|
|
@@ -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"
|
|
@@ -30,7 +30,8 @@ def _agent_names(enabled: list[str] | None = None) -> str:
|
|
|
30
30
|
|
|
31
31
|
_SYSTEM_INSTRUCTIONS: dict[Language, str] = {
|
|
32
32
|
"no": """\
|
|
33
|
-
Du er Olaf, en hjelpsom stemmeassistent for utviklere. Snakk
|
|
33
|
+
Du er Olaf, en hjelpsom stemmeassistent for utviklere. Snakk norsk som standard, \
|
|
34
|
+
men bytt språk umiddelbart hvis brukeren snakker eller ber om et annet språk. \
|
|
34
35
|
Brukeren skal oppleve en helt vanlig samtale — du bare hjelper. \
|
|
35
36
|
Dette er en talesamtale: svar kort, én til to setninger, med mindre brukeren ber om mer.
|
|
36
37
|
|
|
@@ -76,6 +77,10 @@ Avbryt/reset:
|
|
|
76
77
|
- "start på nytt", "reset" → reset_agent
|
|
77
78
|
- remove_branch_instance / remove_session for å fjerne instanser.
|
|
78
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
|
+
|
|
79
84
|
Visning:
|
|
80
85
|
- "vis diff" → show_diff
|
|
81
86
|
- "vis output", "gå tilbake" → show_output
|
|
@@ -84,7 +89,8 @@ Visning:
|
|
|
84
89
|
Spørsmål fra agenten:
|
|
85
90
|
- Still spørsmålet til brukeren. Videresend svaret ordrett via answer_agent_question.""",
|
|
86
91
|
"en": """\
|
|
87
|
-
You are Olaf, a helpful voice assistant for developers.
|
|
92
|
+
You are Olaf, a helpful voice assistant for developers. Speak English by default, \
|
|
93
|
+
but switch immediately if the user speaks or asks for another language. \
|
|
88
94
|
The user should experience a normal conversation — you just help. \
|
|
89
95
|
This is a spoken conversation: keep answers short, one or two sentences, unless the user asks for more.
|
|
90
96
|
|
|
@@ -130,6 +136,10 @@ Cancel/reset:
|
|
|
130
136
|
- "start fresh", "reset" → reset_agent
|
|
131
137
|
- remove_branch_instance / remove_session to remove instances.
|
|
132
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
|
+
|
|
133
143
|
Views:
|
|
134
144
|
- "show diff" → show_diff
|
|
135
145
|
- "show output", "go back" → show_output
|
|
@@ -138,7 +148,8 @@ Views:
|
|
|
138
148
|
Questions from the agent:
|
|
139
149
|
- Ask the user the question. Forward their answer verbatim via answer_agent_question.""",
|
|
140
150
|
"sv": """\
|
|
141
|
-
Du är Olaf, en hjälpsam röstassistent för utvecklare. Tala
|
|
151
|
+
Du är Olaf, en hjälpsam röstassistent för utvecklare. Tala svenska som standard, \
|
|
152
|
+
men byt språk direkt om användaren talar eller ber om ett annat språk. \
|
|
142
153
|
Användaren ska uppleva ett helt vanligt samtal — du bara hjälper. \
|
|
143
154
|
Detta är ett röstsamtal: svara kort, en till två meningar, om inte användaren ber om mer.
|
|
144
155
|
|
|
@@ -184,6 +195,10 @@ Avbryt/reset:
|
|
|
184
195
|
- "börja om", "reset" → reset_agent
|
|
185
196
|
- remove_branch_instance / remove_session för att ta bort instanser.
|
|
186
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
|
+
|
|
187
202
|
Visning:
|
|
188
203
|
- "visa diff" → show_diff
|
|
189
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":
|
|
@@ -70,6 +70,20 @@ def get_startup_context() -> str:
|
|
|
70
70
|
for wt in available:
|
|
71
71
|
parts.append(f" - {wt.branch}")
|
|
72
72
|
|
|
73
|
+
if _dispatch._registry:
|
|
74
|
+
recent = _dispatch._registry.get_transcripts()[-8:]
|
|
75
|
+
if recent:
|
|
76
|
+
parts.append(
|
|
77
|
+
"This is a RESUMED conversation. Recent history (oldest first) — "
|
|
78
|
+
"continue naturally from here, do not greet as if meeting for the "
|
|
79
|
+
"first time:"
|
|
80
|
+
)
|
|
81
|
+
for entry in recent:
|
|
82
|
+
role = entry.get("role", "user")
|
|
83
|
+
text = entry.get("text", "").strip().replace("\n", " ")[:200]
|
|
84
|
+
if text:
|
|
85
|
+
parts.append(f" {role}: {text}")
|
|
86
|
+
|
|
73
87
|
if not parts:
|
|
74
88
|
return ""
|
|
75
89
|
return "Current state:\n" + "\n".join(parts)
|
|
@@ -481,6 +495,35 @@ def _handle_remove_session(session_name: str) -> str:
|
|
|
481
495
|
return f"Removed session '{instance.display_name}'."
|
|
482
496
|
|
|
483
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
|
+
|
|
484
527
|
def _handle_delete_worktree(branch: str, delete_branch: bool = False) -> str:
|
|
485
528
|
if not branch.strip():
|
|
486
529
|
return "Error: branch name is required."
|
{voice_vibecoder-2.24.1 → 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.24.1 → 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
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Resumed sessions must carry recent conversation into the system prompt."""
|
|
2
|
+
|
|
3
|
+
from voice_vibecoder.instances import InstanceRegistry
|
|
4
|
+
from voice_vibecoder.tools import configure
|
|
5
|
+
from voice_vibecoder.tools.handlers import get_startup_context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def make_registry() -> InstanceRegistry:
|
|
9
|
+
reg = InstanceRegistry()
|
|
10
|
+
reg._transcript_buffer = [
|
|
11
|
+
{"role": "user", "text": "lag en fil i repoet"},
|
|
12
|
+
{"role": "assistant", "text": "Klart, jeg setter i gang!"},
|
|
13
|
+
]
|
|
14
|
+
return reg
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_startup_context_includes_recent_transcript():
|
|
18
|
+
configure(registry=make_registry(), repo_root=None)
|
|
19
|
+
ctx = get_startup_context()
|
|
20
|
+
assert "RESUMED" in ctx
|
|
21
|
+
assert "lag en fil i repoet" in ctx
|
|
22
|
+
assert "Klart, jeg setter i gang!" in ctx
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_startup_context_empty_without_history():
|
|
26
|
+
configure(registry=InstanceRegistry(), repo_root=None)
|
|
27
|
+
ctx = get_startup_context()
|
|
28
|
+
assert "RESUMED" not in ctx
|
|
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.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/cursor.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/openclaw.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.24.1 → 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.24.1 → 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
|