Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-02-17 16:23:28 +01:00 committed by GitHub
parent 51fecc67d9
commit ec7f3b25ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 8002 additions and 535 deletions

View file

@ -1,5 +1,8 @@
from __future__ import annotations
from datetime import datetime
import json
from pathlib import Path
from unittest.mock import patch
import pytest
@ -40,3 +43,55 @@ def acp_agent_loop(backend: FakeBackend) -> VibeAcpAgentLoop:
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgent).start()
return _create_acp_agent()
@pytest.fixture
def temp_session_dir(tmp_path: Path) -> Path:
session_dir = tmp_path / "sessions"
session_dir.mkdir()
return session_dir
@pytest.fixture
def create_test_session():
"""Create a test session with configurable messages and metadata.
Supports both messages parameter (for load_session tests) and
end_time parameter (for list_sessions tests).
"""
def _create_session(
session_dir: Path,
session_id: str,
cwd: str,
messages: list[dict] | None = None,
title: str | None = None,
end_time: str | None = None,
) -> Path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
session_folder = session_dir / f"session_{timestamp}_{session_id[:8]}"
session_folder.mkdir(exist_ok=True)
if messages is None:
messages = [{"role": "user", "content": "Hello"}]
messages_file = session_folder / "messages.jsonl"
with messages_file.open("w", encoding="utf-8") as f:
for msg in messages:
f.write(json.dumps(msg) + "\n")
metadata = {
"session_id": session_id,
"start_time": "2024-01-01T12:00:00Z",
"end_time": end_time or "2024-01-01T12:05:00Z",
"environment": {"working_directory": cwd},
"title": title,
}
metadata_file = session_folder / "meta.json"
with metadata_file.open("w", encoding="utf-8") as f:
json.dump(metadata, f)
return session_folder
return _create_session

View file

@ -451,6 +451,18 @@ class TestSessionUpdates:
),
),
)
commands_response_text = await read_response(process)
assert commands_response_text is not None
commands_response = UpdateJsonRpcNotification.model_validate(
json.loads(commands_response_text)
)
assert commands_response.params is not None
assert (
commands_response.params.update.session_update
== "available_commands_update"
)
user_response_text = await read_response(process)
assert user_response_text is not None
user_response = UpdateJsonRpcNotification.model_validate(

View file

@ -6,6 +6,8 @@ from acp.schema import (
ClientCapabilities,
Implementation,
PromptCapabilities,
SessionCapabilities,
SessionListCapabilities,
)
import pytest
@ -19,13 +21,14 @@ class TestACPInitialize:
assert response.protocol_version == PROTOCOL_VERSION
assert response.agent_capabilities == AgentCapabilities(
load_session=False,
load_session=True,
prompt_capabilities=PromptCapabilities(
audio=False, embedded_context=True, image=False
),
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.1.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.0"
)
assert response.auth_methods == []
@ -42,13 +45,14 @@ class TestACPInitialize:
assert response.protocol_version == PROTOCOL_VERSION
assert response.agent_capabilities == AgentCapabilities(
load_session=False,
load_session=True,
prompt_capabilities=PromptCapabilities(
audio=False, embedded_context=True, image=False
),
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.1.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.2.0"
)
assert response.auth_methods is not None

View file

@ -0,0 +1,242 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from tests.acp.conftest import _create_acp_agent
from vibe.core.config import MissingAPIKeyError, SessionLoggingConfig
class TestListSessions:
@pytest.mark.asyncio
async def test_list_sessions_empty(self, temp_session_dir: Path) -> None:
acp_agent = _create_acp_agent()
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_config = mock_load.return_value
mock_config.session_logging = session_config
response = await acp_agent.list_sessions()
assert response.sessions == []
@pytest.mark.asyncio
async def test_list_sessions_returns_all_sessions(
self, temp_session_dir: Path, create_test_session
) -> None:
acp_agent = _create_acp_agent()
create_test_session(
temp_session_dir,
"aaaaaaaa-1111",
"/home/user/project1",
title="First session",
end_time="2024-01-01T12:00:00Z",
)
create_test_session(
temp_session_dir,
"bbbbbbbb-2222",
"/home/user/project2",
title="Second session",
end_time="2024-01-01T13:00:00Z",
)
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_config = mock_load.return_value
mock_config.session_logging = session_config
response = await acp_agent.list_sessions()
assert len(response.sessions) == 2
session_ids = {s.session_id for s in response.sessions}
assert "aaaaaaaa-1111" in session_ids
assert "bbbbbbbb-2222" in session_ids
@pytest.mark.asyncio
async def test_list_sessions_filters_by_cwd(
self, temp_session_dir: Path, create_test_session
) -> None:
acp_agent = _create_acp_agent()
create_test_session(
temp_session_dir,
"aaaaaaaa-proj1",
"/home/user/project1",
title="Project 1 session",
)
create_test_session(
temp_session_dir,
"bbbbbbbb-proj2",
"/home/user/project2",
title="Project 2 session",
)
create_test_session(
temp_session_dir,
"cccccccc-proj1",
"/home/user/project1",
title="Another Project 1 session",
)
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_config = mock_load.return_value
mock_config.session_logging = session_config
response = await acp_agent.list_sessions(cwd="/home/user/project1")
assert len(response.sessions) == 2
for session in response.sessions:
assert session.cwd == "/home/user/project1"
@pytest.mark.asyncio
async def test_list_sessions_sorted_by_updated_at(
self, temp_session_dir: Path, create_test_session
) -> None:
acp_agent = _create_acp_agent()
create_test_session(
temp_session_dir,
"oldest-s",
"/home/user/project",
title="Oldest",
end_time="2024-01-01T10:00:00Z",
)
create_test_session(
temp_session_dir,
"newest-s",
"/home/user/project",
title="Newest",
end_time="2024-01-01T14:00:00Z",
)
create_test_session(
temp_session_dir,
"middle-s",
"/home/user/project",
title="Middle",
end_time="2024-01-01T12:00:00Z",
)
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_config = mock_load.return_value
mock_config.session_logging = session_config
response = await acp_agent.list_sessions()
assert len(response.sessions) == 3
assert response.sessions[0].title == "Newest"
assert response.sessions[1].title == "Middle"
assert response.sessions[2].title == "Oldest"
@pytest.mark.asyncio
async def test_list_sessions_includes_session_info_fields(
self, temp_session_dir: Path, create_test_session
) -> None:
acp_agent = _create_acp_agent()
create_test_session(
temp_session_dir,
"test-session-123",
"/home/user/project",
title="Test Session Title",
end_time="2024-01-15T10:30:00Z",
)
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_config = mock_load.return_value
mock_config.session_logging = session_config
response = await acp_agent.list_sessions()
assert len(response.sessions) == 1
session = response.sessions[0]
assert session.session_id == "test-session-123"
assert session.cwd == "/home/user/project"
assert session.title == "Test Session Title"
# updated_at is normalized to UTC
assert session.updated_at is not None
assert session.updated_at.endswith("+00:00")
@pytest.mark.asyncio
async def test_list_sessions_skips_invalid_sessions(
self, temp_session_dir: Path, create_test_session
) -> None:
acp_agent = _create_acp_agent()
create_test_session(
temp_session_dir, "valid-se", "/home/user/project", title="Valid Session"
)
invalid_session = temp_session_dir / "session_20240101_120000_invalid1"
invalid_session.mkdir()
(invalid_session / "meta.json").write_text('{"session_id": "invalid"}')
no_id_session = temp_session_dir / "session_20240101_120001_noid0000"
no_id_session.mkdir()
(no_id_session / "messages.jsonl").write_text(
'{"role": "user", "content": "Hello"}\n'
)
(no_id_session / "meta.json").write_text(
'{"environment": {"working_directory": "/test"}}'
)
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_config = mock_load.return_value
mock_config.session_logging = session_config
response = await acp_agent.list_sessions()
assert len(response.sessions) == 1
assert response.sessions[0].session_id == "valid-se"
@pytest.mark.asyncio
async def test_list_sessions_nonexistent_save_dir(self) -> None:
acp_agent = _create_acp_agent()
session_config = SessionLoggingConfig(
save_dir="/nonexistent/path", session_prefix="session", enabled=True
)
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_config = mock_load.return_value
mock_config.session_logging = session_config
response = await acp_agent.list_sessions()
assert response.sessions == []
@pytest.mark.asyncio
async def test_list_sessions_without_api_key(self) -> None:
acp_agent = _create_acp_agent()
with patch("vibe.acp.acp_agent_loop.VibeConfig.load") as mock_load:
mock_load.side_effect = MissingAPIKeyError("api_key", "mistral")
response = await acp_agent.list_sessions()
assert response.sessions == []

View file

@ -0,0 +1,301 @@
from __future__ import annotations
from pathlib import Path
from acp import RequestError
from acp.schema import (
AgentMessageChunk,
AgentThoughtChunk,
ToolCallProgress,
ToolCallStart,
UserMessageChunk,
)
import pytest
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import ModelConfig, SessionLoggingConfig
from vibe.core.types import Role
@pytest.fixture
def acp_agent_with_session_config(
backend: FakeBackend, temp_session_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> tuple[VibeAcpAgentLoop, FakeClient]:
session_config = SessionLoggingConfig(
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
)
config = build_test_vibe_config(
active_model="devstral-latest",
models=[
ModelConfig(
name="devstral-latest", provider="mistral", alias="devstral-latest"
)
],
session_logging=session_config,
)
class PatchedAgentLoop(AgentLoop):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **{**kwargs, "backend": backend})
self._base_config = config
self.agent_manager.invalidate_config()
monkeypatch.setattr("vibe.acp.acp_agent_loop.AgentLoop", PatchedAgentLoop)
monkeypatch.setattr(VibeAcpAgentLoop, "_load_config", lambda self: config)
vibe_acp_agent = VibeAcpAgentLoop()
client = FakeClient()
vibe_acp_agent.on_connect(client)
client.on_connect(vibe_acp_agent)
return vibe_acp_agent, client
class TestLoadSession:
@pytest.mark.asyncio
async def test_load_session_returns_response_with_models_and_modes(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, _client = acp_agent_with_session_config
session_id = "test-sess-12345678"
cwd = str(Path.cwd())
create_test_session(temp_session_dir, session_id, cwd)
response = await acp_agent.load_session(
cwd=cwd, mcp_servers=[], session_id=session_id
)
assert response is not None
assert response.models is not None
assert response.models.current_model_id == "devstral-latest"
assert response.modes is not None
assert response.modes.current_mode_id == BuiltinAgentName.DEFAULT
@pytest.mark.asyncio
async def test_load_session_registers_session_with_original_id(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, _client = acp_agent_with_session_config
session_id = "orig-id-12345678"
cwd = str(Path.cwd())
create_test_session(temp_session_dir, session_id, cwd)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
assert session_id in acp_agent.sessions
assert acp_agent.sessions[session_id].id == session_id
@pytest.mark.asyncio
async def test_load_session_injects_messages_into_agent_loop(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, _client = acp_agent_with_session_config
session_id = "msg-test-12345678"
cwd = str(Path.cwd())
messages = [
{"role": "system", "content": "You are helpful"},
{"role": "user", "content": "First question"},
{"role": "assistant", "content": "First answer"},
{"role": "user", "content": "Second question"},
{"role": "assistant", "content": "Second answer"},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
session = acp_agent.sessions[session_id]
non_system = [m for m in session.agent_loop.messages if m.role != Role.system]
assert len(non_system) == 4
@pytest.mark.asyncio
async def test_load_session_replays_user_messages(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "replay-usr-123456"
cwd = str(Path.cwd())
messages = [{"role": "user", "content": "Hello world"}]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
user_updates = [
u for u in client._session_updates if isinstance(u.update, UserMessageChunk)
]
assert len(user_updates) == 1
assert user_updates[0].update.content.text == "Hello world"
@pytest.mark.asyncio
async def test_load_session_replays_assistant_messages(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "replay-ast-123456"
cwd = str(Path.cwd())
messages = [
{"role": "user", "content": "Hi"},
{"role": "assistant", "content": "Hello! How can I help?"},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
agent_updates = [
u
for u in client._session_updates
if isinstance(u.update, AgentMessageChunk)
]
assert len(agent_updates) == 1
assert agent_updates[0].update.content.text == "Hello! How can I help?"
@pytest.mark.asyncio
async def test_load_session_replays_tool_calls(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "replay-tool-12345"
cwd = str(Path.cwd())
messages = [
{"role": "user", "content": "Read the file"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "read_file",
"arguments": '{"path": "/tmp/test.txt"}',
},
}
],
},
{"role": "tool", "tool_call_id": "call_123", "content": "file contents"},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
tool_call_starts = [
u for u in client._session_updates if isinstance(u.update, ToolCallStart)
]
assert len(tool_call_starts) == 1
assert tool_call_starts[0].update.title == "read_file"
assert tool_call_starts[0].update.tool_call_id == "call_123"
tool_results = [
u for u in client._session_updates if isinstance(u.update, ToolCallProgress)
]
assert len(tool_results) == 1
assert tool_results[0].update.tool_call_id == "call_123"
assert tool_results[0].update.status == "completed"
@pytest.mark.asyncio
async def test_load_session_replays_reasoning_content(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "replay-reason-123"
cwd = str(Path.cwd())
messages = [
{"role": "user", "content": "Think about this"},
{
"role": "assistant",
"content": "Here is my answer",
"reasoning_content": "Let me think step by step...",
},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
thought_updates = [
u
for u in client._session_updates
if isinstance(u.update, AgentThoughtChunk)
]
assert len(thought_updates) == 1
assert thought_updates[0].update.content.text == "Let me think step by step..."
@pytest.mark.asyncio
async def test_load_session_not_found_raises_error(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
) -> None:
acp_agent, _client = acp_agent_with_session_config
with pytest.raises(RequestError):
await acp_agent.load_session(
cwd=str(Path.cwd()), mcp_servers=[], session_id="nonexistent-session"
)
@pytest.mark.asyncio
async def test_load_session_replays_full_conversation(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent, client = acp_agent_with_session_config
session_id = "full-conv-1234567"
cwd = str(Path.cwd())
messages = [
{"role": "user", "content": "First message"},
{"role": "assistant", "content": "First response"},
{"role": "user", "content": "Second message"},
{"role": "assistant", "content": "Second response"},
]
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
user_updates = [
u for u in client._session_updates if isinstance(u.update, UserMessageChunk)
]
agent_updates = [
u
for u in client._session_updates
if isinstance(u.update, AgentMessageChunk)
]
assert len(user_updates) == 2
assert len(agent_updates) == 2
assert user_updates[0].update.content.text == "First message"
assert user_updates[1].update.content.text == "Second message"
assert agent_updates[0].update.content.text == "First response"
assert agent_updates[1].update.content.text == "Second response"

View file

@ -41,12 +41,18 @@ def acp_agent_loop(backend) -> VibeAcpAgentLoop:
class TestACPNewSession:
@pytest.mark.asyncio
async def test_new_session_response_structure(
self, acp_agent_loop: VibeAcpAgentLoop
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict]
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
new_session_events = [
e for e in telemetry_events if e.get("event_name") == "vibe/new_session"
]
assert len(new_session_events) == 1
assert new_session_events[0]["properties"]["entrypoint"] == "acp"
assert session_response.session_id is not None
acp_session = next(
(

View file

@ -0,0 +1,271 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from acp.schema import AgentMessageChunk, AvailableCommandsUpdate, TextContentBlock
import pytest
from tests.acp.conftest import _create_acp_agent
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.core.agent_loop import AgentLoop
@pytest.fixture
def acp_agent_loop(backend) -> VibeAcpAgentLoop:
config = build_test_vibe_config()
class PatchedAgentLoop(AgentLoop):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **{**kwargs, "backend": backend})
self._base_config = config
self.agent_manager.invalidate_config()
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgentLoop).start()
return _create_acp_agent()
def _get_fake_client(acp_agent_loop: VibeAcpAgentLoop) -> FakeClient:
assert isinstance(acp_agent_loop.client, FakeClient)
return acp_agent_loop.client
class TestAvailableCommandsUpdate:
@pytest.mark.asyncio
async def test_available_commands_sent_on_new_session(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
import asyncio
await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
await asyncio.sleep(0)
updates = _get_fake_client(acp_agent_loop)._session_updates
available_commands_updates = [
u for u in updates if isinstance(u.update, AvailableCommandsUpdate)
]
assert len(available_commands_updates) == 1
update = available_commands_updates[0].update
assert len(update.available_commands) == 1
assert update.available_commands[0].name == "proxy-setup"
assert "proxy" in update.available_commands[0].description.lower()
class TestProxySetupCommand:
@pytest.mark.asyncio
async def test_proxy_setup_shows_help_when_no_args(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_file = tmp_path / ".env"
class FakeGlobalEnvFile:
path = env_file
monkeypatch.setattr(
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
)
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
session_id = session_response.session_id
_get_fake_client(acp_agent_loop)._session_updates.clear()
response = await acp_agent_loop.prompt(
prompt=[TextContentBlock(type="text", text="/proxy-setup")],
session_id=session_id,
)
assert response.stop_reason == "end_turn"
updates = _get_fake_client(acp_agent_loop)._session_updates
message_updates = [
u for u in updates if isinstance(u.update, AgentMessageChunk)
]
assert len(message_updates) == 1
content = message_updates[0].update.content.text
assert "## Proxy Configuration" in content
assert "HTTP_PROXY" in content
@pytest.mark.asyncio
async def test_proxy_setup_sets_value(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_file = tmp_path / ".env"
class FakeGlobalEnvFile:
path = env_file
monkeypatch.setattr(
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
)
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
session_id = session_response.session_id
_get_fake_client(acp_agent_loop)._session_updates.clear()
response = await acp_agent_loop.prompt(
prompt=[
TextContentBlock(
type="text", text="/proxy-setup HTTP_PROXY http://localhost:8080"
)
],
session_id=session_id,
)
assert response.stop_reason == "end_turn"
updates = _get_fake_client(acp_agent_loop)._session_updates
message_updates = [
u for u in updates if isinstance(u.update, AgentMessageChunk)
]
assert len(message_updates) == 1
content = message_updates[0].update.content.text
assert "HTTP_PROXY" in content
assert "http://localhost:8080" in content
assert env_file.exists()
env_content = env_file.read_text()
assert "HTTP_PROXY" in env_content
assert "http://localhost:8080" in env_content
@pytest.mark.asyncio
async def test_proxy_setup_unsets_value(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_file = tmp_path / ".env"
env_file.write_text("HTTP_PROXY=http://old-proxy.com\n")
class FakeGlobalEnvFile:
path = env_file
monkeypatch.setattr(
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
)
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
session_id = session_response.session_id
_get_fake_client(acp_agent_loop)._session_updates.clear()
response = await acp_agent_loop.prompt(
prompt=[TextContentBlock(type="text", text="/proxy-setup HTTP_PROXY")],
session_id=session_id,
)
assert response.stop_reason == "end_turn"
updates = _get_fake_client(acp_agent_loop)._session_updates
message_updates = [
u for u in updates if isinstance(u.update, AgentMessageChunk)
]
assert len(message_updates) == 1
content = message_updates[0].update.content.text
assert "Removed" in content
assert "HTTP_PROXY" in content
env_content = env_file.read_text()
assert "HTTP_PROXY" not in env_content
@pytest.mark.asyncio
async def test_proxy_setup_invalid_key_returns_error(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_file = tmp_path / ".env"
class FakeGlobalEnvFile:
path = env_file
monkeypatch.setattr(
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
)
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
session_id = session_response.session_id
_get_fake_client(acp_agent_loop)._session_updates.clear()
response = await acp_agent_loop.prompt(
prompt=[
TextContentBlock(type="text", text="/proxy-setup INVALID_KEY value")
],
session_id=session_id,
)
assert response.stop_reason == "end_turn"
updates = _get_fake_client(acp_agent_loop)._session_updates
message_updates = [
u for u in updates if isinstance(u.update, AgentMessageChunk)
]
assert len(message_updates) == 1
content = message_updates[0].update.content.text
assert "Error" in content
assert "Unknown key" in content
@pytest.mark.asyncio
async def test_proxy_setup_case_insensitive(
self,
acp_agent_loop: VibeAcpAgentLoop,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
env_file = tmp_path / ".env"
class FakeGlobalEnvFile:
path = env_file
monkeypatch.setattr(
"vibe.core.proxy_setup.GLOBAL_ENV_FILE", FakeGlobalEnvFile()
)
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
session_id = session_response.session_id
_get_fake_client(acp_agent_loop)._session_updates.clear()
response = await acp_agent_loop.prompt(
prompt=[
TextContentBlock(
type="text", text="/PROXY-SETUP http_proxy http://localhost:8080"
)
],
session_id=session_id,
)
assert response.stop_reason == "end_turn"
assert env_file.exists()
env_content = env_file.read_text()
assert "HTTP_PROXY" in env_content

55
tests/acp/test_utils.py Normal file
View file

@ -0,0 +1,55 @@
from __future__ import annotations
from vibe.acp.utils import get_proxy_help_text
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS
def _write_env_file(content: str) -> None:
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
GLOBAL_ENV_FILE.path.write_text(content, encoding="utf-8")
class TestGetProxyHelpText:
def test_returns_string(self) -> None:
result = get_proxy_help_text()
assert isinstance(result, str)
def test_includes_proxy_configuration_header(self) -> None:
result = get_proxy_help_text()
assert "## Proxy Configuration" in result
def test_includes_usage_section(self) -> None:
result = get_proxy_help_text()
assert "### Usage:" in result
assert "/proxy-setup" in result
def test_includes_all_supported_variables(self) -> None:
result = get_proxy_help_text()
for key in SUPPORTED_PROXY_VARS:
assert f"`{key}`" in result
def test_shows_none_configured_when_no_settings(self) -> None:
result = get_proxy_help_text()
assert "(none configured)" in result
def test_shows_current_settings_when_configured(self) -> None:
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
result = get_proxy_help_text()
assert "HTTP_PROXY=http://proxy:8080" in result
assert "(none configured)" not in result
def test_shows_only_set_values(self) -> None:
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
result = get_proxy_help_text()
assert "HTTP_PROXY=http://proxy:8080" in result
assert "HTTPS_PROXY=" not in result

View file

@ -100,7 +100,7 @@ async def test_arrow_navigation_cycles_through_suggestions(vibe_app: VibeApp) ->
@pytest.mark.asyncio
async def test_pressing_enter_submits_selected_command_and_hides_popup(
vibe_app: VibeApp,
vibe_app: VibeApp, telemetry_events: list[dict]
) -> None:
async with vibe_app.run_test() as pilot:
chat_input = vibe_app.query_one(ChatInputContainer)
@ -115,6 +115,17 @@ async def test_pressing_enter_submits_selected_command_and_hides_popup(
message_content = message.query_one(Markdown)
assert "Show help message" in message_content.source
slash_used = [
e
for e in telemetry_events
if e.get("event_name") == "vibe/slash_command_used"
]
assert any(
e.get("properties", {}).get("command") == "help"
and e.get("properties", {}).get("command_type") == "builtin"
for e in slash_used
)
@pytest.fixture()
def file_tree(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:

View file

@ -0,0 +1,586 @@
from __future__ import annotations
import json
import pytest
from vibe.core.config import ProviderConfig
from vibe.core.llm.backend.anthropic import AnthropicAdapter, AnthropicMapper
from vibe.core.types import (
AvailableFunction,
AvailableTool,
FunctionCall,
LLMMessage,
Role,
ToolCall,
)
@pytest.fixture
def mapper():
return AnthropicMapper()
@pytest.fixture
def adapter():
return AnthropicAdapter()
@pytest.fixture
def provider():
return ProviderConfig(
name="anthropic",
api_base="https://api.anthropic.com",
api_key_env_var="ANTHROPIC_API_KEY",
api_style="anthropic",
)
class TestMapperPrepareMessages:
def test_system_extracted(self, mapper):
messages = [
LLMMessage(role=Role.system, content="You are helpful."),
LLMMessage(role=Role.user, content="Hi"),
]
system, converted = mapper.prepare_messages(messages)
assert system == "You are helpful."
assert len(converted) == 1
assert converted[0]["role"] == "user"
def test_user_message(self, mapper):
messages = [LLMMessage(role=Role.user, content="Hello")]
_, converted = mapper.prepare_messages(messages)
assert converted[0]["content"] == [{"type": "text", "text": "Hello"}]
def test_assistant_text(self, mapper):
messages = [LLMMessage(role=Role.assistant, content="Sure")]
_, converted = mapper.prepare_messages(messages)
assert converted[0]["role"] == "assistant"
content = converted[0]["content"]
assert any(b.get("type") == "text" and b.get("text") == "Sure" for b in content)
def test_assistant_with_reasoning_content_and_signature(self, mapper):
messages = [
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="hmm",
reasoning_signature="sig",
)
]
_, converted = mapper.prepare_messages(messages)
content = converted[0]["content"]
assert content[0] == {"type": "thinking", "thinking": "hmm", "signature": "sig"}
assert content[1]["type"] == "text"
def test_assistant_with_reasoning_content(self, mapper):
messages = [
LLMMessage(
role=Role.assistant, content="Answer", reasoning_content="thinking..."
)
]
_, converted = mapper.prepare_messages(messages)
content = converted[0]["content"]
assert content[0] == {"type": "thinking", "thinking": "thinking..."}
def test_assistant_with_tool_calls(self, mapper):
messages = [
LLMMessage(
role=Role.assistant,
content="Let me search.",
tool_calls=[
ToolCall(
id="tc_1",
index=0,
function=FunctionCall(name="search", arguments='{"q": "test"}'),
)
],
)
]
_, converted = mapper.prepare_messages(messages)
content = converted[0]["content"]
tool_block = [b for b in content if b["type"] == "tool_use"][0]
assert tool_block["name"] == "search"
assert tool_block["input"] == {"q": "test"}
def test_tool_result_appended_to_user(self, mapper):
messages = [
LLMMessage(role=Role.user, content="Do it"),
LLMMessage(role=Role.tool, content="result", tool_call_id="tc_1"),
]
_, converted = mapper.prepare_messages(messages)
# tool_result is merged into the preceding user message
assert len(converted) == 1
assert converted[0]["role"] == "user"
blocks = converted[0]["content"]
assert any(b.get("type") == "tool_result" for b in blocks)
def test_tool_result_new_user_when_no_prior(self, mapper):
messages = [LLMMessage(role=Role.tool, content="result", tool_call_id="tc_1")]
_, converted = mapper.prepare_messages(messages)
assert converted[0]["role"] == "user"
assert converted[0]["content"][0]["type"] == "tool_result"
class TestMapperPrepareTools:
def test_none_returns_none(self, mapper):
assert mapper.prepare_tools(None) is None
def test_empty_returns_none(self, mapper):
assert mapper.prepare_tools([]) is None
def test_converts_tools(self, mapper):
tools = [
AvailableTool(
function=AvailableFunction(
name="search",
description="Search things",
parameters={"type": "object"},
)
)
]
result = mapper.prepare_tools(tools)
assert len(result) == 1
assert result[0]["name"] == "search"
assert result[0]["input_schema"] == {"type": "object"}
class TestMapperToolChoice:
def test_none(self, mapper):
assert mapper.prepare_tool_choice(None) is None
def test_auto(self, mapper):
assert mapper.prepare_tool_choice("auto") == {"type": "auto"}
def test_none_str(self, mapper):
assert mapper.prepare_tool_choice("none") == {"type": "none"}
def test_any(self, mapper):
assert mapper.prepare_tool_choice("any") == {"type": "any"}
def test_required(self, mapper):
assert mapper.prepare_tool_choice("required") == {"type": "any"}
def test_specific_tool(self, mapper):
tool = AvailableTool(
function=AvailableFunction(name="search", description="", parameters={})
)
assert mapper.prepare_tool_choice(tool) == {"type": "tool", "name": "search"}
class TestMapperParseResponse:
def test_text(self, mapper):
data = {
"content": [{"type": "text", "text": "Hello"}],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = mapper.parse_response(data)
assert chunk.message.content == "Hello"
assert chunk.usage.prompt_tokens == 10
def test_thinking(self, mapper):
data = {
"content": [
{"type": "thinking", "thinking": "hmm", "signature": "sig"},
{"type": "text", "text": "Answer"},
],
"usage": {"input_tokens": 1, "output_tokens": 1},
}
chunk = mapper.parse_response(data)
assert chunk.message.content == "Answer"
assert chunk.message.reasoning_content == "hmm"
assert chunk.message.reasoning_signature == "sig"
def test_redacted_thinking(self, mapper):
data = {
"content": [
{"type": "redacted_thinking", "data": "xyz"},
{"type": "text", "text": "Answer"},
],
"usage": {"input_tokens": 1, "output_tokens": 1},
}
chunk = mapper.parse_response(data)
assert chunk.message.content == "Answer"
assert chunk.message.reasoning_content is None
def test_tool_use(self, mapper):
data = {
"content": [
{"type": "tool_use", "id": "t1", "name": "search", "input": {"q": "hi"}}
],
"usage": {"input_tokens": 1, "output_tokens": 1},
}
chunk = mapper.parse_response(data)
assert chunk.message.tool_calls[0].function.name == "search"
assert json.loads(chunk.message.tool_calls[0].function.arguments) == {"q": "hi"}
def test_cache_tokens(self, mapper):
data = {
"content": [{"type": "text", "text": "x"}],
"usage": {
"input_tokens": 10,
"cache_creation_input_tokens": 5,
"cache_read_input_tokens": 3,
"output_tokens": 7,
},
}
chunk = mapper.parse_response(data)
assert chunk.usage.prompt_tokens == 18
assert chunk.usage.completion_tokens == 7
class TestMapperStreamingEvents:
def test_text_delta(self, mapper):
chunk, idx = mapper.parse_streaming_event(
"content_block_delta",
{"delta": {"type": "text_delta", "text": "hi"}, "index": 0},
0,
)
assert chunk.message.content == "hi"
def test_thinking_delta(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"content_block_delta",
{"delta": {"type": "thinking_delta", "thinking": "hmm"}, "index": 0},
0,
)
assert chunk.message.reasoning_content == "hmm"
def test_tool_use_start(self, mapper):
chunk, idx = mapper.parse_streaming_event(
"content_block_start",
{
"content_block": {"type": "tool_use", "id": "t1", "name": "search"},
"index": 2,
},
0,
)
assert chunk.message.tool_calls[0].id == "t1"
assert idx == 2
def test_input_json_delta(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"content_block_delta",
{
"delta": {"type": "input_json_delta", "partial_json": '{"q":'},
"index": 1,
},
0,
)
assert chunk.message.tool_calls[0].function.arguments == '{"q":'
def test_message_start_usage(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"message_start",
{"message": {"usage": {"input_tokens": 50, "cache_read_input_tokens": 10}}},
0,
)
assert chunk.usage.prompt_tokens == 60
def test_message_delta_usage(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"message_delta", {"usage": {"output_tokens": 42}}, 0
)
assert chunk.usage.completion_tokens == 42
def test_unknown_event(self, mapper):
chunk, idx = mapper.parse_streaming_event("ping", {}, 5)
assert chunk is None
assert idx == 5
def test_signature_delta(self, mapper):
chunk, _ = mapper.parse_streaming_event(
"content_block_delta",
{"delta": {"type": "signature_delta", "signature": "sig"}, "index": 0},
0,
)
assert chunk is not None
assert chunk.message.reasoning_signature == "sig"
class TestAdapterPrepareRequest:
def test_basic(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["model"] == "claude-sonnet-4-20250514"
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 0.5
assert req.endpoint == "/v1/messages"
assert req.headers["anthropic-version"] == "2023-06-01"
def test_beta_features(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
assert "prompt-caching-2024-07-31" in req.headers["anthropic-beta"]
assert "interleaved-thinking-2025-05-14" in req.headers["anthropic-beta"]
assert "fine-grained-tool-streaming-2025-05-14" in req.headers["anthropic-beta"]
def test_api_key_header(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
api_key="sk-test-key",
)
assert req.headers["x-api-key"] == "sk-test-key"
def test_streaming(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=True,
provider=provider,
)
assert json.loads(req.body)["stream"] is True
def test_default_max_tokens(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
assert json.loads(req.body)["max_tokens"] == AnthropicAdapter.DEFAULT_MAX_TOKENS
def test_with_thinking(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking="medium",
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10000}
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 1
def test_system_cached(self, adapter, provider):
messages = [
LLMMessage(role=Role.system, content="Be helpful."),
LLMMessage(role=Role.user, content="Hello"),
]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["system"][0]["cache_control"] == {"type": "ephemeral"}
def test_with_tools(self, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
tools = [
AvailableTool(
function=AvailableFunction(
name="test_tool",
description="A test tool",
parameters={"type": "object", "properties": {}},
)
)
]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=tools,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert len(payload["tools"]) == 1
assert payload["tools"][0]["name"] == "test_tool"
@pytest.mark.parametrize(
"level,expected_budget", [("low", 1024), ("medium", 10_000), ("high", 32_000)]
)
def test_thinking_levels_budget_model(
self, adapter, provider, level, expected_budget
):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking=level,
)
payload = json.loads(req.body)
assert payload["thinking"] == {
"type": "enabled",
"budget_tokens": expected_budget,
}
assert payload["temperature"] == 1
assert payload["max_tokens"] == expected_budget + 8192
@pytest.mark.parametrize("level", ["low", "medium", "high"])
def test_thinking_levels_adaptive_model(self, adapter, provider, level):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-opus-4-6-20260101",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking=level,
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "adaptive"}
assert payload["output_config"] == {"effort": level}
assert payload["temperature"] == 1
assert payload["max_tokens"] == 32_768
def test_history_forced_thinking_budget_model(self, adapter, provider):
messages = [
LLMMessage(role=Role.user, content="Hello"),
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="thinking...",
reasoning_signature="sig",
),
LLMMessage(role=Role.user, content="Follow up"),
]
req = adapter.prepare_request(
model_name="claude-sonnet-4-20250514",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10_000}
assert payload["temperature"] == 1
assert payload["max_tokens"] == 18_192
def test_history_forced_thinking_adaptive_model(self, adapter, provider):
messages = [
LLMMessage(role=Role.user, content="Hello"),
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="thinking...",
reasoning_signature="sig",
),
LLMMessage(role=Role.user, content="Follow up"),
]
req = adapter.prepare_request(
model_name="claude-opus-4-6-20260101",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "adaptive"}
assert payload["output_config"] == {"effort": "medium"}
assert payload["max_tokens"] == 32_768
class TestAdapterParseResponse:
def test_non_streaming(self, adapter, provider):
data = {
"content": [{"type": "text", "text": "Hello!"}],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hello!"
assert chunk.usage.prompt_tokens == 10
def test_streaming_text_delta(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hi"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hi"
def test_streaming_message_start(self, adapter, provider):
data = {"type": "message_start", "message": {"usage": {"input_tokens": 100}}}
chunk = adapter.parse_response(data, provider)
assert chunk.usage.prompt_tokens == 100
def test_streaming_unknown_returns_empty(self, adapter, provider):
data = {"type": "ping"}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
assert chunk.message.content is None
def test_cache_control_last_user_message(self, adapter):
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
def test_cache_control_skips_non_user(self, adapter):
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}
]
adapter._add_cache_control_to_last_user_message(messages)
assert "cache_control" not in messages[0]["content"][0]
def test_cache_control_empty(self, adapter):
messages: list[dict] = []
adapter._add_cache_control_to_last_user_message(messages)
assert messages == []

View file

@ -0,0 +1,591 @@
from __future__ import annotations
import json
from unittest.mock import patch
import pytest
from vibe.core.config import ProviderConfig
from vibe.core.llm.backend.vertex import (
VertexAnthropicAdapter,
build_vertex_base_url,
build_vertex_endpoint,
)
from vibe.core.types import AvailableFunction, AvailableTool, LLMMessage, Role
@pytest.fixture
def adapter():
return VertexAnthropicAdapter()
@pytest.fixture
def provider():
return ProviderConfig(
name="vertex",
api_base="",
project_id="test-project",
region="us-central1",
api_style="vertex-anthropic",
)
class TestBuildVertexEndpoint:
def test_non_streaming(self):
endpoint = build_vertex_endpoint(
"us-central1", "my-project", "claude-3-5-sonnet"
)
assert endpoint == (
"/v1/projects/my-project/locations/us-central1/"
"publishers/anthropic/models/claude-3-5-sonnet:rawPredict"
)
def test_streaming(self):
endpoint = build_vertex_endpoint(
"us-central1", "my-project", "claude-3-5-sonnet", streaming=True
)
assert endpoint == (
"/v1/projects/my-project/locations/us-central1/"
"publishers/anthropic/models/claude-3-5-sonnet:streamRawPredict"
)
def test_base_url(self):
base = build_vertex_base_url("us-central1")
assert base == "https://us-central1-aiplatform.googleapis.com"
def test_global_endpoint(self):
endpoint = build_vertex_endpoint("global", "my-project", "claude-3-5-sonnet")
assert endpoint == (
"/v1/projects/my-project/locations/global/"
"publishers/anthropic/models/claude-3-5-sonnet:rawPredict"
)
def test_global_base_url(self):
base = build_vertex_base_url("global")
assert base == "https://aiplatform.googleapis.com"
class TestPrepareRequest:
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_basic_request(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["anthropic_version"] == "vertex-2023-10-16"
assert "model" not in payload
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 0.5
assert req.headers["Authorization"] == "Bearer fake-token"
assert req.headers["anthropic-beta"] == adapter.BETA_FEATURES
assert "rawPredict" in req.endpoint
assert "streamRawPredict" not in req.endpoint
assert req.base_url == "https://us-central1-aiplatform.googleapis.com"
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_streaming_request(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=True,
provider=provider,
)
payload = json.loads(req.body)
assert payload.get("stream") is True
assert "streamRawPredict" in req.endpoint
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_no_beta_features_for_vertex(self, mock_token, adapter, provider):
"""Vertex AI doesn't support the same beta features as direct Anthropic API."""
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
# Vertex AI doesn't support prompt-caching or other beta features
assert req.headers.get("anthropic-beta", "") == ""
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_with_extended_thinking(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
thinking="medium",
)
payload = json.loads(req.body)
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 10000}
assert payload["max_tokens"] == 1024
assert payload["temperature"] == 1
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_with_tools(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
tools = [
AvailableTool(
function=AvailableFunction(
name="test_tool",
description="A test tool",
parameters={"type": "object", "properties": {}},
)
)
]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=tools,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert len(payload["tools"]) == 1
assert payload["tools"][0]["name"] == "test_tool"
def test_missing_project_id(self, adapter):
provider = ProviderConfig(
name="vertex",
api_base="",
region="us-central1",
api_style="vertex-anthropic",
)
with pytest.raises(ValueError, match="project_id"):
adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=[LLMMessage(role=Role.user, content="Hello")],
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
def test_missing_region(self, adapter):
provider = ProviderConfig(
name="vertex",
api_base="",
project_id="test-project",
api_style="vertex-anthropic",
)
with pytest.raises(ValueError, match="region"):
adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=[LLMMessage(role=Role.user, content="Hello")],
temperature=0.5,
tools=None,
max_tokens=1024,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
@patch(
"vibe.core.llm.backend.vertex.get_vertex_access_token",
return_value="fake-token",
)
def test_default_max_tokens(self, mock_token, adapter, provider):
messages = [LLMMessage(role=Role.user, content="Hello")]
req = adapter.prepare_request(
model_name="claude-3-5-sonnet",
messages=messages,
temperature=0.5,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
payload = json.loads(req.body)
assert payload["max_tokens"] == adapter.DEFAULT_MAX_TOKENS
class TestParseFullResponse:
def test_simple_text_response(self, adapter, provider):
data = {
"content": [{"type": "text", "text": "Hello there!"}],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hello there!"
assert chunk.usage.prompt_tokens == 10
assert chunk.usage.completion_tokens == 5
def test_response_with_tool_calls(self, adapter, provider):
data = {
"content": [
{"type": "text", "text": "Let me help."},
{
"type": "tool_use",
"id": "tool_123",
"name": "search",
"input": {"query": "test"},
},
],
"usage": {"input_tokens": 20, "output_tokens": 15},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Let me help."
assert len(chunk.message.tool_calls) == 1
assert chunk.message.tool_calls[0].id == "tool_123"
assert chunk.message.tool_calls[0].function.name == "search"
assert json.loads(chunk.message.tool_calls[0].function.arguments) == {
"query": "test"
}
def test_response_with_thinking(self, adapter, provider):
data = {
"content": [
{
"type": "thinking",
"thinking": "Let me think...",
"signature": "sig123",
},
{"type": "text", "text": "Here's my answer."},
],
"usage": {"input_tokens": 30, "output_tokens": 20},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Here's my answer."
assert chunk.message.reasoning_content == "Let me think..."
assert chunk.message.reasoning_signature == "sig123"
def test_response_with_cache_tokens(self, adapter, provider):
data = {
"content": [{"type": "text", "text": "Hello"}],
"usage": {
"input_tokens": 10,
"cache_creation_input_tokens": 5,
"cache_read_input_tokens": 3,
"output_tokens": 7,
},
}
chunk = adapter.parse_response(data, provider)
assert chunk.usage.prompt_tokens == 18
assert chunk.usage.completion_tokens == 7
def test_response_with_redacted_thinking(self, adapter, provider):
data = {
"content": [
{"type": "redacted_thinking", "data": "redacted_data_here"},
{"type": "text", "text": "Answer."},
],
"usage": {"input_tokens": 10, "output_tokens": 5},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Answer."
assert chunk.message.reasoning_content is None
def test_response_empty_usage(self, adapter, provider):
data = {"content": [{"type": "text", "text": "Hello"}], "usage": {}}
chunk = adapter.parse_response(data, provider)
assert chunk.usage.prompt_tokens == 0
assert chunk.usage.completion_tokens == 0
class TestStreamingEvents:
def test_message_start(self, adapter, provider):
data = {
"type": "message_start",
"message": {
"usage": {
"input_tokens": 100,
"cache_creation_input_tokens": 20,
"cache_read_input_tokens": 10,
}
},
}
chunk = adapter.parse_response(data, provider)
assert chunk.usage is not None
assert chunk.usage.prompt_tokens == 130
assert chunk.usage.completion_tokens == 0
def test_message_start_without_usage(self, adapter, provider):
data = {"type": "message_start", "message": {}}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
def test_content_block_start_tool_use(self, adapter, provider):
data = {
"type": "content_block_start",
"index": 0,
"content_block": {"type": "tool_use", "id": "tool_abc", "name": "search"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.tool_calls is not None
assert len(chunk.message.tool_calls) == 1
assert chunk.message.tool_calls[0].id == "tool_abc"
assert chunk.message.tool_calls[0].function.name == "search"
assert chunk.message.tool_calls[0].index == 0
def test_content_block_start_thinking(self, adapter, provider):
data = {
"type": "content_block_start",
"index": 0,
"content_block": {"type": "thinking", "thinking": ""},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_content is not None
def test_content_block_start_redacted_thinking(self, adapter, provider):
data = {
"type": "content_block_start",
"index": 0,
"content_block": {"type": "redacted_thinking", "data": "abc"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content is None
assert chunk.message.reasoning_content is None
def test_content_block_delta_text(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "text_delta", "text": "Hello"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hello"
def test_content_block_delta_thinking(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "thinking_delta", "thinking": "I think..."},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_content == "I think..."
def test_content_block_delta_input_json(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 1,
"delta": {"type": "input_json_delta", "partial_json": '{"key":'},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.tool_calls is not None
assert chunk.message.tool_calls[0].function.arguments == '{"key":'
def test_content_block_stop(self, adapter, provider):
data = {"type": "content_block_stop", "index": 0}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content is None
assert chunk.message.reasoning_content is None
def test_message_delta_with_usage(self, adapter, provider):
data = {"type": "message_delta", "usage": {"output_tokens": 42}}
chunk = adapter.parse_response(data, provider)
assert chunk.usage is not None
assert chunk.usage.completion_tokens == 42
assert chunk.usage.prompt_tokens == 0
def test_message_delta_without_usage(self, adapter, provider):
data = {"type": "message_delta", "usage": {}}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
def test_unknown_event_returns_empty_chunk(self, adapter, provider):
data = {"type": "ping"}
chunk = adapter.parse_response(data, provider)
assert chunk.message.role == Role.assistant
assert chunk.message.content is None
def test_signature_delta(self, adapter, provider):
data = {
"type": "content_block_delta",
"index": 0,
"delta": {"type": "signature_delta", "signature": "sig_abc"},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_signature == "sig_abc"
def test_message_start_resets_state(self, adapter, provider):
adapter._current_index = 5
data = {"type": "message_start", "message": {"usage": {"input_tokens": 10}}}
adapter.parse_response(data, provider)
assert adapter._current_index == 0
def test_full_streaming_sequence(self, adapter, provider):
chunks = []
# message_start
chunks.append(
adapter.parse_response(
{"type": "message_start", "message": {"usage": {"input_tokens": 50}}},
provider,
)
)
assert chunks[-1].usage.prompt_tokens == 50
# thinking block
adapter.parse_response(
{
"type": "content_block_start",
"index": 0,
"content_block": {"type": "thinking", "thinking": ""},
},
provider,
)
chunks.append(
adapter.parse_response(
{
"type": "content_block_delta",
"index": 0,
"delta": {"type": "thinking_delta", "thinking": "Analyzing..."},
},
provider,
)
)
assert chunks[-1].message.reasoning_content == "Analyzing..."
adapter.parse_response({"type": "content_block_stop", "index": 0}, provider)
# text block
chunks.append(
adapter.parse_response(
{
"type": "content_block_delta",
"index": 1,
"delta": {"type": "text_delta", "text": "Here's the result."},
},
provider,
)
)
assert chunks[-1].message.content == "Here's the result."
# tool use
chunks.append(
adapter.parse_response(
{
"type": "content_block_start",
"index": 2,
"content_block": {
"type": "tool_use",
"id": "tool_1",
"name": "search",
},
},
provider,
)
)
assert chunks[-1].message.tool_calls[0].function.name == "search"
# message_delta with final usage
chunks.append(
adapter.parse_response(
{"type": "message_delta", "usage": {"output_tokens": 100}}, provider
)
)
assert chunks[-1].usage.completion_tokens == 100
class TestHelperMethods:
def test_has_thinking_content_true(self, adapter):
messages = [
{"role": "user", "content": "Hello"},
{
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "Let me think..."},
{"type": "text", "text": "Answer"},
],
},
]
assert adapter._has_thinking_content(messages) is True
def test_has_thinking_content_false(self, adapter):
messages = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Just text"},
]
assert adapter._has_thinking_content(messages) is False
def test_has_thinking_content_empty(self, adapter):
assert adapter._has_thinking_content([]) is False
def test_has_thinking_content_non_list_content(self, adapter):
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "no thinking"}]}
]
assert adapter._has_thinking_content(messages) is False
def test_add_cache_control_to_last_user_message(self, adapter):
messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
def test_add_cache_control_skips_non_user(self, adapter):
messages = [
{"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}
]
adapter._add_cache_control_to_last_user_message(messages)
assert "cache_control" not in messages[0]["content"][0]
def test_add_cache_control_skips_string_content(self, adapter):
messages = [{"role": "user", "content": "Hello"}]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"] == "Hello"
def test_add_cache_control_tool_result(self, adapter):
messages = [
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "123", "content": "result"}
],
}
]
adapter._add_cache_control_to_last_user_message(messages)
assert messages[0]["content"][0]["cache_control"] == {"type": "ephemeral"}
def test_add_cache_control_empty_messages(self, adapter):
messages: list[dict] = []
adapter._add_cache_control_to_last_user_message(messages)
assert messages == []

View file

@ -74,7 +74,8 @@ def test_copy_selection_to_clipboard_no_notification(
del widgets[0].text_selection
mock_app.query.return_value = widgets
copy_selection_to_clipboard(mock_app)
result = copy_selection_to_clipboard(mock_app)
assert result is None
mock_app.notify.assert_not_called()
@ -87,8 +88,9 @@ def test_copy_selection_to_clipboard_success(
)
mock_app.query.return_value = [widget]
copy_selection_to_clipboard(mock_app)
result = copy_selection_to_clipboard(mock_app)
assert result == "selected text"
mock_copy_osc52.assert_called_once_with("selected text")
mock_app.notify.assert_called_once_with(
'"selected text" copied to clipboard',
@ -109,8 +111,9 @@ def test_copy_selection_to_clipboard_failure(
mock_copy_osc52.side_effect = Exception("OSC52 failed")
copy_selection_to_clipboard(mock_app)
result = copy_selection_to_clipboard(mock_app)
assert result is None
mock_copy_osc52.assert_called_once_with("selected text")
mock_app.notify.assert_called_once_with(
"Failed to copy - clipboard not available", severity="warning", timeout=3
@ -129,8 +132,9 @@ def test_copy_selection_to_clipboard_multiple_widgets(mock_app: MagicMock) -> No
mock_app.query.return_value = [widget1, widget2, widget3]
with patch("vibe.cli.clipboard._copy_osc52") as mock_copy_osc52:
copy_selection_to_clipboard(mock_app)
result = copy_selection_to_clipboard(mock_app)
assert result == "first selection\nsecond selection"
mock_copy_osc52.assert_called_once_with("first selection\nsecond selection")
mock_app.notify.assert_called_once_with(
'"first selection\u23cesecond selection" copied to clipboard',
@ -148,8 +152,9 @@ def test_copy_selection_to_clipboard_preview_shortening(mock_app: MagicMock) ->
mock_app.query.return_value = [widget]
with patch("vibe.cli.clipboard._copy_osc52") as mock_copy_osc52:
copy_selection_to_clipboard(mock_app)
result = copy_selection_to_clipboard(mock_app)
assert result == long_text
mock_copy_osc52.assert_called_once_with(long_text)
notification_call = mock_app.notify.call_args
assert notification_call is not None

View file

@ -0,0 +1,54 @@
from __future__ import annotations
from vibe.cli.commands import Command, CommandRegistry
class TestCommandRegistry:
def test_get_command_name_returns_canonical_name_for_alias(self) -> None:
registry = CommandRegistry()
assert registry.get_command_name("/help") == "help"
assert registry.get_command_name("/config") == "config"
assert registry.get_command_name("/model") == "config"
assert registry.get_command_name("/clear") == "clear"
assert registry.get_command_name("/exit") == "exit"
def test_get_command_name_normalizes_input(self) -> None:
registry = CommandRegistry()
assert registry.get_command_name(" /help ") == "help"
assert registry.get_command_name("/HELP") == "help"
def test_get_command_name_returns_none_for_unknown(self) -> None:
registry = CommandRegistry()
assert registry.get_command_name("/unknown") is None
assert registry.get_command_name("hello") is None
assert registry.get_command_name("") is None
def test_find_command_returns_command_when_alias_matches(self) -> None:
registry = CommandRegistry()
cmd = registry.find_command("/help")
assert cmd is not None
assert cmd.handler == "_show_help"
assert isinstance(cmd, Command)
def test_find_command_returns_none_when_no_match(self) -> None:
registry = CommandRegistry()
assert registry.find_command("/nonexistent") is None
def test_find_command_uses_get_command_name(self) -> None:
"""find_command and get_command_name stay in sync for same input."""
registry = CommandRegistry()
for alias in ["/help", "/config", "/clear", "/exit"]:
cmd_name = registry.get_command_name(alias)
cmd = registry.find_command(alias)
if cmd_name is None:
assert cmd is None
else:
assert cmd is not None
assert cmd_name in registry.commands
assert registry.commands[cmd_name] is cmd
def test_excluded_commands_not_in_registry(self) -> None:
registry = CommandRegistry(excluded_commands=["exit"])
assert registry.get_command_name("/exit") is None
assert registry.find_command("/exit") is None
assert registry.get_command_name("/help") == "help"

View file

@ -94,6 +94,22 @@ def _mock_update_commands(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("vibe.cli.update_notifier.update.UPDATE_COMMANDS", ["true"])
@pytest.fixture(autouse=True)
def telemetry_events(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]:
events: list[dict[str, Any]] = []
def record_telemetry(
self: Any, event_name: str, properties: dict[str, Any]
) -> None:
events.append({"event_name": event_name, "properties": properties})
monkeypatch.setattr(
"vibe.core.telemetry.send.TelemetryClient.send_telemetry_event",
record_telemetry,
)
return events
@pytest.fixture
def vibe_app() -> VibeApp:
return build_test_vibe_app()

View file

@ -0,0 +1,57 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
from vibe.core.paths.config_paths import resolve_local_skills_dirs
class TestResolveLocalSkillsDirs:
def test_returns_empty_list_when_dir_not_trusted(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = False
assert resolve_local_skills_dirs(tmp_path) == []
def test_returns_empty_list_when_trusted_but_no_skills_dirs(
self, tmp_path: Path
) -> None:
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
assert resolve_local_skills_dirs(tmp_path) == []
def test_returns_vibe_skills_only_when_only_it_exists(self, tmp_path: Path) -> None:
vibe_skills = tmp_path / ".vibe" / "skills"
vibe_skills.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = resolve_local_skills_dirs(tmp_path)
assert result == [vibe_skills]
def test_returns_agents_skills_only_when_only_it_exists(
self, tmp_path: Path
) -> None:
agents_skills = tmp_path / ".agents" / "skills"
agents_skills.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = resolve_local_skills_dirs(tmp_path)
assert result == [agents_skills]
def test_returns_both_in_order_when_both_exist(self, tmp_path: Path) -> None:
vibe_skills = tmp_path / ".vibe" / "skills"
agents_skills = tmp_path / ".agents" / "skills"
vibe_skills.mkdir(parents=True)
agents_skills.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = resolve_local_skills_dirs(tmp_path)
assert result == [vibe_skills, agents_skills]
def test_ignores_vibe_skills_when_file_not_dir(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "skills").write_text("", encoding="utf-8")
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = resolve_local_skills_dirs(tmp_path)
assert result == []

View file

@ -0,0 +1,280 @@
from __future__ import annotations
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from textwrap import dedent
from unittest.mock import MagicMock, patch
import pytest
from vibe.core.utils import StructuredLogFormatter, apply_logging_config
@pytest.fixture
def mock_log_dir(tmp_path: Path):
"""Mock LOG_DIR and LOG_FILE to use tmp_path for testing."""
mock_dir = MagicMock()
mock_dir.path = tmp_path
mock_file = MagicMock()
mock_file.path = tmp_path / "vibe.log"
with (
patch("vibe.core.utils.LOG_DIR", mock_dir),
patch("vibe.core.utils.LOG_FILE", mock_file),
):
yield tmp_path
class TestStructuredFormatter:
def test_format_contains_required_fields(self) -> None:
formatter = StructuredLogFormatter()
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Test message",
args=(),
exc_info=None,
)
output = formatter.format(record)
parts = output.split(" ", 4)
assert len(parts) == 5
assert "T" in parts[0]
assert parts[1].isdigit()
assert parts[2].isdigit()
assert parts[3] == "INFO"
assert parts[4] == "Test message"
def test_format_includes_exception(self) -> None:
formatter = StructuredLogFormatter()
try:
raise ValueError("test error")
except ValueError:
import sys
exc_info = sys.exc_info()
record = logging.LogRecord(
name="test_logger",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Error occurred",
args=(),
exc_info=exc_info,
)
output = formatter.format(record)
assert "Error occurred" in output
assert "ValueError" in output
assert "test error" in output
def test_format_escapes_newlines_in_message(self) -> None:
formatter = StructuredLogFormatter()
multiline_msg = dedent("""
Line one
Line two
Line three""")
record = logging.LogRecord(
name="test_logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg=multiline_msg,
args=(),
exc_info=None,
)
output = formatter.format(record)
assert "\n" not in output
assert "Line one\\nLine two\\nLine three" in output
def test_format_escapes_newlines_in_exception(self) -> None:
formatter = StructuredLogFormatter()
try:
raise ValueError("test error")
except ValueError:
import sys
exc_info = sys.exc_info()
record = logging.LogRecord(
name="test_logger",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Error occurred",
args=(),
exc_info=exc_info,
)
output = formatter.format(record)
assert "\n" not in output
assert "ValueError" in output
assert "test error" in output
assert "\\n" in output
def test_format_output_is_single_line(self) -> None:
formatter = StructuredLogFormatter()
try:
error_msg = dedent("""
multi
line
error""")
raise RuntimeError(error_msg)
except RuntimeError:
import sys
exc_info = sys.exc_info()
multiline_msg = dedent("""
Something
went
wrong""")
record = logging.LogRecord(
name="test_logger",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg=multiline_msg,
args=(),
exc_info=exc_info,
)
output = formatter.format(record)
lines = output.split("\n")
assert len(lines) == 1
class TestApplyLoggingConfig:
def test_adds_handler_to_logger(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_apply_logging")
initial_handler_count = len(test_logger.handlers)
apply_logging_config(test_logger)
assert len(test_logger.handlers) == initial_handler_count + 1
def test_creates_log_file(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_log_file")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.info("Test log entry")
log_file = mock_log_dir / "vibe.log"
assert log_file.exists()
def test_log_entry_format(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_format")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.warning("Test warning message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "WARNING" in content
assert "Test warning message" in content
def test_respects_log_level(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "WARNING")
test_logger = logging.getLogger("test_level_filter")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.debug("Debug message")
test_logger.info("Info message")
test_logger.warning("Warning message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "Debug message" not in content
assert "Info message" not in content
assert "Warning message" in content
def test_creates_log_directory_if_missing(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
log_dir = tmp_path / "nested" / "logs"
mock_dir = MagicMock()
mock_dir.path = log_dir
mock_file = MagicMock()
mock_file.path = log_dir / "vibe.log"
with (
patch("vibe.core.utils.LOG_DIR", mock_dir),
patch("vibe.core.utils.LOG_FILE", mock_file),
):
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_mkdir")
apply_logging_config(test_logger)
assert log_dir.exists()
def test_debug_mode_overrides_log_level(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "WARNING")
monkeypatch.setenv("DEBUG_MODE", "true")
test_logger = logging.getLogger("test_debug_mode")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.debug("Debug message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "Debug message" in content
def test_invalid_log_level_defaults_to_warning(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_LEVEL", "INVALID")
test_logger = logging.getLogger("test_invalid_level")
test_logger.setLevel(logging.DEBUG)
apply_logging_config(test_logger)
test_logger.info("Info message")
test_logger.warning("Warning message")
log_file = mock_log_dir / "vibe.log"
content = log_file.read_text()
assert "Info message" not in content
assert "Warning message" in content
def test_log_max_bytes_from_env(
self, mock_log_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("LOG_MAX_BYTES", "5242880") # 5 MB
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
test_logger = logging.getLogger("test_max_bytes")
apply_logging_config(test_logger)
# Verify handler was added with correct maxBytes
handler = test_logger.handlers[-1]
assert isinstance(handler, RotatingFileHandler)
assert handler.maxBytes == 5242880

View file

@ -0,0 +1,304 @@
from __future__ import annotations
import pytest
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
from vibe.core.proxy_setup import (
SUPPORTED_PROXY_VARS,
ProxySetupError,
get_current_proxy_settings,
parse_proxy_command,
set_proxy_var,
unset_proxy_var,
)
def _write_env_file(content: str) -> None:
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
GLOBAL_ENV_FILE.path.write_text(content, encoding="utf-8")
class TestProxySetupError:
def test_inherits_from_exception(self) -> None:
assert issubclass(ProxySetupError, Exception)
def test_preserves_message(self) -> None:
error = ProxySetupError("test message")
assert str(error) == "test message"
class TestSupportedProxyVars:
def test_contains_all_expected_keys(self) -> None:
expected_keys = {
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"NO_PROXY",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
}
assert set(SUPPORTED_PROXY_VARS.keys()) == expected_keys
def test_all_keys_are_uppercase(self) -> None:
for key in SUPPORTED_PROXY_VARS:
assert key == key.upper()
def test_all_values_are_non_empty_strings(self) -> None:
for value in SUPPORTED_PROXY_VARS.values():
assert isinstance(value, str)
assert len(value) > 0
class TestGetCurrentProxySettings:
def test_returns_all_none_when_env_file_does_not_exist(self) -> None:
result = get_current_proxy_settings()
assert all(value is None for value in result.values())
def test_returns_dict_with_all_supported_keys(self) -> None:
result = get_current_proxy_settings()
assert set(result.keys()) == set(SUPPORTED_PROXY_VARS.keys())
def test_returns_values_from_env_file(self) -> None:
_write_env_file(
"HTTP_PROXY=http://proxy:8080\nHTTPS_PROXY=https://proxy:8443\n"
)
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://proxy:8080"
assert result["HTTPS_PROXY"] == "https://proxy:8443"
def test_returns_none_for_unset_keys(self) -> None:
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://proxy:8080"
assert result["HTTPS_PROXY"] is None
assert result["ALL_PROXY"] is None
def test_ignores_non_proxy_vars_in_env_file(self) -> None:
_write_env_file("HTTP_PROXY=http://proxy:8080\nOTHER_VAR=ignored\n")
result = get_current_proxy_settings()
assert "OTHER_VAR" not in result
assert result["HTTP_PROXY"] == "http://proxy:8080"
def test_handles_values_with_special_characters(self) -> None:
_write_env_file("HTTP_PROXY=http://user:p@ss@proxy:8080\n")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://user:p@ss@proxy:8080"
def test_returns_all_none_when_env_file_read_fails(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
_write_env_file("HTTP_PROXY=http://proxy:8080\n")
def raise_error(*args, **kwargs):
raise OSError("Permission denied")
monkeypatch.setattr("vibe.core.proxy_setup.dotenv_values", raise_error)
result = get_current_proxy_settings()
assert all(value is None for value in result.values())
class TestSetProxyVar:
def test_sets_valid_proxy_var(self) -> None:
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://proxy:8080"
@pytest.mark.parametrize("key", SUPPORTED_PROXY_VARS.keys())
def test_sets_all_supported_vars(self, key: str) -> None:
set_proxy_var(key, "test-value")
result = get_current_proxy_settings()
assert result[key] == "test-value"
def test_uppercases_key_before_validation(self) -> None:
set_proxy_var("http_proxy", "http://proxy:8080")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://proxy:8080"
def test_raises_error_for_unknown_key(self) -> None:
with pytest.raises(ProxySetupError) as exc_info:
set_proxy_var("UNKNOWN_KEY", "value")
assert "Unknown key 'UNKNOWN_KEY'" in str(exc_info.value)
def test_error_message_contains_supported_keys(self) -> None:
with pytest.raises(ProxySetupError) as exc_info:
set_proxy_var("UNKNOWN_KEY", "value")
error_msg = str(exc_info.value)
assert "HTTP_PROXY" in error_msg
assert "HTTPS_PROXY" in error_msg
def test_creates_env_file_if_missing(self) -> None:
assert not GLOBAL_ENV_FILE.path.exists()
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
assert GLOBAL_ENV_FILE.path.exists()
def test_overwrites_existing_value(self) -> None:
set_proxy_var("HTTP_PROXY", "http://old:8080")
set_proxy_var("HTTP_PROXY", "http://new:8080")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://new:8080"
def test_preserves_other_values(self) -> None:
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://proxy:8080"
assert result["HTTPS_PROXY"] == "https://proxy:8443"
def test_handles_value_with_spaces(self) -> None:
set_proxy_var("NO_PROXY", "localhost, 127.0.0.1, .local")
result = get_current_proxy_settings()
assert result["NO_PROXY"] == "localhost, 127.0.0.1, .local"
def test_handles_url_with_credentials(self) -> None:
set_proxy_var("HTTP_PROXY", "http://user:password@proxy:8080")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] == "http://user:password@proxy:8080"
class TestUnsetProxyVar:
def test_removes_existing_var(self) -> None:
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
unset_proxy_var("HTTP_PROXY")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] is None
def test_uppercases_key_before_validation(self) -> None:
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
unset_proxy_var("http_proxy")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] is None
def test_raises_error_for_unknown_key(self) -> None:
with pytest.raises(ProxySetupError) as exc_info:
unset_proxy_var("UNKNOWN_KEY")
assert "Unknown key 'UNKNOWN_KEY'" in str(exc_info.value)
def test_error_message_contains_supported_keys(self) -> None:
with pytest.raises(ProxySetupError) as exc_info:
unset_proxy_var("UNKNOWN_KEY")
error_msg = str(exc_info.value)
assert "HTTP_PROXY" in error_msg
def test_no_op_when_env_file_does_not_exist(self) -> None:
assert not GLOBAL_ENV_FILE.path.exists()
unset_proxy_var("HTTP_PROXY")
assert not GLOBAL_ENV_FILE.path.exists()
def test_no_op_when_key_not_in_file(self) -> None:
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
unset_proxy_var("HTTP_PROXY")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] is None
assert result["HTTPS_PROXY"] == "https://proxy:8443"
def test_preserves_other_values(self) -> None:
set_proxy_var("HTTP_PROXY", "http://proxy:8080")
set_proxy_var("HTTPS_PROXY", "https://proxy:8443")
unset_proxy_var("HTTP_PROXY")
result = get_current_proxy_settings()
assert result["HTTP_PROXY"] is None
assert result["HTTPS_PROXY"] == "https://proxy:8443"
@pytest.mark.parametrize("key", SUPPORTED_PROXY_VARS.keys())
def test_all_supported_vars_can_be_unset(self, key: str) -> None:
set_proxy_var(key, "test-value")
unset_proxy_var(key)
result = get_current_proxy_settings()
assert result[key] is None
class TestParseProxyCommand:
def test_parses_key_only(self) -> None:
key, value = parse_proxy_command("HTTP_PROXY")
assert key == "HTTP_PROXY"
assert value is None
def test_parses_key_and_value(self) -> None:
key, value = parse_proxy_command("HTTP_PROXY http://proxy:8080")
assert key == "HTTP_PROXY"
assert value == "http://proxy:8080"
def test_uppercases_key(self) -> None:
key, value = parse_proxy_command("http_proxy")
assert key == "HTTP_PROXY"
def test_preserves_value_case(self) -> None:
key, value = parse_proxy_command("HTTP_PROXY http://Proxy:8080")
assert value == "http://Proxy:8080"
def test_strips_leading_whitespace(self) -> None:
key, value = parse_proxy_command(" HTTP_PROXY")
assert key == "HTTP_PROXY"
def test_strips_trailing_whitespace(self) -> None:
key, value = parse_proxy_command("HTTP_PROXY ")
assert key == "HTTP_PROXY"
assert value is None
def test_splits_on_first_space_only(self) -> None:
key, value = parse_proxy_command("NO_PROXY localhost, 127.0.0.1, .local")
assert key == "NO_PROXY"
assert value == "localhost, 127.0.0.1, .local"
def test_raises_error_for_empty_string(self) -> None:
with pytest.raises(ProxySetupError) as exc_info:
parse_proxy_command("")
assert "No key provided" in str(exc_info.value)
def test_raises_error_for_whitespace_only(self) -> None:
with pytest.raises(ProxySetupError) as exc_info:
parse_proxy_command(" ")
assert "No key provided" in str(exc_info.value)
def test_handles_tab_as_separator(self) -> None:
key, value = parse_proxy_command("HTTP_PROXY\thttp://proxy:8080")
assert key == "HTTP_PROXY"
assert value == "http://proxy:8080"
def test_handles_multiple_spaces_as_separator(self) -> None:
key, value = parse_proxy_command("HTTP_PROXY http://proxy:8080")
assert key == "HTTP_PROXY"
assert value == "http://proxy:8080"

View file

@ -0,0 +1,269 @@
from __future__ import annotations
import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from tests.conftest import build_test_vibe_config
from tests.stubs.fake_tool import FakeTool, FakeToolArgs
from vibe.core.agent_loop import ToolDecision, ToolExecutionResponse
from vibe.core.config import Backend
from vibe.core.llm.format import ResolvedToolCall
from vibe.core.telemetry.send import DATALAKE_EVENTS_URL, TelemetryClient
from vibe.core.tools.base import BaseTool, ToolPermission
from vibe.core.utils import get_user_agent
_original_send_telemetry_event = TelemetryClient.send_telemetry_event
from vibe.core.tools.builtins.write_file import WriteFile, WriteFileArgs
def _make_resolved_tool_call(
tool_name: str, args_dict: dict[str, Any]
) -> ResolvedToolCall:
if tool_name == "write_file":
validated = WriteFileArgs(
path="foo.txt", content="x", overwrite=args_dict.get("overwrite", False)
)
cls: type[BaseTool] = WriteFile
else:
validated = FakeToolArgs()
cls = FakeTool
return ResolvedToolCall(
tool_name=tool_name, tool_class=cls, validated_args=validated, call_id="call_1"
)
def _run_telemetry_tasks() -> None:
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(asyncio.sleep(0))
finally:
loop.close()
class TestTelemetryClient:
def test_send_telemetry_event_does_nothing_when_api_key_is_none(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
env_key = config.get_provider_for_model(
config.get_active_model()
).api_key_env_var
monkeypatch.delenv(env_key, raising=False)
client = TelemetryClient(config_getter=lambda: config)
assert client._get_mistral_api_key() is None
client._client = MagicMock()
client._client.post = AsyncMock()
client.send_telemetry_event("vibe/test", {})
_run_telemetry_tasks()
client._client.post.assert_not_called()
def test_send_telemetry_event_does_nothing_when_disabled(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
config = build_test_vibe_config(disable_telemetry=True)
env_key = config.get_provider_for_model(
config.get_active_model()
).api_key_env_var
monkeypatch.setenv(env_key, "sk-test")
client = TelemetryClient(config_getter=lambda: config)
client._client = MagicMock()
client._client.post = AsyncMock()
client.send_telemetry_event("vibe/test", {})
_run_telemetry_tasks()
client._client.post.assert_not_called()
@pytest.mark.asyncio
async def test_send_telemetry_event_posts_when_enabled(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
)
config = build_test_vibe_config(disable_telemetry=False)
env_key = config.get_provider_for_model(
config.get_active_model()
).api_key_env_var
monkeypatch.setenv(env_key, "sk-test")
client = TelemetryClient(config_getter=lambda: config)
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
client._client = MagicMock()
client._client.post = mock_post
client._client.aclose = AsyncMock()
client.send_telemetry_event("vibe/test_event", {"key": "value"})
await client.aclose()
mock_post.assert_called_once_with(
DATALAKE_EVENTS_URL,
json={"event": "vibe/test_event", "properties": {"key": "value"}},
headers={
"Content-Type": "application/json",
"Authorization": "Bearer sk-test",
"User-Agent": get_user_agent(Backend.MISTRAL),
},
)
def test_send_tool_call_finished_payload_shape(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
tool_call = _make_resolved_tool_call("todo", {})
decision = ToolDecision(
verdict=ToolExecutionResponse.EXECUTE, approval_type=ToolPermission.ALWAYS
)
client.send_tool_call_finished(
tool_call=tool_call,
status="success",
decision=decision,
agent_profile_name="default",
)
assert len(telemetry_events) == 1
event_name = telemetry_events[0]["event_name"]
assert event_name == "vibe/tool_call_finished"
properties = telemetry_events[0]["properties"]
assert properties["tool_name"] == "todo"
assert properties["status"] == "success"
assert properties["decision"] == "execute"
assert properties["approval_type"] == "always"
assert properties["agent_profile_name"] == "default"
assert properties["nb_files_created"] == 0
assert properties["nb_files_modified"] == 0
def test_send_tool_call_finished_nb_files_created_write_file_new(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
tool_call = _make_resolved_tool_call("write_file", {"overwrite": False})
client.send_tool_call_finished(
tool_call=tool_call,
status="success",
decision=None,
agent_profile_name="default",
result={"file_existed": False},
)
assert telemetry_events[0]["properties"]["nb_files_created"] == 1
assert telemetry_events[0]["properties"]["nb_files_modified"] == 0
def test_send_tool_call_finished_nb_files_modified_write_file_overwrite(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
tool_call = _make_resolved_tool_call("write_file", {"overwrite": True})
client.send_tool_call_finished(
tool_call=tool_call,
status="success",
decision=None,
agent_profile_name="default",
result={"file_existed": True},
)
assert telemetry_events[0]["properties"]["nb_files_created"] == 0
assert telemetry_events[0]["properties"]["nb_files_modified"] == 1
def test_send_tool_call_finished_decision_none(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
tool_call = _make_resolved_tool_call("todo", {})
client.send_tool_call_finished(
tool_call=tool_call,
status="skipped",
decision=None,
agent_profile_name="default",
)
assert telemetry_events[0]["properties"]["decision"] is None
assert telemetry_events[0]["properties"]["approval_type"] is None
def test_send_user_copied_text_payload(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
client.send_user_copied_text("hello world")
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe/user_copied_text"
assert telemetry_events[0]["properties"]["text_length"] == 11
def test_send_user_cancelled_action_payload(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
client.send_user_cancelled_action("interrupt_agent")
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe/user_cancelled_action"
assert telemetry_events[0]["properties"]["action"] == "interrupt_agent"
def test_send_auto_compact_triggered_payload(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
client.send_auto_compact_triggered()
assert len(telemetry_events) == 1
assert telemetry_events[0]["event_name"] == "vibe/auto_compact_triggered"
def test_send_slash_command_used_payload(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
client.send_slash_command_used("help", "builtin")
client.send_slash_command_used("my_skill", "skill")
assert len(telemetry_events) == 2
assert telemetry_events[0]["event_name"] == "vibe/slash_command_used"
assert telemetry_events[0]["properties"]["command"] == "help"
assert telemetry_events[0]["properties"]["command_type"] == "builtin"
assert telemetry_events[1]["properties"]["command"] == "my_skill"
assert telemetry_events[1]["properties"]["command_type"] == "skill"
def test_send_new_session_payload(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(disable_telemetry=False)
client = TelemetryClient(config_getter=lambda: config)
client.send_new_session(
has_agents_md=True,
nb_skills=2,
nb_mcp_servers=1,
nb_models=3,
entrypoint="cli",
)
assert len(telemetry_events) == 1
event_name = telemetry_events[0]["event_name"]
assert event_name == "vibe/new_session"
properties = telemetry_events[0]["properties"]
assert properties["has_agents_md"] is True
assert properties["nb_skills"] == 2
assert properties["nb_mcp_servers"] == 1
assert properties["nb_models"] == 3
assert properties["entrypoint"] == "cli"
assert "version" in properties

View file

@ -8,7 +8,12 @@ import pytest
import tomli_w
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
from vibe.core.trusted_folders import TrustedFoldersManager
from vibe.core.trusted_folders import (
AGENTS_MD_FILENAMES,
TrustedFoldersManager,
has_agents_md_file,
has_trustable_content,
)
class TestTrustedFoldersManager:
@ -203,3 +208,48 @@ class TestTrustedFoldersManager:
manager.add_trusted(tmp_path)
assert manager.is_trusted(tmp_path) is True
class TestHasAgentsMdFile:
def test_returns_false_for_empty_directory(self, tmp_path: Path) -> None:
assert has_agents_md_file(tmp_path) is False
def test_returns_true_when_agents_md_exists(self, tmp_path: Path) -> None:
(tmp_path / "AGENTS.md").write_text("# Agents", encoding="utf-8")
assert has_agents_md_file(tmp_path) is True
def test_returns_true_when_vibe_md_exists(self, tmp_path: Path) -> None:
(tmp_path / "VIBE.md").write_text("# Vibe", encoding="utf-8")
assert has_agents_md_file(tmp_path) is True
def test_returns_true_when_dot_vibe_md_exists(self, tmp_path: Path) -> None:
(tmp_path / ".vibe.md").write_text("# Vibe", encoding="utf-8")
assert has_agents_md_file(tmp_path) is True
def test_returns_false_when_only_other_files_exist(self, tmp_path: Path) -> None:
(tmp_path / "README.md").write_text("", encoding="utf-8")
(tmp_path / ".vibe").mkdir()
assert has_agents_md_file(tmp_path) is False
def test_agents_md_filenames_constant(self) -> None:
assert AGENTS_MD_FILENAMES == ["AGENTS.md", "VIBE.md", ".vibe.md"]
class TestHasTrustableContent:
def test_returns_true_when_vibe_dir_exists(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
assert has_trustable_content(tmp_path) is True
def test_returns_true_when_agents_dir_exists(self, tmp_path: Path) -> None:
(tmp_path / ".agents").mkdir()
assert has_trustable_content(tmp_path) is True
def test_returns_true_when_agents_md_filename_exists(self, tmp_path: Path) -> None:
for name in AGENTS_MD_FILENAMES:
(tmp_path / name).write_text("", encoding="utf-8")
assert has_trustable_content(tmp_path) is True
(tmp_path / name).unlink()
def test_returns_false_when_no_trustable_content(self, tmp_path: Path) -> None:
(tmp_path / "other.txt").write_text("", encoding="utf-8")
assert has_trustable_content(tmp_path) is False

View file

@ -41,7 +41,6 @@ async def test_ui_gets_through_the_onboarding_successfully() -> None:
async with app.run_test() as pilot:
await pass_welcome_screen(pilot)
api_screen = app.screen
input_widget = api_screen.query_one("#key", Input)
await pilot.press(*api_key_value)

View file

@ -67,8 +67,8 @@ def create_test_session():
if metadata is None:
metadata = {
"session_id": session_id,
"start_time": "2023-01-01T12:00:00",
"end_time": "2023-01-01T12:05:00",
"start_time": "2023-01-01T12:00:00Z",
"end_time": "2023-01-01T12:05:00Z",
"total_messages": 2,
"stats": {
"steps": 1,
@ -635,3 +635,190 @@ class TestSessionLoaderEdgeCases:
assert messages[0].content == "Hello"
assert messages[1].role == Role.assistant
assert messages[1].content == "Hi there!"
@pytest.fixture
def create_test_session_with_cwd():
def _create_session(
session_dir: Path,
session_id: str,
cwd: str,
title: str | None = None,
end_time: str | None = None,
) -> Path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
session_folder = session_dir / f"test_{timestamp}_{session_id[:8]}"
session_folder.mkdir(exist_ok=True)
messages_file = session_folder / "messages.jsonl"
messages_file.write_text('{"role": "user", "content": "Hello"}\n')
metadata = {
"session_id": session_id,
"start_time": "2024-01-01T12:00:00Z",
"end_time": end_time or "2024-01-01T12:05:00Z",
"environment": {"working_directory": cwd},
"title": title,
}
metadata_file = session_folder / "meta.json"
with metadata_file.open("w", encoding="utf-8") as f:
json.dump(metadata, f)
return session_folder
return _create_session
class TestSessionLoaderListSessions:
def test_list_sessions_empty(self, session_config: SessionLoggingConfig) -> None:
result = SessionLoader.list_sessions(session_config)
assert result == []
def test_list_sessions_returns_all_sessions(
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
) -> None:
session_dir = Path(session_config.save_dir)
create_test_session_with_cwd(
session_dir,
"aaaaaaaa-1111",
"/home/user/project1",
title="First session",
end_time="2024-01-01T12:00:00Z",
)
create_test_session_with_cwd(
session_dir,
"bbbbbbbb-2222",
"/home/user/project2",
title="Second session",
end_time="2024-01-01T13:00:00Z",
)
result = SessionLoader.list_sessions(session_config)
assert len(result) == 2
session_ids = {s["session_id"] for s in result}
assert "aaaaaaaa-1111" in session_ids
assert "bbbbbbbb-2222" in session_ids
def test_list_sessions_filters_by_cwd(
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
) -> None:
session_dir = Path(session_config.save_dir)
create_test_session_with_cwd(
session_dir,
"aaaaaaaa-proj1",
"/home/user/project1",
title="Project 1 session",
)
create_test_session_with_cwd(
session_dir,
"bbbbbbbb-proj2",
"/home/user/project2",
title="Project 2 session",
)
create_test_session_with_cwd(
session_dir,
"cccccccc-proj1",
"/home/user/project1",
title="Another Project 1 session",
)
result = SessionLoader.list_sessions(session_config, cwd="/home/user/project1")
assert len(result) == 2
for session in result:
assert session["cwd"] == "/home/user/project1"
def test_list_sessions_includes_all_fields(
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
) -> None:
session_dir = Path(session_config.save_dir)
create_test_session_with_cwd(
session_dir,
"test-session-123",
"/home/user/project",
title="Test Session Title",
end_time="2024-01-15T10:30:00Z",
)
result = SessionLoader.list_sessions(session_config)
assert len(result) == 1
session = result[0]
assert session["session_id"] == "test-session-123"
assert session["cwd"] == "/home/user/project"
assert session["title"] == "Test Session Title"
def test_list_sessions_skips_invalid_sessions(
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
) -> None:
session_dir = Path(session_config.save_dir)
create_test_session_with_cwd(
session_dir, "valid-se", "/home/user/project", title="Valid Session"
)
invalid_session = session_dir / "test_20240101_120000_invalid1"
invalid_session.mkdir()
(invalid_session / "meta.json").write_text('{"session_id": "invalid"}')
no_id_session = session_dir / "test_20240101_120001_noid0000"
no_id_session.mkdir()
(no_id_session / "messages.jsonl").write_text(
'{"role": "user", "content": "Hello"}\n'
)
(no_id_session / "meta.json").write_text(
'{"environment": {"working_directory": "/test"}}'
)
result = SessionLoader.list_sessions(session_config)
assert len(result) == 1
assert result[0]["session_id"] == "valid-se"
def test_list_sessions_nonexistent_save_dir(self) -> None:
bad_config = SessionLoggingConfig(
save_dir="/nonexistent/path", session_prefix="test", enabled=True
)
result = SessionLoader.list_sessions(bad_config)
assert result == []
def test_list_sessions_handles_missing_environment(
self, session_config: SessionLoggingConfig
) -> None:
session_dir = Path(session_config.save_dir)
session_folder = session_dir / "test_20240101_120000_noenv000"
session_folder.mkdir()
(session_folder / "messages.jsonl").write_text(
'{"role": "user", "content": "Hello"}\n'
)
(session_folder / "meta.json").write_text(
'{"session_id": "noenv000", "end_time": "2024-01-01T12:00:00Z"}'
)
result = SessionLoader.list_sessions(session_config)
assert len(result) == 1
assert result[0]["session_id"] == "noenv000"
assert result[0]["cwd"] == "" # Empty string when no working_directory
def test_list_sessions_handles_none_title(
self, session_config: SessionLoggingConfig, create_test_session_with_cwd
) -> None:
session_dir = Path(session_config.save_dir)
create_test_session_with_cwd(
session_dir, "notitle0", "/home/user/project", title=None
)
result = SessionLoader.list_sessions(session_config)
assert len(result) == 1
assert result[0]["session_id"] == "notitle0"
assert result[0]["title"] is None

View file

@ -8,6 +8,7 @@ from tests.conftest import build_test_vibe_config
from tests.skills.conftest import create_skill
from vibe.core.config import VibeConfig
from vibe.core.skills.manager import SkillManager
from vibe.core.trusted_folders import trusted_folders_manager
@pytest.fixture
@ -184,6 +185,85 @@ class TestSkillManagerParsing:
class TestSkillManagerSearchPaths:
def test_discovers_from_vibe_skills_when_cwd_trusted(
self, tmp_working_directory: Path
) -> None:
trusted_folders_manager.add_trusted(tmp_working_directory)
vibe_skills = tmp_working_directory / ".vibe" / "skills"
vibe_skills.mkdir(parents=True)
create_skill(vibe_skills, "vibe-skill", "Skill from .vibe/skills")
config = build_test_vibe_config(
system_prompt_id="tests", include_project_context=False, skill_paths=[]
)
manager = SkillManager(lambda: config)
assert "vibe-skill" in manager.available_skills
assert (
manager.available_skills["vibe-skill"].description
== "Skill from .vibe/skills"
)
def test_discovers_from_agents_skills_when_cwd_trusted(
self, tmp_working_directory: Path
) -> None:
trusted_folders_manager.add_trusted(tmp_working_directory)
agents_skills = tmp_working_directory / ".agents" / "skills"
agents_skills.mkdir(parents=True)
create_skill(agents_skills, "agents-skill", "Skill from .agents/skills")
config = build_test_vibe_config(
system_prompt_id="tests", include_project_context=False, skill_paths=[]
)
manager = SkillManager(lambda: config)
assert "agents-skill" in manager.available_skills
assert (
manager.available_skills["agents-skill"].description
== "Skill from .agents/skills"
)
def test_discovers_from_both_vibe_and_agents_skills_when_cwd_trusted(
self, tmp_working_directory: Path
) -> None:
trusted_folders_manager.add_trusted(tmp_working_directory)
vibe_skills = tmp_working_directory / ".vibe" / "skills"
agents_skills = tmp_working_directory / ".agents" / "skills"
vibe_skills.mkdir(parents=True)
agents_skills.mkdir(parents=True)
create_skill(vibe_skills, "vibe-only", "From .vibe")
create_skill(agents_skills, "agents-only", "From .agents")
config = build_test_vibe_config(
system_prompt_id="tests", include_project_context=False, skill_paths=[]
)
manager = SkillManager(lambda: config)
assert len(manager.available_skills) == 2
assert manager.available_skills["vibe-only"].description == "From .vibe"
assert manager.available_skills["agents-only"].description == "From .agents"
def test_first_discovered_wins_when_same_skill_in_vibe_and_agents(
self, tmp_working_directory: Path
) -> None:
trusted_folders_manager.add_trusted(tmp_working_directory)
vibe_skills = tmp_working_directory / ".vibe" / "skills"
agents_skills = tmp_working_directory / ".agents" / "skills"
vibe_skills.mkdir(parents=True)
agents_skills.mkdir(parents=True)
create_skill(vibe_skills, "shared-skill", "First from .vibe")
create_skill(agents_skills, "shared-skill", "Second from .agents")
config = build_test_vibe_config(
system_prompt_id="tests", include_project_context=False, skill_paths=[]
)
manager = SkillManager(lambda: config)
assert len(manager.available_skills) == 1
assert (
manager.available_skills["shared-skill"].description == "First from .vibe"
)
def test_discovers_from_multiple_skill_paths(self, tmp_path: Path) -> None:
# Create two separate skill directories
skills_dir_1 = tmp_path / "skills1"

View file

@ -111,7 +111,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Before After
Before After

View file

@ -141,7 +141,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -160,7 +160,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -164,7 +164,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#4b4e55" x="12.2" y="245.5" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#9a9b99" x="48.8" y="294.3" width="61" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -160,7 +160,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -160,7 +160,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -160,7 +160,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -159,7 +159,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -159,7 +159,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -0,0 +1,200 @@
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #9a9b99 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type&#160;</text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy&#160;setup&#160;opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="48.8" y="678.8" textLength="268.4" clip-path="url(#terminal-line-27)">Proxy&#160;setup&#160;cancelled.</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,200 @@
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #9a9b99 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">PrePopulatedProxySetupTestApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type&#160;</text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy&#160;setup&#160;opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="48.8" y="678.8" textLength="793" clip-path="url(#terminal-line-27)">Proxy&#160;settings&#160;saved.&#160;Restart&#160;the&#160;CLI&#160;for&#160;changes&#160;to&#160;take&#160;effect.</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,204 @@
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #4b4e55 }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #292929 }
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #868887 }
.terminal-r8 { fill: #949798 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#4b4e55" x="1207.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="48.8" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="20" textLength="414.8" clip-path="url(#terminal-line-0)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r2" x="1207.8" y="20" textLength="12.2" clip-path="url(#terminal-line-0)"></text><text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">Type&#160;</text><text class="terminal-r3" x="231.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">/help</text><text class="terminal-r1" x="292.8" y="44.4" textLength="256.2" clip-path="url(#terminal-line-1)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r5" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)"></text><text class="terminal-r1" x="48.8" y="93.2" textLength="256.2" clip-path="url(#terminal-line-3)">Proxy&#160;setup&#160;opened...</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r5" x="0" y="166.4" textLength="1220" clip-path="url(#terminal-line-6)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r5" x="0" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r6" x="24.4" y="190.8" textLength="231.8" clip-path="url(#terminal-line-7)">Proxy&#160;Configuration</text><text class="terminal-r5" x="1207.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r5" x="0" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r5" x="1207.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r5" x="0" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r6" x="24.4" y="239.6" textLength="122" clip-path="url(#terminal-line-9)">HTTP_PROXY</text><text class="terminal-r7" x="170.8" y="239.6" textLength="329.4" clip-path="url(#terminal-line-9)">Proxy&#160;URL&#160;for&#160;HTTP&#160;requests</text><text class="terminal-r5" x="1207.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r5" x="0" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r5" x="1207.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r5" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r8" x="48.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">N</text><text class="terminal-r7" x="61" y="288.4" textLength="73.2" clip-path="url(#terminal-line-11)">OT&#160;SET</text><text class="terminal-r5" x="1207.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r5" x="1207.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r5" x="0" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r6" x="24.4" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">HTTPS_PROXY</text><text class="terminal-r7" x="183" y="337.2" textLength="341.6" clip-path="url(#terminal-line-13)">Proxy&#160;URL&#160;for&#160;HTTPS&#160;requests</text><text class="terminal-r5" x="1207.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r5" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r5" x="1207.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r5" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r5" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r7" x="48.8" y="386" textLength="85.4" clip-path="url(#terminal-line-15)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r5" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r5" x="1207.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r5" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r6" x="24.4" y="434.8" textLength="109.8" clip-path="url(#terminal-line-17)">ALL_PROXY</text><text class="terminal-r7" x="158.6" y="434.8" textLength="451.4" clip-path="url(#terminal-line-17)">Proxy&#160;URL&#160;for&#160;all&#160;requests&#160;(fallback)</text><text class="terminal-r5" x="1207.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r5" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r5" x="1207.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r5" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r5" x="24.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r7" x="48.8" y="483.6" textLength="85.4" clip-path="url(#terminal-line-19)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r5" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r5" x="1207.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r5" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r6" x="24.4" y="532.4" textLength="97.6" clip-path="url(#terminal-line-21)">NO_PROXY</text><text class="terminal-r7" x="146.4" y="532.4" textLength="549" clip-path="url(#terminal-line-21)">Comma-separated&#160;list&#160;of&#160;hosts&#160;to&#160;bypass&#160;proxy</text><text class="terminal-r5" x="1207.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r5" x="1207.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r7" x="48.8" y="581.2" textLength="85.4" clip-path="url(#terminal-line-23)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r5" x="1207.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r5" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r6" x="24.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">SSL_CERT_FILE</text><text class="terminal-r7" x="207.4" y="630" textLength="427" clip-path="url(#terminal-line-25)">Path&#160;to&#160;custom&#160;SSL&#160;certificate&#160;file</text><text class="terminal-r5" x="1207.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r5" x="1207.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r5" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r7" x="48.8" y="678.8" textLength="85.4" clip-path="url(#terminal-line-27)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1207.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r6" x="24.4" y="727.6" textLength="146.4" clip-path="url(#terminal-line-29)">SSL_CERT_DIR</text><text class="terminal-r7" x="195.2" y="727.6" textLength="549" clip-path="url(#terminal-line-29)">Path&#160;to&#160;directory&#160;containing&#160;SSL&#160;certificates</text><text class="terminal-r5" x="1207.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r5" x="1207.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r5" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r7" x="48.8" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="512.4" clip-path="url(#terminal-line-33)">↑↓&#160;navigate&#160;&#160;Enter&#160;save&#160;&amp;&#160;exit&#160;&#160;ESC&#160;cancel</text><text class="terminal-r5" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,203 @@
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #4b4e55 }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #292929 }
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">PrePopulatedProxySetupTestApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#4b4e55" x="1207.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="48.8" y="269.9" width="256.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="305" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="20" textLength="414.8" clip-path="url(#terminal-line-0)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r2" x="1207.8" y="20" textLength="12.2" clip-path="url(#terminal-line-0)"></text><text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">Type&#160;</text><text class="terminal-r3" x="231.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">/help</text><text class="terminal-r1" x="292.8" y="44.4" textLength="256.2" clip-path="url(#terminal-line-1)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r5" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)"></text><text class="terminal-r1" x="48.8" y="93.2" textLength="256.2" clip-path="url(#terminal-line-3)">Proxy&#160;setup&#160;opened...</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r5" x="0" y="166.4" textLength="1220" clip-path="url(#terminal-line-6)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r5" x="0" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r6" x="24.4" y="190.8" textLength="231.8" clip-path="url(#terminal-line-7)">Proxy&#160;Configuration</text><text class="terminal-r5" x="1207.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r5" x="0" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r5" x="1207.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r5" x="0" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r6" x="24.4" y="239.6" textLength="122" clip-path="url(#terminal-line-9)">HTTP_PROXY</text><text class="terminal-r7" x="170.8" y="239.6" textLength="329.4" clip-path="url(#terminal-line-9)">Proxy&#160;URL&#160;for&#160;HTTP&#160;requests</text><text class="terminal-r5" x="1207.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r5" x="0" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r5" x="1207.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r5" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="48.8" y="288.4" textLength="256.2" clip-path="url(#terminal-line-11)">http://old-proxy:8080</text><text class="terminal-r5" x="1207.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r5" x="1207.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r5" x="0" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r6" x="24.4" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">HTTPS_PROXY</text><text class="terminal-r7" x="183" y="337.2" textLength="341.6" clip-path="url(#terminal-line-13)">Proxy&#160;URL&#160;for&#160;HTTPS&#160;requests</text><text class="terminal-r5" x="1207.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r5" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r5" x="1207.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r5" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r5" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="48.8" y="386" textLength="1146.8" clip-path="url(#terminal-line-15)">https://old-proxy:8443&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-r5" x="1207.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r5" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r5" x="1207.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r5" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r6" x="24.4" y="434.8" textLength="109.8" clip-path="url(#terminal-line-17)">ALL_PROXY</text><text class="terminal-r7" x="158.6" y="434.8" textLength="451.4" clip-path="url(#terminal-line-17)">Proxy&#160;URL&#160;for&#160;all&#160;requests&#160;(fallback)</text><text class="terminal-r5" x="1207.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r5" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r5" x="1207.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r5" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r5" x="24.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r7" x="48.8" y="483.6" textLength="85.4" clip-path="url(#terminal-line-19)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r5" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r5" x="1207.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r5" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r6" x="24.4" y="532.4" textLength="97.6" clip-path="url(#terminal-line-21)">NO_PROXY</text><text class="terminal-r7" x="146.4" y="532.4" textLength="549" clip-path="url(#terminal-line-21)">Comma-separated&#160;list&#160;of&#160;hosts&#160;to&#160;bypass&#160;proxy</text><text class="terminal-r5" x="1207.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r5" x="1207.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r7" x="48.8" y="581.2" textLength="85.4" clip-path="url(#terminal-line-23)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r5" x="1207.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r5" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r6" x="24.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">SSL_CERT_FILE</text><text class="terminal-r7" x="207.4" y="630" textLength="427" clip-path="url(#terminal-line-25)">Path&#160;to&#160;custom&#160;SSL&#160;certificate&#160;file</text><text class="terminal-r5" x="1207.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r5" x="1207.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r5" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r7" x="48.8" y="678.8" textLength="85.4" clip-path="url(#terminal-line-27)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1207.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r6" x="24.4" y="727.6" textLength="146.4" clip-path="url(#terminal-line-29)">SSL_CERT_DIR</text><text class="terminal-r7" x="195.2" y="727.6" textLength="549" clip-path="url(#terminal-line-29)">Path&#160;to&#160;directory&#160;containing&#160;SSL&#160;certificates</text><text class="terminal-r5" x="1207.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r5" x="1207.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r5" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r7" x="48.8" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">NOT&#160;SET</text><text class="terminal-r5" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="512.4" clip-path="url(#terminal-line-33)">↑↓&#160;navigate&#160;&#160;Enter&#160;save&#160;&amp;&#160;exit&#160;&#160;ESC&#160;cancel</text><text class="terminal-r5" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,201 @@
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #9a9b99 }
.terminal-r5 { fill: #cc555a;font-weight: bold }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type&#160;</text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy&#160;setup&#160;opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r5" x="48.8" y="678.8" textLength="671" clip-path="url(#terminal-line-27)">Error:&#160;Failed&#160;to&#160;save&#160;proxy&#160;settings:&#160;Permission&#160;denied</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1,200 @@
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #9a9b99 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">ProxySetupTestApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">devstral-latest</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type&#160;</text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r4" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">Proxy&#160;setup&#160;opened...</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="48.8" y="678.8" textLength="793" clip-path="url(#terminal-line-27)">Proxy&#160;settings&#160;saved.&#160;Restart&#160;the&#160;CLI&#160;for&#160;changes&#160;to&#160;take&#160;effect.</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r4" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r4" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r4" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r4" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r4" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -162,7 +162,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -162,7 +162,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -162,7 +162,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -162,7 +162,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -163,7 +163,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -161,7 +161,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -161,7 +161,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -162,7 +162,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -162,7 +162,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -161,7 +161,7 @@
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v2.1.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="20" textLength="146.4" clip-path="url(#terminal-line-0)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="20" textLength="122" clip-path="url(#terminal-line-0)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="20" textLength="183" clip-path="url(#terminal-line-0)">devstral-latest</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="44.4" textLength="414.8" clip-path="url(#terminal-line-1)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Type&#160;</text><text class="terminal-r3" x="231.8" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">/help</text><text class="terminal-r1" x="292.8" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -0,0 +1,10 @@
from __future__ import annotations
import pytest
@pytest.fixture(autouse=True)
def _pin_banner_version(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"vibe.cli.textual_ui.widgets.banner.banner.__version__", "0.0.0"
)

View file

@ -0,0 +1,129 @@
from __future__ import annotations
import pytest
from textual.pilot import Pilot
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
from vibe.core.proxy_setup import get_current_proxy_settings, set_proxy_var
class ProxySetupTestApp(BaseSnapshotTestApp):
async def on_mount(self) -> None:
await super().on_mount()
await self._switch_to_proxy_setup_app()
class PrePopulatedProxySetupTestApp(BaseSnapshotTestApp):
async def on_mount(self) -> None:
set_proxy_var("HTTP_PROXY", "http://old-proxy:8080")
set_proxy_var("HTTPS_PROXY", "https://old-proxy:8443")
await super().on_mount()
await self._switch_to_proxy_setup_app()
def test_snapshot_proxy_setup_initial_empty(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
terminal_size=(100, 36),
run_before=run_before,
)
def test_snapshot_proxy_setup_initial_with_values(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_proxy_setup.py:PrePopulatedProxySetupTestApp",
terminal_size=(100, 36),
run_before=run_before,
)
def test_snapshot_proxy_setup_save_new_values(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.2)
await pilot.press(*"http://proxy.example.com:8080")
await pilot.press("down")
await pilot.press(*"https://proxy.example.com:8443")
await pilot.pause(0.1)
await pilot.press("enter")
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
terminal_size=(100, 36),
run_before=run_before,
)
settings = get_current_proxy_settings()
assert settings["HTTP_PROXY"] == "http://proxy.example.com:8080"
assert settings["HTTPS_PROXY"] == "https://proxy.example.com:8443"
def test_snapshot_proxy_setup_edit_existing_values(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.2)
await pilot.press("ctrl+u")
await pilot.press(*"http://new-proxy:9090")
await pilot.press("down")
await pilot.press("ctrl+u")
await pilot.pause(0.1)
await pilot.press("enter")
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_proxy_setup.py:PrePopulatedProxySetupTestApp",
terminal_size=(100, 36),
run_before=run_before,
)
settings = get_current_proxy_settings()
assert settings["HTTP_PROXY"] == "http://new-proxy:9090"
assert settings["HTTPS_PROXY"] is None
def test_snapshot_proxy_setup_cancel_discards_changes(
snap_compare: SnapCompare,
) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.2)
await pilot.press(*"http://should-not-save:8080")
await pilot.pause(0.1)
await pilot.press("escape")
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
terminal_size=(100, 36),
run_before=run_before,
)
settings = get_current_proxy_settings()
assert settings["HTTP_PROXY"] is None
def test_snapshot_proxy_setup_save_error(
snap_compare: SnapCompare, monkeypatch: pytest.MonkeyPatch
) -> None:
def raise_error(*args, **kwargs):
raise OSError("Permission denied")
monkeypatch.setattr("vibe.core.proxy_setup.set_key", raise_error)
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.2)
await pilot.press(*"http://proxy:8080")
await pilot.press("enter")
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_proxy_setup.py:ProxySetupTestApp",
terminal_size=(100, 36),
run_before=run_before,
)

View file

@ -16,7 +16,9 @@ from vibe.core.types import (
@pytest.mark.asyncio
async def test_auto_compact_triggers_and_batches_observer() -> None:
async def test_auto_compact_triggers_and_batches_observer(
telemetry_events: list[dict],
) -> None:
observed: list[tuple[Role, str | None]] = []
def observer(msg: LLMMessage) -> None:
@ -52,3 +54,10 @@ async def test_auto_compact_triggers_and_batches_observer() -> None:
assert roles == [Role.system, Role.user, Role.assistant]
assert observed[1][1] is not None and "<summary>" in observed[1][1]
assert observed[2][1] == "<final>"
auto_compact = [
e
for e in telemetry_events
if e.get("event_name") == "vibe/auto_compact_triggered"
]
assert len(auto_compact) == 1

View file

@ -17,7 +17,6 @@ from vibe.core.llm.exceptions import BackendErrorBuilder
from vibe.core.middleware import (
ConversationContext,
MiddlewareAction,
MiddlewarePipeline,
MiddlewareResult,
ResetReason,
)
@ -48,9 +47,6 @@ class InjectBeforeMiddleware:
action=MiddlewareAction.INJECT_MESSAGE, message=self.injected_message
)
async def after_turn(self, context: ConversationContext) -> MiddlewareResult:
return MiddlewareResult()
def reset(self, reset_reason: ResetReason = ResetReason.STOP) -> None:
return None
@ -99,15 +95,17 @@ async def test_act_flushes_batched_messages_with_injection_middleware(
async for _ in agent.act("How can you help?"):
pass
assert len(observed) == 3
assert [r for r, _ in observed] == [Role.system, Role.user, Role.assistant]
assert len(observed) == 4
assert [r for r, _ in observed] == [
Role.system,
Role.user,
Role.user,
Role.assistant,
]
assert observed[0][1] == "You are Vibe, a super useful programming assistant."
# injected content should be appended to the user's message before emission
assert (
observed[1][1]
== f"How can you help?\n\n{InjectBeforeMiddleware.injected_message}"
)
assert observed[2][1] == "I can write very efficient code."
assert observed[1][1] == "How can you help?"
assert observed[2][1] == InjectBeforeMiddleware.injected_message
assert observed[3][1] == "I can write very efficient code."
@pytest.mark.asyncio
@ -318,19 +316,14 @@ async def test_act_merges_streamed_tool_call_arguments() -> None:
@pytest.mark.asyncio
async def test_act_handles_user_cancellation_during_streaming() -> None:
class CountingMiddleware(MiddlewarePipeline):
class CountingMiddleware:
def __init__(self) -> None:
self.before_calls = 0
self.after_calls = 0
async def before_turn(self, context: ConversationContext) -> MiddlewareResult:
self.before_calls += 1
return MiddlewareResult()
async def after_turn(self, context: ConversationContext) -> MiddlewareResult:
self.after_calls += 1
return MiddlewareResult()
def reset(self, reset_reason: ResetReason = ResetReason.STOP) -> None:
return None
@ -371,7 +364,6 @@ async def test_act_handles_user_cancellation_during_streaming() -> None:
ToolResultEvent,
]
assert middleware.before_calls == 1
assert middleware.after_calls == 0
assert isinstance(events[-1], ToolResultEvent)
assert events[-1].skipped is True
assert events[-1].skip_reason is not None

View file

@ -378,9 +378,7 @@ class TestReloadPreservesMessages:
assert agent.messages[0].role == Role.system
@pytest.mark.asyncio
async def test_reload_notifies_observer_with_all_messages(
self, observer_capture
) -> None:
async def test_reload_does_not_reemit_to_observer(self, observer_capture) -> None:
observed, observer = observer_capture
backend = FakeBackend(mock_llm_chunk(content="Response"))
agent = build_test_agent_loop(
@ -394,10 +392,7 @@ class TestReloadPreservesMessages:
await agent.reload_with_initial_messages()
assert len(observed) == 3
assert observed[0].role == Role.system
assert observed[1].role == Role.user
assert observed[2].role == Role.assistant
assert len(observed) == 0
class TestCompactStatsHandling:

View file

@ -75,7 +75,9 @@ def make_agent_loop(
@pytest.mark.asyncio
async def test_single_tool_call_executes_under_auto_approve() -> None:
async def test_single_tool_call_executes_under_auto_approve(
telemetry_events: list[dict],
) -> None:
mocked_tool_call_id = "call_1"
tool_call = make_todo_tool_call(mocked_tool_call_id)
backend = FakeBackend([
@ -110,9 +112,19 @@ async def test_single_tool_call_executes_under_auto_approve() -> None:
assert tool_msgs[-1].tool_call_id == mocked_tool_call_id
assert "total_count" in (tool_msgs[-1].content or "")
tool_finished = [
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
]
assert len(tool_finished) == 1
assert tool_finished[0]["properties"]["tool_name"] == "todo"
assert tool_finished[0]["properties"]["status"] == "success"
assert tool_finished[0]["properties"]["approval_type"] == "always"
@pytest.mark.asyncio
async def test_tool_call_requires_approval_if_not_auto_approved() -> None:
async def test_tool_call_requires_approval_if_not_auto_approved(
telemetry_events: list[dict],
) -> None:
agent_loop = make_agent_loop(
auto_approve=False,
todo_permission=ToolPermission.ASK,
@ -145,9 +157,15 @@ async def test_tool_call_requires_approval_if_not_auto_approved() -> None:
assert agent_loop.stats.tool_calls_agreed == 0
assert agent_loop.stats.tool_calls_succeeded == 0
tool_finished = [
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
]
assert len(tool_finished) == 1
assert tool_finished[0]["properties"]["approval_type"] == "ask"
@pytest.mark.asyncio
async def test_tool_call_approved_by_callback() -> None:
async def test_tool_call_approved_by_callback(telemetry_events: list[dict]) -> None:
def approval_callback(
_tool_name: str, _args: BaseModel, _tool_call_id: str
) -> tuple[ApprovalResponse, str | None]:
@ -179,11 +197,17 @@ async def test_tool_call_approved_by_callback() -> None:
assert agent_loop.stats.tool_calls_rejected == 0
assert agent_loop.stats.tool_calls_succeeded == 1
tool_finished = [
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
]
assert len(tool_finished) == 1
assert tool_finished[0]["properties"]["approval_type"] == "ask"
@pytest.mark.asyncio
async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_callback() -> (
None
):
async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_callback(
telemetry_events: list[dict],
) -> None:
custom_feedback = "User declined tool execution"
def approval_callback(
@ -218,9 +242,17 @@ async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_cal
assert agent_loop.stats.tool_calls_agreed == 0
assert agent_loop.stats.tool_calls_succeeded == 0
tool_finished = [
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
]
assert len(tool_finished) == 1
assert tool_finished[0]["properties"]["approval_type"] == "ask"
@pytest.mark.asyncio
async def test_tool_call_skipped_when_permission_is_never() -> None:
async def test_tool_call_skipped_when_permission_is_never(
telemetry_events: list[dict],
) -> None:
agent_loop = make_agent_loop(
auto_approve=False,
todo_permission=ToolPermission.NEVER,
@ -254,6 +286,12 @@ async def test_tool_call_skipped_when_permission_is_never() -> None:
assert agent_loop.stats.tool_calls_agreed == 0
assert agent_loop.stats.tool_calls_succeeded == 0
tool_finished = [
e for e in telemetry_events if e.get("event_name") == "vibe/tool_call_finished"
]
assert len(tool_finished) == 1
assert tool_finished[0]["properties"]["approval_type"] == "never"
@pytest.mark.asyncio
async def test_approval_always_sets_tool_permission_for_subsequent_calls() -> None:

View file

@ -26,7 +26,7 @@ class SpyStreamingFormatter:
def test_run_programmatic_preload_streaming_is_batched(
monkeypatch: pytest.MonkeyPatch,
monkeypatch: pytest.MonkeyPatch, telemetry_events: list[dict]
) -> None:
spy = SpyStreamingFormatter()
monkeypatch.setattr(
@ -77,6 +77,14 @@ def test_run_programmatic_preload_streaming_is_batched(
Role.user,
Role.assistant,
]
new_session = [
e for e in telemetry_events if e.get("event_name") == "vibe/new_session"
]
assert len(new_session) == 1
assert new_session[0]["properties"]["entrypoint"] == "programmatic"
assert "version" in new_session[0]["properties"]
assert (
spy.emitted[0][1] == "You are Vibe, a super useful programming assistant."
)

View file

@ -0,0 +1,47 @@
from __future__ import annotations
from vibe.core.llm.message_utils import merge_consecutive_user_messages
from vibe.core.types import LLMMessage, Role
def test_merge_consecutive_user_messages() -> None:
messages = [
LLMMessage(role=Role.system, content="System"),
LLMMessage(role=Role.user, content="User 1"),
LLMMessage(role=Role.user, content="User 2"),
LLMMessage(role=Role.assistant, content="Assistant"),
]
result = merge_consecutive_user_messages(messages)
assert len(result) == 3
assert result[1].content == "User 1\n\nUser 2"
def test_preserves_non_consecutive_user_messages() -> None:
messages = [
LLMMessage(role=Role.user, content="User 1"),
LLMMessage(role=Role.assistant, content="Assistant"),
LLMMessage(role=Role.user, content="User 2"),
]
result = merge_consecutive_user_messages(messages)
assert len(result) == 3
def test_empty_messages() -> None:
assert merge_consecutive_user_messages([]) == []
def test_single_message() -> None:
messages = [LLMMessage(role=Role.user, content="Only one")]
result = merge_consecutive_user_messages(messages)
assert len(result) == 1
def test_three_consecutive_user_messages() -> None:
messages = [
LLMMessage(role=Role.user, content="A"),
LLMMessage(role=Role.user, content="B"),
LLMMessage(role=Role.user, content="C"),
]
result = merge_consecutive_user_messages(messages)
assert len(result) == 1
assert result[0].content == "A\n\nB\n\nC"

View file

@ -2,14 +2,17 @@ from __future__ import annotations
import pytest
from tests.conftest import build_test_agent_loop, build_test_vibe_config
from vibe.core.agents.models import BUILTIN_AGENTS, AgentProfile, BuiltinAgentName
from vibe.core.config import VibeConfig
from vibe.core.middleware import (
PLAN_AGENT_EXIT,
PLAN_AGENT_REMINDER,
ConversationContext,
MiddlewareAction,
MiddlewarePipeline,
PlanAgentMiddleware,
ResetReason,
)
from vibe.core.types import AgentStats
@ -71,28 +74,58 @@ class TestPlanAgentMiddleware:
assert result.message is None
@pytest.mark.asyncio
async def test_after_turn_always_continues(self, ctx: ConversationContext) -> None:
async def test_injects_reminder_only_once_while_in_plan_mode(
self, ctx: ConversationContext
) -> None:
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
result = await middleware.after_turn(ctx)
result1 = await middleware.before_turn(ctx)
assert result1.action == MiddlewareAction.INJECT_MESSAGE
assert result1.message == PLAN_AGENT_REMINDER
assert result.action == MiddlewareAction.CONTINUE
result2 = await middleware.before_turn(ctx)
assert result2.action == MiddlewareAction.CONTINUE
assert result2.message is None
@pytest.mark.asyncio
async def test_dynamically_checks_agent(self, ctx: ConversationContext) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
async def test_injects_exit_message_when_leaving_plan_mode(
self, ctx: ConversationContext
) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
# Enter plan mode
await middleware.before_turn(ctx)
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
# Leave plan mode
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
current_profile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
@pytest.mark.asyncio
async def test_reinjects_reminder_when_reentering_plan_mode(
self, ctx: ConversationContext
) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
# Enter plan mode - should inject reminder
result1 = await middleware.before_turn(ctx)
assert result1.action == MiddlewareAction.INJECT_MESSAGE
assert result1.message == PLAN_AGENT_REMINDER
# Leave plan mode - should inject exit message
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
result2 = await middleware.before_turn(ctx)
assert result2.action == MiddlewareAction.INJECT_MESSAGE
assert result2.message == PLAN_AGENT_EXIT
# Re-enter plan mode - should inject reminder again
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
result3 = await middleware.before_turn(ctx)
assert result3.action == MiddlewareAction.INJECT_MESSAGE
assert result3.message == PLAN_AGENT_REMINDER
@pytest.mark.asyncio
async def test_custom_reminder(self, ctx: ConversationContext) -> None:
@ -105,10 +138,238 @@ class TestPlanAgentMiddleware:
assert result.message == custom_reminder
def test_reset_does_nothing(self) -> None:
@pytest.mark.asyncio
async def test_reset_clears_state(self, ctx: ConversationContext) -> None:
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
await middleware.before_turn(ctx) # Enter and inject
middleware.reset()
# Should inject again after reset
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
@pytest.mark.asyncio
async def test_exit_message_fires_only_once(self, ctx: ConversationContext) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
# Enter plan mode
await middleware.before_turn(ctx)
# Leave plan mode - first call should inject exit
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
# Subsequent calls in default mode should be CONTINUE
result2 = await middleware.before_turn(ctx)
assert result2.action == MiddlewareAction.CONTINUE
assert result2.message is None
@pytest.mark.asyncio
async def test_multiple_turns_in_plan_mode_after_entry(
self, ctx: ConversationContext
) -> None:
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
# First turn: inject reminder
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
# Several more turns in plan mode: all should be CONTINUE
for _ in range(5):
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
assert result.message is None
@pytest.mark.asyncio
async def test_multiple_turns_in_default_mode_after_exit(
self, ctx: ConversationContext
) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
await middleware.before_turn(ctx) # enter plan
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
await middleware.before_turn(ctx) # exit plan (fires exit message)
# Several more turns in default mode: all should be CONTINUE
for _ in range(5):
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
assert result.message is None
@pytest.mark.asyncio
async def test_rapid_toggling_plan_default_multiple_cycles(
self, ctx: ConversationContext
) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
for _ in range(3):
# Enter plan
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
# Leave plan
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
@pytest.mark.asyncio
async def test_exit_to_non_default_agent(self, ctx: ConversationContext) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
await middleware.before_turn(ctx) # enter plan
# Switch to auto_approve (not default)
current_profile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
@pytest.mark.asyncio
async def test_exit_to_accept_edits_agent(self, ctx: ConversationContext) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
await middleware.before_turn(ctx) # enter plan
# Switch to accept_edits
current_profile = BUILTIN_AGENTS[BuiltinAgentName.ACCEPT_EDITS]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
@pytest.mark.asyncio
async def test_switching_between_non_plan_agents(
self, ctx: ConversationContext
) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
middleware = PlanAgentMiddleware(lambda: current_profile)
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
current_profile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
current_profile = BUILTIN_AGENTS[BuiltinAgentName.ACCEPT_EDITS]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
@pytest.mark.asyncio
async def test_non_plan_to_plan_entry(self, ctx: ConversationContext) -> None:
"""Starting in a non-plan agent then entering plan should inject reminder."""
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.AUTO_APPROVE]
middleware = PlanAgentMiddleware(lambda: current_profile)
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
# Now switch to plan
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
@pytest.mark.asyncio
async def test_reset_while_in_default_after_exiting_plan(
self, ctx: ConversationContext
) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
await middleware.before_turn(ctx) # enter plan
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
await middleware.before_turn(ctx) # exit plan
middleware.reset()
# Still in default mode - should CONTINUE (no phantom exit message)
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
@pytest.mark.asyncio
async def test_reset_while_in_default_then_reenter_plan(
self, ctx: ConversationContext
) -> None:
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
await middleware.before_turn(ctx) # enter plan
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
await middleware.before_turn(ctx) # exit plan
middleware.reset()
# Re-enter plan after reset
current_profile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
@pytest.mark.asyncio
async def test_reset_with_compact_reason(self, ctx: ConversationContext) -> None:
middleware = PlanAgentMiddleware(lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN])
await middleware.before_turn(ctx) # enter and inject
middleware.reset(ResetReason.COMPACT)
# Should reinject reminder after compact reset
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
@pytest.mark.asyncio
async def test_custom_exit_message(self, ctx: ConversationContext) -> None:
custom_exit = "Custom exit message"
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(
lambda: current_profile, exit_message=custom_exit
)
await middleware.before_turn(ctx) # enter plan
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
result = await middleware.before_turn(ctx)
assert result.message == custom_exit
@pytest.mark.asyncio
async def test_plan_entry_then_immediate_exit_same_not_possible(
self, ctx: ConversationContext
) -> None:
"""Even if profile changes between two calls, each call sees one transition."""
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
middleware = PlanAgentMiddleware(lambda: current_profile)
# First call: entry
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
# Second call (still plan): no injection
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
# Third call (switched to default): exit
current_profile = BUILTIN_AGENTS[BuiltinAgentName.DEFAULT]
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
# Fourth call (still default): no injection
result = await middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
class TestMiddlewarePipelineWithPlanAgent:
@pytest.mark.asyncio
@ -135,3 +396,222 @@ class TestMiddlewarePipelineWithPlanAgent:
result = await pipeline.run_before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
class TestPlanAgentMiddlewareIntegration:
@pytest.mark.asyncio
async def test_switch_agent_preserves_middleware_state_for_exit_message(
self,
) -> None:
config = build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
enabled_tools=[],
)
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
plan_middleware = next(
mw
for mw in agent.middleware_pipeline.middlewares
if isinstance(mw, PlanAgentMiddleware)
)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
result = await plan_middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
await agent.switch_agent(BuiltinAgentName.DEFAULT)
plan_middleware_after = next(
mw
for mw in agent.middleware_pipeline.middlewares
if isinstance(mw, PlanAgentMiddleware)
)
assert plan_middleware is plan_middleware_after
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
result = await plan_middleware_after.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
@pytest.mark.asyncio
async def test_switch_agent_allows_reinjection_on_reentry(self) -> None:
config = build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
enabled_tools=[],
)
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
plan_middleware = next(
mw
for mw in agent.middleware_pipeline.middlewares
if isinstance(mw, PlanAgentMiddleware)
)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
await plan_middleware.before_turn(ctx)
await agent.switch_agent(BuiltinAgentName.DEFAULT)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
result = await plan_middleware.before_turn(ctx)
assert result.message == PLAN_AGENT_EXIT
await agent.switch_agent(BuiltinAgentName.PLAN)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
result = await plan_middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
@pytest.mark.asyncio
async def test_switch_plan_to_auto_approve_fires_exit(self) -> None:
config = build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
enabled_tools=[],
)
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
plan_middleware = next(
mw
for mw in agent.middleware_pipeline.middlewares
if isinstance(mw, PlanAgentMiddleware)
)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
await plan_middleware.before_turn(ctx) # enter plan
await agent.switch_agent(BuiltinAgentName.AUTO_APPROVE)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
result = await plan_middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_EXIT
@pytest.mark.asyncio
async def test_switch_between_non_plan_agents_no_injection(self) -> None:
config = build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
enabled_tools=[],
)
agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.DEFAULT
)
plan_middleware = next(
mw
for mw in agent.middleware_pipeline.middlewares
if isinstance(mw, PlanAgentMiddleware)
)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
result = await plan_middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
await agent.switch_agent(BuiltinAgentName.AUTO_APPROVE)
ctx = ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
result = await plan_middleware.before_turn(ctx)
assert result.action == MiddlewareAction.CONTINUE
@pytest.mark.asyncio
async def test_full_lifecycle_plan_default_plan_default(self) -> None:
"""Integration test for a full plan -> default -> plan -> default cycle."""
config = build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,
include_model_info=False,
include_commit_signature=False,
enabled_tools=[],
)
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
plan_middleware = next(
mw
for mw in agent.middleware_pipeline.middlewares
if isinstance(mw, PlanAgentMiddleware)
)
def _ctx():
return ConversationContext(
messages=agent.messages, stats=agent.stats, config=agent.config
)
# 1. Enter plan: inject reminder
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.INJECT_MESSAGE
assert r.message == PLAN_AGENT_REMINDER
# 2. Stay in plan: no injection
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.CONTINUE
# 3. Switch to default: inject exit
await agent.switch_agent(BuiltinAgentName.DEFAULT)
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.INJECT_MESSAGE
assert r.message == PLAN_AGENT_EXIT
# 4. Stay in default: no injection
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.CONTINUE
# 5. Switch back to plan: inject reminder again
await agent.switch_agent(BuiltinAgentName.PLAN)
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.INJECT_MESSAGE
assert r.message == PLAN_AGENT_REMINDER
# 6. Stay in plan: no injection
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.CONTINUE
# 7. Switch to default again: inject exit
await agent.switch_agent(BuiltinAgentName.DEFAULT)
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.INJECT_MESSAGE
assert r.message == PLAN_AGENT_EXIT
# 8. Stay in default: no injection
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.CONTINUE

View file

@ -267,6 +267,7 @@ class TestAPIToolFormatHandlerReasoningContent:
mock_message.role = "assistant"
mock_message.content = "The answer is 42."
mock_message.reasoning_content = "Let me think..."
mock_message.reasoning_signature = None
mock_message.tool_calls = None
result = handler.process_api_response_message(mock_message)

View file

@ -1,6 +1,10 @@
from __future__ import annotations
from unittest.mock import MagicMock
import logging
import os
import threading
import time
from unittest.mock import MagicMock, patch
from pydantic import ValidationError
import pytest
@ -9,7 +13,9 @@ from vibe.core.config import MCPHttp, MCPStdio, MCPStreamableHttp
from vibe.core.tools.mcp import (
MCPToolResult,
RemoteTool,
_mcp_stderr_capture,
_parse_call_result,
_stderr_logger_thread,
create_mcp_http_proxy_tool_class,
create_mcp_stdio_proxy_tool_class,
)
@ -121,6 +127,66 @@ class TestParseCallResult:
assert result.text == "line1\nline2"
class TestMCPStderrCapture:
"""Tests for _mcp_stderr_capture and _stderr_logger_thread."""
@pytest.mark.asyncio
async def test_mcp_stderr_capture_returns_writable_stream(self):
async with _mcp_stderr_capture() as stream:
assert stream is not None
assert callable(getattr(stream, "write", None))
stream.write("test\n")
def test_stderr_logger_thread_logs_decoded_lines(self):
r_fd, w_fd = os.pipe()
try:
vibe_logger = logging.getLogger("vibe")
with patch.object(vibe_logger, "debug") as debug_mock:
thread = threading.Thread(
target=_stderr_logger_thread, args=(r_fd,), daemon=True
)
thread.start()
try:
w = os.fdopen(w_fd, "wb")
w_fd = -1
w.write(b"hello stderr\n")
w.write(b"second line\n")
w.close()
w = None
finally:
time.sleep(0.05)
debug_mock.assert_any_call("[MCP stderr] hello stderr")
debug_mock.assert_any_call("[MCP stderr] second line")
finally:
if w_fd >= 0:
try:
os.close(w_fd)
except OSError:
pass
try:
os.close(r_fd)
except OSError:
pass
@pytest.mark.asyncio
async def test_mcp_stderr_capture_logs_written_data(self):
vibe_logger = logging.getLogger("vibe")
with patch.object(vibe_logger, "debug") as debug_mock:
async with _mcp_stderr_capture() as stream:
stream.write("captured line\n")
time.sleep(0.05)
debug_mock.assert_called_with("[MCP stderr] captured line")
@pytest.mark.asyncio
async def test_mcp_stderr_capture_ignores_empty_lines(self):
vibe_logger = logging.getLogger("vibe")
with patch.object(vibe_logger, "debug") as debug_mock:
async with _mcp_stderr_capture() as stream:
stream.write("\n\n")
time.sleep(0.05)
debug_mock.assert_not_called()
class TestCreateMCPHttpProxyToolClass:
def test_creates_tool_class_with_correct_name(self):
remote = RemoteTool(name="my_tool", description="Test tool")