voice-vibecoder 2.7.0__tar.gz → 2.8.1__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.7.0 → voice_vibecoder-2.8.1}/CHANGELOG.md +19 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/PKG-INFO +1 -1
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/pyproject.toml +1 -1
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/__init__.py +1 -1
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/app_config.py +4 -0
- voice_vibecoder-2.8.1/src/voice_vibecoder/code_providers/openclaw.py +183 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/registry.py +12 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/config.py +1 -1
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/server.py +6 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/dispatch.py +3 -1
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/callbacks.py +20 -2
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/panels.py +46 -55
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/.github/workflows/ci.yml +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/.github/workflows/release.yml +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/.gitignore +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/LICENSE +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/README.md +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/app.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/index.html +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/audio.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/cli_serve.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/__init__.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/claude.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/cursor.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/instances.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/session.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/__init__.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/agent_task.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/definitions.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/file_tree.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/tools/handlers.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/__init__.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/diff_view.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/help_modal.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/screen.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/setup.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/state.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/styles.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/update_modal.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/ui/wizard.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/__init__.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/openai.py +0 -0
- {voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/worktrees.py +0 -0
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v2.8.1 (2026-02-21)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Simplify OpenClaw provider to use CLI only
|
|
10
|
+
([`dfe92c8`](https://github.com/snokam/voice-vibecoder/commit/dfe92c888e390bb5b50a5594452d276f2ff2a8d8))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## v2.8.0 (2026-02-20)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- Add OpenClaw code provider
|
|
18
|
+
([`910dc31`](https://github.com/snokam/voice-vibecoder/commit/910dc311df6a49f0e36e6bb53c98c2ea94753a25))
|
|
19
|
+
|
|
20
|
+
- **server**: Add text message support
|
|
21
|
+
([`86873ca`](https://github.com/snokam/voice-vibecoder/commit/86873cade8ec8feaa8479d120b86222fe106abdc))
|
|
22
|
+
|
|
23
|
+
|
|
5
24
|
## v2.7.0 (2026-02-20)
|
|
6
25
|
|
|
7
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voice-vibecoder
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.1
|
|
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.8.1"
|
|
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"
|
|
@@ -72,3 +72,7 @@ class VoiceConfig:
|
|
|
72
72
|
# Raises ValueError on validation failure.
|
|
73
73
|
# When None, auth is disabled (no auth frame expected from clients).
|
|
74
74
|
authenticate: Callable[[str], Awaitable[tuple[str, str]]] | None = None
|
|
75
|
+
|
|
76
|
+
# Default agent type for new instances ("claude", "cursor", "openclaw").
|
|
77
|
+
# Can be overridden per-instance via send_to_agent/send_to_session.
|
|
78
|
+
default_agent: str = "claude"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""OpenClaw Agent code provider.
|
|
2
|
+
|
|
3
|
+
Routes coding tasks to OpenClaw via CLI. Uses `openclaw agent --local`
|
|
4
|
+
for embedded agent execution with full tool access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Awaitable, Callable
|
|
16
|
+
|
|
17
|
+
from voice_vibecoder.code_providers import AgentOutput, AgentResult
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _extract_tool_info(line: str) -> tuple[str, str] | None:
|
|
23
|
+
"""Extract tool name and description from agent output lines."""
|
|
24
|
+
match = re.match(r"^\[(\w+)\]\s*(.*)$", line.strip())
|
|
25
|
+
if match:
|
|
26
|
+
return match.group(1), match.group(2)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OpenClawRunner:
|
|
31
|
+
"""AgentRunner implementation using OpenClaw CLI."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
session_id: str | None = None,
|
|
36
|
+
agent_id: str | None = None,
|
|
37
|
+
env: dict[str, str] | None = None,
|
|
38
|
+
**_kwargs,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._session_id = session_id or f"voice-{uuid.uuid4().hex[:8]}"
|
|
41
|
+
self._agent_id = agent_id
|
|
42
|
+
self._env = env
|
|
43
|
+
self._task: asyncio.Task | None = None
|
|
44
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
45
|
+
|
|
46
|
+
async def run(
|
|
47
|
+
self,
|
|
48
|
+
message: str,
|
|
49
|
+
cwd: str,
|
|
50
|
+
session_id: str | None = None,
|
|
51
|
+
on_output: Callable[[AgentOutput], None] | None = None,
|
|
52
|
+
can_use_tool: Callable[[str, dict, Any], Awaitable[Any]] | None = None,
|
|
53
|
+
) -> AgentResult:
|
|
54
|
+
"""Run a coding task via OpenClaw CLI."""
|
|
55
|
+
self._task = asyncio.current_task()
|
|
56
|
+
effective_session = session_id or self._session_id
|
|
57
|
+
|
|
58
|
+
full_message = f"[Working directory: {cwd}]\n\n{message}"
|
|
59
|
+
|
|
60
|
+
if on_output:
|
|
61
|
+
on_output(AgentOutput(category="text", content="🦞 Running via OpenClaw..."))
|
|
62
|
+
|
|
63
|
+
cmd = [
|
|
64
|
+
"openclaw", "agent",
|
|
65
|
+
"--session-id", effective_session,
|
|
66
|
+
"--message", full_message,
|
|
67
|
+
"--local",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
if self._agent_id:
|
|
71
|
+
cmd.extend(["--agent", self._agent_id])
|
|
72
|
+
|
|
73
|
+
env = os.environ.copy()
|
|
74
|
+
env["PATH"] = f"/home/olaf/.npm-global/bin:/home/olaf/.local/bin:{env.get('PATH', '')}"
|
|
75
|
+
if self._env:
|
|
76
|
+
env.update(self._env)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
80
|
+
*cmd,
|
|
81
|
+
stdout=asyncio.subprocess.PIPE,
|
|
82
|
+
stderr=asyncio.subprocess.PIPE,
|
|
83
|
+
env=env,
|
|
84
|
+
cwd=cwd,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
result_text = ""
|
|
88
|
+
|
|
89
|
+
async def read_stdout():
|
|
90
|
+
nonlocal result_text
|
|
91
|
+
assert self._process and self._process.stdout
|
|
92
|
+
|
|
93
|
+
buffer = ""
|
|
94
|
+
while True:
|
|
95
|
+
chunk = await self._process.stdout.read(1024)
|
|
96
|
+
if not chunk:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
text = chunk.decode("utf-8", errors="replace")
|
|
100
|
+
buffer += text
|
|
101
|
+
|
|
102
|
+
while "\n" in buffer:
|
|
103
|
+
line, buffer = buffer.split("\n", 1)
|
|
104
|
+
line = line.strip()
|
|
105
|
+
|
|
106
|
+
if not line:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
data = json.loads(line)
|
|
111
|
+
if isinstance(data, dict):
|
|
112
|
+
if "result" in data:
|
|
113
|
+
result_text = data["result"]
|
|
114
|
+
elif "text" in data:
|
|
115
|
+
result_text = data["text"]
|
|
116
|
+
continue
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
result_text += line + "\n"
|
|
121
|
+
|
|
122
|
+
if on_output:
|
|
123
|
+
tool_info = _extract_tool_info(line)
|
|
124
|
+
if tool_info:
|
|
125
|
+
tool_name, tool_desc = tool_info
|
|
126
|
+
category = (
|
|
127
|
+
"file_edit" if tool_name in ("Edit", "Write")
|
|
128
|
+
else "bash" if tool_name == "Bash"
|
|
129
|
+
else "tool_call"
|
|
130
|
+
)
|
|
131
|
+
on_output(AgentOutput(category=category, content=f"[{tool_name}] {tool_desc}"))
|
|
132
|
+
else:
|
|
133
|
+
on_output(AgentOutput(category="text", content=line))
|
|
134
|
+
|
|
135
|
+
if buffer.strip():
|
|
136
|
+
try:
|
|
137
|
+
data = json.loads(buffer)
|
|
138
|
+
if isinstance(data, dict) and "result" in data:
|
|
139
|
+
result_text = data["result"]
|
|
140
|
+
except json.JSONDecodeError:
|
|
141
|
+
result_text += buffer
|
|
142
|
+
|
|
143
|
+
async def read_stderr():
|
|
144
|
+
assert self._process and self._process.stderr
|
|
145
|
+
async for line in self._process.stderr:
|
|
146
|
+
text = line.decode("utf-8", errors="replace").strip()
|
|
147
|
+
if text:
|
|
148
|
+
logger.debug("stderr: %s", text)
|
|
149
|
+
|
|
150
|
+
await asyncio.gather(read_stdout(), read_stderr())
|
|
151
|
+
await self._process.wait()
|
|
152
|
+
|
|
153
|
+
if self._process.returncode != 0 and not result_text:
|
|
154
|
+
raise RuntimeError(f"OpenClaw CLI failed with exit code {self._process.returncode}")
|
|
155
|
+
|
|
156
|
+
return AgentResult(
|
|
157
|
+
text=result_text.strip(),
|
|
158
|
+
session_id=effective_session,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
except asyncio.CancelledError:
|
|
162
|
+
if self._process:
|
|
163
|
+
self._process.terminate()
|
|
164
|
+
try:
|
|
165
|
+
await asyncio.wait_for(self._process.wait(), timeout=5.0)
|
|
166
|
+
except asyncio.TimeoutError:
|
|
167
|
+
self._process.kill()
|
|
168
|
+
raise
|
|
169
|
+
finally:
|
|
170
|
+
self._process = None
|
|
171
|
+
|
|
172
|
+
def cancel(self) -> bool:
|
|
173
|
+
"""Cancel the running task."""
|
|
174
|
+
if self._process:
|
|
175
|
+
self._process.terminate()
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
task = self._task
|
|
179
|
+
if task and not task.done():
|
|
180
|
+
task.cancel()
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
return False
|
{voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/registry.py
RENAMED
|
@@ -34,6 +34,11 @@ def _make_cursor_runner(**_kw):
|
|
|
34
34
|
return CursorRunner()
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _make_openclaw_runner(session_id: str | None = None, agent_id: str | None = None, env: dict | None = None, **_kw):
|
|
38
|
+
from voice_vibecoder.code_providers.openclaw import OpenClawRunner
|
|
39
|
+
return OpenClawRunner(session_id=session_id, agent_id=agent_id, env=env)
|
|
40
|
+
|
|
41
|
+
|
|
37
42
|
AGENTS: dict[str, AgentInfo] = {
|
|
38
43
|
"claude": AgentInfo(
|
|
39
44
|
id="claude",
|
|
@@ -49,6 +54,13 @@ AGENTS: dict[str, AgentInfo] = {
|
|
|
49
54
|
color="green",
|
|
50
55
|
runner_factory=_make_cursor_runner,
|
|
51
56
|
),
|
|
57
|
+
"openclaw": AgentInfo(
|
|
58
|
+
id="openclaw",
|
|
59
|
+
label="OpenClaw",
|
|
60
|
+
cli_command="openclaw",
|
|
61
|
+
color="red",
|
|
62
|
+
runner_factory=_make_openclaw_runner,
|
|
63
|
+
),
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
|
|
@@ -15,7 +15,7 @@ CHUNK_SIZE = AUDIO_SAMPLE_RATE * CHUNK_DURATION_MS // 1000
|
|
|
15
15
|
Language = Literal["no", "en", "sv"]
|
|
16
16
|
PermissionMode = Literal["bypass", "acceptEdits", "default"]
|
|
17
17
|
InputMode = Literal["vad", "ptt"]
|
|
18
|
-
AgentType = Literal["claude", "cursor"]
|
|
18
|
+
AgentType = Literal["claude", "cursor", "openclaw"]
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _agent_names(enabled: list[str] | None = None) -> str:
|
|
@@ -136,6 +136,12 @@ class WebSocketBridge:
|
|
|
136
136
|
elif msg_type == "audio.commit":
|
|
137
137
|
if self._session:
|
|
138
138
|
await self._session.commit_audio()
|
|
139
|
+
elif msg_type == "text":
|
|
140
|
+
# Text-only mode: inject user message directly
|
|
141
|
+
text = msg.get("text", "") or msg.get("message", "")
|
|
142
|
+
if text and self._session:
|
|
143
|
+
self._send({"type": "transcript", "role": "user", "text": text})
|
|
144
|
+
await self._session.inject_user_message(text)
|
|
139
145
|
elif msg_type == "sync_state":
|
|
140
146
|
# Frontend requests full state sync from backend
|
|
141
147
|
self._send({"type": "status", "status": "syncing"})
|
|
@@ -97,7 +97,9 @@ def configure(
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def _default_agent_type() -> str:
|
|
100
|
-
"""Return the first enabled agent
|
|
100
|
+
"""Return the default agent type from config, or first enabled agent."""
|
|
101
|
+
if _voice_config and _voice_config.default_agent:
|
|
102
|
+
return _voice_config.default_agent
|
|
101
103
|
return _enabled_agents[0] if _enabled_agents else "claude"
|
|
102
104
|
|
|
103
105
|
|
|
@@ -47,7 +47,7 @@ class CallbackMixin:
|
|
|
47
47
|
return
|
|
48
48
|
|
|
49
49
|
inst = self._session.registry.get_by_id(instance_id)
|
|
50
|
-
if not inst
|
|
50
|
+
if not inst:
|
|
51
51
|
return
|
|
52
52
|
|
|
53
53
|
category = None
|
|
@@ -71,9 +71,27 @@ class CallbackMixin:
|
|
|
71
71
|
styled = _format_output_line(category, display_line, inst.agent_type, show_badge=show_badge)
|
|
72
72
|
|
|
73
73
|
self._state.add_instance_line(inst.display_key, styled)
|
|
74
|
-
self.app.call_from_thread(self.
|
|
74
|
+
self.app.call_from_thread(self._ensure_output, instance_id, inst, styled)
|
|
75
75
|
self._write_log_file(f"[{inst.display_key}] {display_line}")
|
|
76
76
|
|
|
77
|
+
def _ensure_output(self, instance_id: str, inst, styled: str) -> None:
|
|
78
|
+
"""Write output to panel, recovering the widget reference if needed."""
|
|
79
|
+
recovered = False
|
|
80
|
+
if not inst.output_widget:
|
|
81
|
+
# Widget reference lost (e.g. after restore) — recover from DOM
|
|
82
|
+
try:
|
|
83
|
+
inst.output_widget = self.query_one(f"#output-{instance_id}", RichLog)
|
|
84
|
+
recovered = True
|
|
85
|
+
except Exception:
|
|
86
|
+
# Panel doesn't exist yet — create it
|
|
87
|
+
self._create_instance_panel(instance_id, inst.display_key)
|
|
88
|
+
recovered = True
|
|
89
|
+
if not inst.output_widget:
|
|
90
|
+
return
|
|
91
|
+
if recovered:
|
|
92
|
+
self._write_to_output(inst.output_widget, "[dim]─── session resumed ───[/dim]")
|
|
93
|
+
self._write_to_output(inst.output_widget, styled)
|
|
94
|
+
|
|
77
95
|
def _write_to_output(self, widget: RichLog, text: str) -> None:
|
|
78
96
|
try:
|
|
79
97
|
widget.write(text)
|
|
@@ -13,7 +13,7 @@ from textual.containers import Grid, Horizontal, Vertical
|
|
|
13
13
|
from textual.widgets import Static, RichLog, Tree
|
|
14
14
|
|
|
15
15
|
from voice_vibecoder.code_providers.registry import get_badge
|
|
16
|
-
from voice_vibecoder.instances import AgentInstance, InstanceStatus
|
|
16
|
+
from voice_vibecoder.instances import AgentInstance, InstanceStatus, InstanceType
|
|
17
17
|
from voice_vibecoder.ui.styles import _STATUS_ICONS
|
|
18
18
|
|
|
19
19
|
|
|
@@ -166,14 +166,31 @@ class PanelMixin:
|
|
|
166
166
|
classes="agent-output",
|
|
167
167
|
id=f"output-{inst.instance_id}",
|
|
168
168
|
)
|
|
169
|
+
inst.header_widget = header
|
|
170
|
+
inst.output_widget = output
|
|
171
|
+
|
|
172
|
+
if inst.instance_type == InstanceType.CODE:
|
|
173
|
+
_empty_tree = {"name": "", "path": "", "type": "folder", "children": [], "expanded": True}
|
|
174
|
+
tree_widget = FileTreeWidget(
|
|
175
|
+
_empty_tree,
|
|
176
|
+
classes="file-tree-sidebar",
|
|
177
|
+
id=f"file-tree-{inst.instance_id}",
|
|
178
|
+
)
|
|
179
|
+
body = Horizontal(
|
|
180
|
+
tree_widget,
|
|
181
|
+
output,
|
|
182
|
+
classes="file-tree-container",
|
|
183
|
+
id=f"file-tree-wrap-{inst.instance_id}",
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
body = output
|
|
187
|
+
|
|
169
188
|
panel = Vertical(
|
|
170
189
|
header,
|
|
171
|
-
|
|
190
|
+
body,
|
|
172
191
|
classes="agent-panel",
|
|
173
192
|
id=f"panel-{inst.instance_id}",
|
|
174
193
|
)
|
|
175
|
-
inst.header_widget = header
|
|
176
|
-
inst.output_widget = output
|
|
177
194
|
panels.append(panel)
|
|
178
195
|
|
|
179
196
|
try:
|
|
@@ -200,17 +217,35 @@ class PanelMixin:
|
|
|
200
217
|
classes="agent-output",
|
|
201
218
|
id=f"output-{instance_id}",
|
|
202
219
|
)
|
|
220
|
+
|
|
221
|
+
if inst:
|
|
222
|
+
inst.header_widget = header
|
|
223
|
+
inst.output_widget = output
|
|
224
|
+
|
|
225
|
+
is_code = inst and inst.instance_type == InstanceType.CODE
|
|
226
|
+
if is_code:
|
|
227
|
+
_empty_tree = {"name": "", "path": "", "type": "folder", "children": [], "expanded": True}
|
|
228
|
+
tree_widget = FileTreeWidget(
|
|
229
|
+
_empty_tree,
|
|
230
|
+
classes="file-tree-sidebar",
|
|
231
|
+
id=f"file-tree-{instance_id}",
|
|
232
|
+
)
|
|
233
|
+
body = Horizontal(
|
|
234
|
+
tree_widget,
|
|
235
|
+
output,
|
|
236
|
+
classes="file-tree-container",
|
|
237
|
+
id=f"file-tree-wrap-{instance_id}",
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
body = output
|
|
241
|
+
|
|
203
242
|
panel = Vertical(
|
|
204
243
|
header,
|
|
205
|
-
|
|
244
|
+
body,
|
|
206
245
|
classes="agent-panel",
|
|
207
246
|
id=f"panel-{instance_id}",
|
|
208
247
|
)
|
|
209
248
|
|
|
210
|
-
if inst:
|
|
211
|
-
inst.header_widget = header
|
|
212
|
-
inst.output_widget = output
|
|
213
|
-
|
|
214
249
|
try:
|
|
215
250
|
grid = self.query_one("#agent-grid", Grid)
|
|
216
251
|
grid.styles.display = "block"
|
|
@@ -325,61 +360,17 @@ class PanelMixin:
|
|
|
325
360
|
grid.styles.grid_size_rows = 1
|
|
326
361
|
|
|
327
362
|
def _update_file_tree(self, instance_id: str, data: dict) -> None:
|
|
328
|
-
"""Update
|
|
329
|
-
if not self._session:
|
|
330
|
-
return
|
|
331
|
-
inst = self._session.registry.get_by_id(instance_id)
|
|
332
|
-
if not inst:
|
|
333
|
-
return
|
|
334
|
-
|
|
363
|
+
"""Update the file tree sidebar for a code instance panel."""
|
|
335
364
|
tree_data = data.get("tree")
|
|
336
365
|
if not tree_data or not tree_data.get("children"):
|
|
337
366
|
return
|
|
338
367
|
|
|
339
|
-
sidebar_id = f"file-tree-{instance_id}"
|
|
340
|
-
|
|
341
|
-
# Try to update existing tree widget
|
|
342
368
|
try:
|
|
343
|
-
tree_widget = self.query_one(f"#{
|
|
369
|
+
tree_widget = self.query_one(f"#file-tree-{instance_id}", FileTreeWidget)
|
|
344
370
|
tree_widget.rebuild(tree_data)
|
|
345
|
-
return
|
|
346
371
|
except Exception:
|
|
347
372
|
pass
|
|
348
373
|
|
|
349
|
-
# First time — wrap output in a Horizontal with tree sidebar
|
|
350
|
-
if not inst.output_widget:
|
|
351
|
-
return
|
|
352
|
-
|
|
353
|
-
try:
|
|
354
|
-
panel = self.query_one(f"#panel-{instance_id}")
|
|
355
|
-
except Exception:
|
|
356
|
-
return
|
|
357
|
-
|
|
358
|
-
tree_widget = FileTreeWidget(
|
|
359
|
-
tree_data,
|
|
360
|
-
classes="file-tree-sidebar",
|
|
361
|
-
id=sidebar_id,
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
# Remove output from panel, wrap in Horizontal, re-mount
|
|
365
|
-
output = inst.output_widget
|
|
366
|
-
try:
|
|
367
|
-
output.remove()
|
|
368
|
-
container = Horizontal(
|
|
369
|
-
tree_widget,
|
|
370
|
-
output,
|
|
371
|
-
classes="file-tree-container",
|
|
372
|
-
id=f"file-tree-wrap-{instance_id}",
|
|
373
|
-
)
|
|
374
|
-
panel.mount(container)
|
|
375
|
-
except Exception:
|
|
376
|
-
logger.exception("Failed to mount file tree for %s", instance_id)
|
|
377
|
-
# If something fails, try to restore output directly
|
|
378
|
-
try:
|
|
379
|
-
panel.mount(output)
|
|
380
|
-
except Exception:
|
|
381
|
-
pass
|
|
382
|
-
|
|
383
374
|
def _remove_instance_panel(self, instance_id: str) -> None:
|
|
384
375
|
"""Remove a panel from the grid."""
|
|
385
376
|
if self._session:
|
|
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.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/claude.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/code_providers/cursor.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.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/__init__.py
RENAMED
|
File without changes
|
{voice_vibecoder-2.7.0 → voice_vibecoder-2.8.1}/src/voice_vibecoder/voice_providers/openai.py
RENAMED
|
File without changes
|
|
File without changes
|