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.
Files changed (50) hide show
  1. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/CHANGELOG.md +22 -0
  2. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/PKG-INFO +1 -1
  3. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/pyproject.toml +1 -1
  4. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/__init__.py +1 -1
  5. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/config.py +18 -3
  6. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/session.py +1 -1
  7. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/definitions.py +20 -0
  8. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/dispatch.py +3 -0
  9. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/handlers.py +43 -0
  10. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/__init__.py +1 -0
  11. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/openai.py +11 -2
  12. {voice_vibecoder-2.24.1 → 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.26.0/tests/test_startup_context.py +28 -0
  15. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/.github/workflows/ci.yml +0 -0
  16. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/.github/workflows/release.yml +0 -0
  17. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/.gitignore +0 -0
  18. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/LICENSE +0 -0
  19. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/README.md +0 -0
  20. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/app.py +0 -0
  21. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/index.html +0 -0
  22. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/app_config.py +0 -0
  23. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio.py +0 -0
  24. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/audio_macos.py +0 -0
  25. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/cli_serve.py +0 -0
  26. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/__init__.py +0 -0
  27. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/claude.py +0 -0
  28. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/cursor.py +0 -0
  29. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/openclaw.py +0 -0
  30. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/code_providers/registry.py +0 -0
  31. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/instances.py +0 -0
  32. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/server.py +0 -0
  33. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/__init__.py +0 -0
  34. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/agent_task.py +0 -0
  35. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/tools/file_tree.py +0 -0
  36. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/__init__.py +0 -0
  37. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/callbacks.py +0 -0
  38. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/diff_view.py +0 -0
  39. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/help_modal.py +0 -0
  40. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/panels.py +0 -0
  41. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/screen.py +0 -0
  42. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/setup.py +0 -0
  43. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/state.py +0 -0
  44. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/styles.py +0 -0
  45. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/update_modal.py +0 -0
  46. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/ui/wizard.py +0 -0
  47. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/src/voice_vibecoder/voice_providers/gemini.py +0 -0
  48. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/tests/test_audio_gating.py +0 -0
  49. {voice_vibecoder-2.24.1 → voice_vibecoder-2.26.0}/tests/test_file_tree.py +0 -0
  50. {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.24.1
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.24.1"
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.24.1"
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
@@ -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 alltid norsk. \
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. Always speak English. \
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 alltid svenska. \
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."
@@ -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
@@ -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