Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-authored-by: Clement Sirieix <clem.sirieix@gmail.com>
Co-authored-by: Antoine W <antoine.wronka@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-09 19:28:09 +01:00 committed by GitHub
parent 5d2e01a6d7
commit dd372ce494
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 2086 additions and 596 deletions

View file

@ -7,7 +7,7 @@ from unittest.mock import patch
from acp.schema import TextContentBlock, ToolCallProgress, ToolCallStart
import pytest
from tests.conftest import build_test_vibe_config
from tests.conftest import build_test_vibe_config, make_test_models
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
@ -18,8 +18,9 @@ from vibe.core.agent_loop import AgentLoop
def acp_agent_loop(backend: FakeBackend) -> VibeAcpAgentLoop:
class PatchedAgent(AgentLoop):
def __init__(self, *args, **kwargs) -> None:
# Force our config with auto_compact_threshold=1
kwargs["config"] = build_test_vibe_config(auto_compact_threshold=1)
kwargs["config"] = build_test_vibe_config(
models=make_test_models(auto_compact_threshold=1)
)
super().__init__(*args, **kwargs, backend=backend)
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=PatchedAgent).start()

View file

@ -28,7 +28,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.3.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.0"
)
assert response.auth_methods == []
@ -52,7 +52,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.3.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.0"
)
assert response.auth_methods is not None

View file

@ -114,6 +114,10 @@ class TestAcpSearchReplaceExecution:
assert isinstance(result, SearchReplaceResult)
assert result.file == str(test_file)
assert result.blocks_applied == 1
assert (
result.file_content_before
== "original line 1\noriginal line 2\noriginal line 3"
)
assert mock_client._read_text_file_called
assert mock_client._write_text_file_called
assert mock_client._session_update_called
@ -314,6 +318,7 @@ class TestAcpSearchReplaceSessionUpdates:
lines_changed=1,
content=search_replace_content,
warnings=[],
file_content_before="old text",
)
event = ToolResultEvent(

View file

@ -106,6 +106,36 @@ class TestACPSetConfigOptionMode:
assert response is not None
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.PLAN
@pytest.mark.asyncio
async def test_set_config_option_mode_to_chat(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
session_id = session_response.session_id
acp_session = next(
(s for s in acp_agent_loop.sessions.values() if s.id == session_id), None
)
assert acp_session is not None
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.DEFAULT
response = await acp_agent_loop.set_config_option(
session_id=session_id, config_id="mode", value=BuiltinAgentName.CHAT
)
assert response is not None
assert response.config_options is not None
assert len(response.config_options) == 2
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.CHAT
assert (
acp_session.agent_loop.auto_approve is True
) # Chat mode auto-approves read-only tools
mode_config = response.config_options[0]
assert mode_config.root.id == "mode"
assert mode_config.root.current_value == BuiltinAgentName.CHAT
@pytest.mark.asyncio
async def test_set_config_option_mode_invalid_returns_none(
self, acp_agent_loop: VibeAcpAgentLoop

View file

@ -76,8 +76,8 @@ class TestACPSetMode:
assert response is not None
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.PLAN
assert (
acp_session.agent_loop.auto_approve is True
) # Plan mode auto-approves read-only tools
acp_session.agent_loop.auto_approve is False
) # Plan mode uses per-tool allowlists, not global auto-approve
@pytest.mark.asyncio
async def test_set_mode_to_accept_edits(

View file

@ -80,6 +80,7 @@ class TestAcpWriteFileExecution:
assert result.content == "Hello, world!"
assert result.bytes_written == len(b"Hello, world!")
assert result.file_existed is False
assert result.file_content_before is None
assert mock_client._write_text_file_called
assert mock_client._session_update_called
@ -113,6 +114,7 @@ class TestAcpWriteFileExecution:
assert result.content == "New content"
assert result.bytes_written == len(b"New content")
assert result.file_existed is True
assert result.file_content_before == ""
assert mock_client._write_text_file_called
assert mock_client._session_update_called

View file

@ -0,0 +1,254 @@
from __future__ import annotations
import json
import pytest
from vibe.core.config import ProviderConfig
from vibe.core.llm.backend.reasoning_adapter import ReasoningAdapter
from vibe.core.types import (
AvailableFunction,
AvailableTool,
FunctionCall,
LLMMessage,
Role,
ToolCall,
)
@pytest.fixture
def adapter():
return ReasoningAdapter()
@pytest.fixture
def provider():
return ProviderConfig(
name="test-reasoning",
api_base="https://api.example.com/v1",
api_key_env_var="TEST_API_KEY",
api_style="reasoning",
)
def _prepare(adapter, provider, messages, **kwargs):
defaults = dict(
model_name="m",
messages=messages,
temperature=0,
tools=None,
max_tokens=None,
tool_choice=None,
enable_streaming=False,
provider=provider,
)
defaults.update(kwargs)
return json.loads(adapter.prepare_request(**defaults).body)
class TestReasoningEffort:
@pytest.mark.parametrize("level", ["low", "medium", "high"])
def test_sets_reasoning_effort(self, adapter, provider, level):
payload = _prepare(
adapter,
provider,
[LLMMessage(role=Role.user, content="Hi")],
thinking=level,
)
assert payload["reasoning_effort"] == level
def test_omitted_when_off(self, adapter, provider):
payload = _prepare(
adapter,
provider,
[LLMMessage(role=Role.user, content="Hi")],
thinking="off",
)
assert "reasoning_effort" not in payload
class TestThinkingBlocksConversion:
def test_assistant_with_reasoning_to_content_blocks(self, adapter, provider):
messages = [
LLMMessage(role=Role.user, content="Hi"),
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="Let me think...",
),
]
payload = _prepare(adapter, provider, messages)
msg = payload["messages"][1]
assert msg["content"] == [
{
"type": "thinking",
"thinking": [{"type": "text", "text": "Let me think..."}],
},
{"type": "text", "text": "Answer"},
]
def test_assistant_without_reasoning_is_plain_string(self, adapter, provider):
messages = [
LLMMessage(role=Role.user, content="Hi"),
LLMMessage(role=Role.assistant, content="Hello"),
]
payload = _prepare(adapter, provider, messages)
assert payload["messages"][1]["content"] == "Hello"
def test_assistant_with_reasoning_and_tool_calls(self, adapter, provider):
messages = [
LLMMessage(role=Role.user, content="Hi"),
LLMMessage(
role=Role.assistant,
content="Let me search.",
reasoning_content="I should look this up.",
tool_calls=[
ToolCall(
id="tc_1",
index=0,
function=FunctionCall(name="search", arguments='{"q": "test"}'),
)
],
),
]
payload = _prepare(adapter, provider, messages)
msg = payload["messages"][1]
assert msg["content"][0]["type"] == "thinking"
assert msg["content"][1] == {"type": "text", "text": "Let me search."}
assert msg["tool_calls"][0]["id"] == "tc_1"
assert msg["tool_calls"][0]["function"]["name"] == "search"
def test_tools_in_payload(self, adapter, provider):
tools = [
AvailableTool(
function=AvailableFunction(
name="search",
description="Search things",
parameters={"type": "object", "properties": {}},
)
)
]
payload = _prepare(
adapter, provider, [LLMMessage(role=Role.user, content="Hi")], tools=tools
)
assert len(payload["tools"]) == 1
assert payload["tools"][0]["function"]["name"] == "search"
class TestParseThinkingBlocks:
def test_string_content(self, adapter, provider):
data = {
"choices": [{"message": {"role": "assistant", "content": "Hello!"}}],
"usage": {"prompt_tokens": 10, "completion_tokens": 5},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hello!"
assert chunk.message.reasoning_content is None
def test_thinking_and_text_blocks(self, adapter, provider):
data = {
"choices": [
{
"message": {
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": [
{"type": "text", "text": "Let me reason..."}
],
},
{"type": "text", "text": "Final answer"},
],
}
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Final answer"
assert chunk.message.reasoning_content == "Let me reason..."
def test_multiple_thinking_inner_blocks(self, adapter, provider):
data = {
"choices": [
{
"message": {
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": [
{"type": "text", "text": "Step 1. "},
{"type": "text", "text": "Step 2."},
],
},
{"type": "text", "text": "Done"},
],
}
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_content == "Step 1. Step 2."
def test_tool_calls_in_response(self, adapter, provider):
data = {
"choices": [
{
"message": {
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": [
{"type": "text", "text": "need to search"}
],
},
{"type": "text", "text": "Searching..."},
],
"tool_calls": [
{
"id": "tc_1",
"index": 0,
"function": {
"name": "search",
"arguments": '{"q": "test"}',
},
}
],
}
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1},
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_content == "need to search"
assert chunk.message.content == "Searching..."
assert chunk.message.tool_calls[0].function.name == "search"
def test_streaming_text_delta_is_plain_string(self, adapter, provider):
data = {"choices": [{"delta": {"role": "assistant", "content": "Hi"}}]}
chunk = adapter.parse_response(data, provider)
assert chunk.message.content == "Hi"
assert chunk.message.reasoning_content is None
def test_thinking_delta_streaming(self, adapter, provider):
data = {
"choices": [
{
"delta": {
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": [{"type": "text", "text": "hmm"}],
}
],
}
}
]
}
chunk = adapter.parse_response(data, provider)
assert chunk.message.reasoning_content == "hmm"

View file

@ -8,8 +8,7 @@ import pytest
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
from vibe.cli.plan_offer.decide_plan_offer import (
PlanOfferAction,
PlanType,
WhoAmIPlanType,
decide_plan_offer,
resolve_api_key_for_plan,
)
@ -31,105 +30,96 @@ def mistral_api_key_env() -> Generator[str, None, None]:
@pytest.mark.asyncio
async def test_proposes_upgrade_without_call_when_api_key_is_empty() -> None:
async def test_returns_unknown_plan_when_api_key_is_empty() -> None:
gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.API,
plan_name="Free plan",
prompt_switching_to_pro_plan=False,
)
)
action, plan_type = await decide_plan_offer("", gateway)
plan_info = await decide_plan_offer("", gateway)
assert action is PlanOfferAction.UPGRADE
assert plan_type is PlanType.FREE
assert plan_info.plan_type is WhoAmIPlanType.UNKNOWN
assert plan_info.plan_name == ""
assert plan_info.prompt_switching_to_pro_plan is False
assert gateway.calls == []
@pytest.mark.parametrize(
("response", "expected_action", "expected_plan_type"),
("response", "expected_plan_type", "expected_plan_name", "expected_switch_flag"),
[
(
WhoAmIResponse(
is_pro_plan=True,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.API,
plan_name="Free Plan",
prompt_switching_to_pro_plan=False,
),
PlanOfferAction.NONE,
PlanType.PRO,
WhoAmIPlanType.API,
"Free Plan",
False,
),
(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=True,
plan_type=WhoAmIPlanType.CHAT,
plan_name="Pro Plan",
prompt_switching_to_pro_plan=False,
),
PlanOfferAction.UPGRADE,
PlanType.FREE,
WhoAmIPlanType.CHAT,
"Pro Plan",
False,
),
(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.CHAT,
plan_name="Pro Plan",
prompt_switching_to_pro_plan=True,
),
PlanOfferAction.SWITCH_TO_PRO_KEY,
PlanType.PRO,
WhoAmIPlanType.CHAT,
"Pro Plan",
True,
),
],
ids=["with-a-pro-plan", "without-a-pro-plan", "with-a-non-pro-key"],
ids=["api-plan", "chat-plan", "chat-plan-with-prompt"],
)
@pytest.mark.asyncio
async def test_proposes_an_action_based_on_current_plan_status(
async def test_returns_plan_info_and_proposes_an_action_based_on_current_plan_status(
response: WhoAmIResponse,
expected_action: PlanOfferAction,
expected_plan_type: PlanType,
expected_plan_type: WhoAmIPlanType,
expected_plan_name: str,
expected_switch_flag: bool,
) -> None:
gateway = FakeWhoAmIGateway(response)
action, plan_type = await decide_plan_offer("api-key", gateway)
plan_info = await decide_plan_offer("api-key", gateway)
assert action is expected_action
assert plan_type is expected_plan_type
assert plan_info.plan_type is expected_plan_type
assert plan_info.plan_name == expected_plan_name
assert plan_info.prompt_switching_to_pro_plan is expected_switch_flag
assert gateway.calls == ["api-key"]
@pytest.mark.asyncio
async def test_proposes_nothing_when_nothing_is_suggested() -> None:
gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
prompt_switching_to_pro_plan=False,
)
)
action, plan_type = await decide_plan_offer("api-key", gateway)
assert action is PlanOfferAction.NONE
assert plan_type is PlanType.UNKNOWN
assert gateway.calls == ["api-key"]
@pytest.mark.asyncio
async def test_proposes_upgrade_when_api_key_is_unauthorized() -> None:
async def test_returns_unauthorized_plan_when_api_key_is_unauthorized() -> None:
gateway = FakeWhoAmIGateway(unauthorized=True)
action, plan_type = await decide_plan_offer("bad-key", gateway)
plan_info = await decide_plan_offer("bad-key", gateway)
assert action is PlanOfferAction.UPGRADE
assert plan_type is PlanType.FREE
assert plan_info.plan_type is WhoAmIPlanType.UNAUTHORIZED
assert plan_info.plan_name == ""
assert plan_info.prompt_switching_to_pro_plan is False
assert gateway.calls == ["bad-key"]
@pytest.mark.asyncio
async def test_proposes_none_and_logs_warning_when_gateway_error_occurs(
async def test_returns_unknown_plan_and_logs_warning_when_gateway_error_occurs(
caplog: pytest.LogCaptureFixture,
) -> None:
gateway = FakeWhoAmIGateway(error=True)
with caplog.at_level(logging.WARNING):
action, plan_type = await decide_plan_offer("api-key", gateway)
plan_info = await decide_plan_offer("api-key", gateway)
assert action is PlanOfferAction.NONE
assert plan_type is PlanType.UNKNOWN
assert plan_info.plan_type is WhoAmIPlanType.UNKNOWN
assert plan_info.plan_name == ""
assert plan_info.prompt_switching_to_pro_plan is False
assert gateway.calls == ["api-key"]
assert "Failed to fetch plan status." in caplog.text

View file

@ -8,6 +8,7 @@ from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway
from vibe.cli.plan_offer.ports.whoami_gateway import (
WhoAmIGatewayError,
WhoAmIGatewayUnauthorized,
WhoAmIPlanType,
WhoAmIResponse,
)
@ -18,8 +19,8 @@ async def test_returns_plan_flags(respx_mock: respx.MockRouter) -> None:
return_value=httpx.Response(
200,
json={
"is_pro_plan": True,
"advertise_pro_plan": False,
"plan_type": "CHAT",
"plan_name": "INDIVIDUAL",
"prompt_switching_to_pro_plan": False,
},
)
@ -31,8 +32,8 @@ async def test_returns_plan_flags(respx_mock: respx.MockRouter) -> None:
assert route.called
request = route.calls.last.request
assert request.headers["Authorization"] == "Bearer api-key"
assert response.is_pro_plan is True
assert response.advertise_pro_plan is False
assert response.plan_type == "CHAT"
assert response.plan_name == "INDIVIDUAL"
assert response.prompt_switching_to_pro_plan is False
@ -68,16 +69,31 @@ async def test_incomplete_payload_defaults_missing_flags_to_false(
respx_mock: respx.MockRouter,
) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(200, json={"is_pro_plan": True})
return_value=httpx.Response(
200, json={"plan_type": "CHAT", "plan_name": "INDIVIDUAL"}
)
)
gateway = HttpWhoAmIGateway(base_url="http://test")
response = await gateway.whoami("api-key")
assert response == WhoAmIResponse(
is_pro_plan=True, advertise_pro_plan=False, prompt_switching_to_pro_plan=False
plan_type=WhoAmIPlanType.CHAT,
plan_name="INDIVIDUAL",
prompt_switching_to_pro_plan=False,
)
@pytest.mark.asyncio
async def test_raises_on_missing_plan_info(respx_mock: respx.MockRouter) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(200, json={"prompt_switching_to_pro_plan": False})
)
gateway = HttpWhoAmIGateway(base_url="http://test")
with pytest.raises(WhoAmIGatewayError):
await gateway.whoami("api-key")
@pytest.mark.asyncio
async def test_wraps_request_error(respx_mock: respx.MockRouter) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
@ -96,9 +112,9 @@ async def test_parses_boolean_strings(respx_mock: respx.MockRouter) -> None:
return_value=httpx.Response(
200,
json={
"is_pro_plan": "true",
"advertise_pro_plan": "false",
"prompt_switching_to_pro_plan": "true",
"plan_type": "API",
"plan_name": "FREE",
"prompt_switching_to_pro_plan": "false",
},
)
)
@ -106,14 +122,23 @@ async def test_parses_boolean_strings(respx_mock: respx.MockRouter) -> None:
gateway = HttpWhoAmIGateway(base_url="http://test")
response = await gateway.whoami("api-key")
assert response == WhoAmIResponse(
is_pro_plan=True, advertise_pro_plan=False, prompt_switching_to_pro_plan=True
plan_type=WhoAmIPlanType.API,
plan_name="FREE",
prompt_switching_to_pro_plan=False,
)
@pytest.mark.asyncio
async def test_raises_on_invalid_boolean_string(respx_mock: respx.MockRouter) -> None:
respx_mock.get("http://test/api/vibe/whoami").mock(
return_value=httpx.Response(200, json={"is_pro_plan": "yes"})
return_value=httpx.Response(
200,
json={
"plan_type": "CHAT",
"plan_name": "INDIVIDUAL",
"prompt_switching_to_pro_plan": "something",
},
)
)
gateway = HttpWhoAmIGateway(base_url="http://test")

View file

@ -7,7 +7,7 @@ from textual.widgets import Button
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
from tests.conftest import build_test_agent_loop
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
from vibe.cli.textual_ui.app import ChatScroll, VibeApp
from vibe.cli.textual_ui.widgets.load_more import (
HistoryLoadMoreMessage,
@ -32,8 +32,8 @@ def vibe_config() -> VibeConfig:
def _pro_plan_gateway() -> FakeWhoAmIGateway:
return FakeWhoAmIGateway(
response=WhoAmIResponse(
is_pro_plan=True,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.CHAT,
plan_name="INDIVIDUAL",
prompt_switching_to_pro_plan=False,
)
)

View file

@ -16,7 +16,7 @@ from tests.update_notifier.adapters.fake_update_cache_repository import (
FakeUpdateCacheRepository,
)
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
from vibe.cli.textual_ui.widgets.messages import (
AssistantMessage,
UserMessage,
@ -165,8 +165,8 @@ async def test_ui_rebuilds_history_when_whats_new_is_shown(
update_cache_repository = FakeUpdateCacheRepository(update_cache=update_cache)
plan_offer_gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=True,
plan_type=WhoAmIPlanType.API,
plan_name="FREE",
prompt_switching_to_pro_plan=False,
)
)

View file

@ -13,11 +13,16 @@ from tests.update_notifier.adapters.fake_update_cache_repository import (
FakeUpdateCacheRepository,
)
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
from vibe.cli.textual_ui.app import CORE_VERSION, VibeApp
from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.config import SessionLoggingConfig, VibeConfig
from vibe.core.config import (
DEFAULT_MODELS,
ModelConfig,
SessionLoggingConfig,
VibeConfig,
)
from vibe.core.llm.types import BackendLike
from vibe.core.paths import global_paths
from vibe.core.paths.config_paths import unlock_config_paths
@ -125,6 +130,13 @@ def vibe_config() -> VibeConfig:
return build_test_vibe_config()
def make_test_models(auto_compact_threshold: int) -> list[ModelConfig]:
return [
m.model_copy(update={"auto_compact_threshold": auto_compact_threshold})
for m in DEFAULT_MODELS
]
def build_test_vibe_config(**kwargs) -> VibeConfig:
session_logging = kwargs.pop("session_logging", None)
resolved_session_logging = (
@ -136,6 +148,8 @@ def build_test_vibe_config(**kwargs) -> VibeConfig:
resolved_enable_update_checks = (
False if enable_update_checks is None else enable_update_checks
)
if kwargs.get("models"):
kwargs.setdefault("active_model", kwargs["models"][0].alias)
return VibeConfig(
session_logging=resolved_session_logging,
enable_update_checks=resolved_enable_update_checks,
@ -184,8 +198,8 @@ def build_test_vibe_app(
resolved_plan_offer_gateway = (
FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=True,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.CHAT,
plan_name="INDIVIDUAL",
prompt_switching_to_pro_plan=False,
)
)

View file

@ -0,0 +1,42 @@
from __future__ import annotations
from vibe.core.paths.global_paths import PLANS_DIR
from vibe.core.plan_session import PlanSession
class TestPlanSession:
def test_lazy_initialization(self) -> None:
session = PlanSession()
assert session._plan_file_path is None
def test_stable_path(self) -> None:
session = PlanSession()
first = session.plan_file_path
second = session.plan_file_path
assert first == second
def test_md_extension(self) -> None:
session = PlanSession()
assert session.plan_file_path.suffix == ".md"
def test_name_format(self) -> None:
session = PlanSession()
stem = session.plan_file_path.stem
parts = stem.split("-", 1)
assert len(parts) == 2
timestamp_str, slug = parts
assert timestamp_str.isdigit()
assert len(slug.split("-")) == 3
def test_plan_file_path_str_matches(self) -> None:
session = PlanSession()
assert session.plan_file_path_str == str(session.plan_file_path)
def test_path_under_plans_dir(self) -> None:
session = PlanSession()
assert session.plan_file_path.parent == PLANS_DIR.path
def test_different_sessions_get_different_paths(self) -> None:
session1 = PlanSession()
session2 = PlanSession()
assert session1.plan_file_path != session2.plan_file_path

34
tests/core/test_slug.py Normal file
View file

@ -0,0 +1,34 @@
from __future__ import annotations
from vibe.core.slug import _ADJECTIVES, _NOUNS, create_slug
class TestCreateSlug:
def test_format_is_adj_adj_noun(self) -> None:
slug = create_slug()
parts = slug.split("-")
assert len(parts) == 3
def test_parts_from_word_pools(self) -> None:
slug = create_slug()
adj1, adj2, noun = slug.split("-")
assert adj1 in _ADJECTIVES
assert adj2 in _ADJECTIVES
assert noun in _NOUNS
def test_adjectives_are_distinct(self) -> None:
for _ in range(20):
adj1, adj2, _ = create_slug().split("-")
assert adj1 != adj2
def test_randomness_produces_variety(self) -> None:
slugs = {create_slug() for _ in range(20)}
assert len(slugs) > 1
def test_word_pools_non_empty(self) -> None:
assert len(_ADJECTIVES) > 0
assert len(_NOUNS) > 0
def test_word_pools_have_no_duplicates(self) -> None:
assert len(_ADJECTIVES) == len(set(_ADJECTIVES))
assert len(_NOUNS) == len(set(_NOUNS))

View file

@ -37,6 +37,7 @@ def create_test_session():
session_id: str,
messages: list[LLMMessage] | None = None,
metadata: dict | None = None,
encoding: str = "utf-8",
) -> Path:
"""Create a test session directory with messages and metadata files."""
# Create session directory
@ -53,7 +54,7 @@ def create_test_session():
LLMMessage(role=Role.assistant, content="Hi there!"),
]
with messages_file.open("w", encoding="utf-8") as f:
with messages_file.open("w", encoding=encoding) as f:
for message in messages:
f.write(
json.dumps(
@ -76,9 +77,13 @@ def create_test_session():
"session_completion_tokens": 20,
},
"system_prompt": {"content": "System prompt", "role": "system"},
"username": "testuser",
"environment": {"working_directory": "/test"},
"git_commit": None,
"git_branch": None,
}
with metadata_file.open("w", encoding="utf-8") as f:
with metadata_file.open("w", encoding=encoding) as f:
json.dump(metadata, f, indent=2)
return session_folder
@ -952,3 +957,109 @@ class TestSessionLoaderGetFirstUserMessage:
# Should return "User question", not "Assistant response"
assert result == "User question"
class TestSessionLoaderUTF8Encoding:
def test_load_metadata_with_utf8_encoding(
self, session_config: SessionLoggingConfig, create_test_session
) -> None:
session_dir = Path(session_config.save_dir)
session_folder = create_test_session(session_dir, "utf8-test")
metadata = SessionLoader.load_metadata(session_folder)
assert metadata.session_id == "utf8-test"
assert metadata.start_time == "2023-01-01T12:00:00Z"
assert metadata.username is not None
def test_load_metadata_with_unicode_characters(
self, session_config: SessionLoggingConfig
) -> None:
session_dir = Path(session_config.save_dir)
session_folder = session_dir / "test_20230101_120000_unicode0"
session_folder.mkdir()
metadata_content = {
"session_id": "unicode-test-123",
"start_time": "2023-01-01T12:00:00Z",
"end_time": "2023-01-01T12:05:00Z",
"environment": {"working_directory": "/home/user/café_project"},
"username": "testuser",
"git_commit": None,
"git_branch": None,
}
metadata_file = session_folder / "meta.json"
with metadata_file.open("w", encoding="utf-8") as f:
json.dump(metadata_content, f, indent=2, ensure_ascii=False)
messages_file = session_folder / "messages.jsonl"
messages_file.write_text('{"role": "user", "content": "Hello"}\n')
metadata = SessionLoader.load_metadata(session_folder)
assert metadata.session_id == "unicode-test-123"
assert metadata.environment["working_directory"] == "/home/user/café_project"
def test_load_metadata_with_different_encoding_handled(
self, session_config: SessionLoggingConfig
) -> None:
session_dir = Path(session_config.save_dir)
session_folder = session_dir / "test_20230101_120000_latin100"
session_folder.mkdir()
metadata_content = {
"session_id": "latin1-test",
"start_time": "2023-01-01T12:00:00Z",
"end_time": "2023-01-01T12:05:00Z",
"username": "testuser",
"environment": {"working_directory": "/home/user/café_project"},
"git_commit": None,
"git_branch": None,
}
metadata_file = session_folder / "meta.json"
with metadata_file.open("w", encoding="latin-1") as f:
json.dump(metadata_content, f, indent=2, ensure_ascii=False)
messages_file = session_folder / "messages.jsonl"
messages_file.write_text('{"role": "user", "content": "Hello"}\n')
metadata = SessionLoader.load_metadata(session_folder)
assert metadata.session_id == "latin1-test"
assert metadata.environment["working_directory"] == "/home/user/caf_project"
def test_load_session_with_utf8_metadata_and_messages(
self, session_config: SessionLoggingConfig
) -> None:
session_dir = Path(session_config.save_dir)
session_folder = session_dir / "test_20230101_120000_utf8all0"
session_folder.mkdir()
metadata_content = {
"session_id": "utf8-all-test",
"start_time": "2023-01-01T12:00:00Z",
"end_time": "2023-01-01T12:05:00Z",
"username": "testuser",
"environment": {},
"git_commit": None,
"git_branch": None,
}
metadata_file = session_folder / "meta.json"
with metadata_file.open("w", encoding="utf-8") as f:
json.dump(metadata_content, f, indent=2, ensure_ascii=False)
messages_file = session_folder / "messages.jsonl"
messages_file.write_text(
'{"role": "user", "content": "Hello café"}\n'
+ '{"role": "assistant", "content": "Hi there naïve"}\n',
encoding="utf-8",
)
messages, metadata = SessionLoader.load_session(session_folder)
assert metadata["session_id"] == "utf8-all-test"
assert len(messages) == 2
assert messages[0].content == "Hello café"
assert messages[1].content == "Hi there naïve"

View file

@ -119,7 +119,7 @@
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="0" y="215.2" textLength="134.2" clip-path="url(#terminal-line-8)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="215.2" textLength="146.4" clip-path="url(#terminal-line-8)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="215.2" textLength="122" clip-path="url(#terminal-line-8)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="215.2" textLength="183" clip-path="url(#terminal-line-8)">devstral-latest</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="0" y="215.2" textLength="134.2" clip-path="url(#terminal-line-8)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="215.2" textLength="146.4" clip-path="url(#terminal-line-8)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="215.2" textLength="122" clip-path="url(#terminal-line-8)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="215.2" textLength="183" clip-path="url(#terminal-line-8)">devstral-latest</text><text class="terminal-r1" x="622.2" y="215.2" textLength="256.2" clip-path="url(#terminal-line-8)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="0" y="239.6" textLength="134.2" clip-path="url(#terminal-line-9)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="239.6" textLength="414.8" clip-path="url(#terminal-line-9)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="0" y="264" textLength="134.2" clip-path="url(#terminal-line-10)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="264" textLength="61" clip-path="url(#terminal-line-10)">Type&#160;</text><text class="terminal-r3" x="231.8" y="264" textLength="61" clip-path="url(#terminal-line-10)">/help</text><text class="terminal-r1" x="292.8" y="264" textLength="256.2" clip-path="url(#terminal-line-10)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

View file

@ -153,7 +153,7 @@
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="0" y="312.8" textLength="134.2" clip-path="url(#terminal-line-12)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="312.8" textLength="183" clip-path="url(#terminal-line-12)">devstral-latest</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="312.8" textLength="134.2" clip-path="url(#terminal-line-12)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="312.8" textLength="183" clip-path="url(#terminal-line-12)">devstral-latest</text><text class="terminal-r1" x="622.2" y="312.8" textLength="256.2" clip-path="url(#terminal-line-12)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="414.8" clip-path="url(#terminal-line-13)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type&#160;</text><text class="terminal-r3" x="231.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">/help</text><text class="terminal-r1" x="292.8" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -180,7 +180,7 @@
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="508" textLength="183" clip-path="url(#terminal-line-20)">devstral-latest</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="508" textLength="183" clip-path="url(#terminal-line-20)">devstral-latest</text><text class="terminal-r1" x="622.2" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="532.4" textLength="414.8" clip-path="url(#terminal-line-21)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" 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;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">Type&#160;</text><text class="terminal-r3" x="231.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">/help</text><text class="terminal-r1" x="292.8" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -180,7 +180,7 @@
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="386" textLength="146.4" clip-path="url(#terminal-line-15)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="386" textLength="122" clip-path="url(#terminal-line-15)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="386" textLength="183" clip-path="url(#terminal-line-15)">devstral-latest</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="386" textLength="146.4" clip-path="url(#terminal-line-15)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="386" textLength="122" clip-path="url(#terminal-line-15)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="386" textLength="183" clip-path="url(#terminal-line-15)">devstral-latest</text><text class="terminal-r1" x="622.2" y="386" textLength="256.2" clip-path="url(#terminal-line-15)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="410.4" textLength="414.8" clip-path="url(#terminal-line-16)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">Type&#160;</text><text class="terminal-r3" x="231.8" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">/help</text><text class="terminal-r1" x="292.8" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -180,7 +180,7 @@
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type&#160;</text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -184,7 +184,7 @@
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" 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;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -184,7 +184,7 @@
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" 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;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -184,7 +184,7 @@
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" 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;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -183,7 +183,7 @@
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" 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;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -183,7 +183,7 @@
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" 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;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -181,7 +181,7 @@
</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="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="622.2" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">&#160;·&#160;[Subscription]&#160;Pro</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)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -181,7 +181,7 @@
</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="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="622.2" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">&#160;·&#160;[Subscription]&#160;Pro</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)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -174,7 +174,7 @@
</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="0" y="312.8" textLength="134.2" clip-path="url(#terminal-line-12)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="312.8" textLength="183" clip-path="url(#terminal-line-12)">devstral-latest</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="0" y="312.8" textLength="134.2" clip-path="url(#terminal-line-12)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="312.8" textLength="183" clip-path="url(#terminal-line-12)">devstral-latest</text><text class="terminal-r1" x="622.2" y="312.8" textLength="256.2" clip-path="url(#terminal-line-12)">&#160;·&#160;[Subscription]&#160;Pro</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="0" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="414.8" clip-path="url(#terminal-line-13)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</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="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type&#160;</text><text class="terminal-r3" x="231.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">/help</text><text class="terminal-r1" x="292.8" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)">&#160;for&#160;more&#160;information</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)">

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

@ -173,7 +173,7 @@
</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="0" y="312.8" textLength="134.2" clip-path="url(#terminal-line-12)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="312.8" textLength="183" clip-path="url(#terminal-line-12)">devstral-latest</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="0" y="312.8" textLength="134.2" clip-path="url(#terminal-line-12)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="312.8" textLength="183" clip-path="url(#terminal-line-12)">devstral-latest</text><text class="terminal-r1" x="622.2" y="312.8" textLength="256.2" clip-path="url(#terminal-line-12)">&#160;·&#160;[Subscription]&#160;Pro</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="0" y="337.2" textLength="134.2" clip-path="url(#terminal-line-13)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="414.8" clip-path="url(#terminal-line-13)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</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="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type&#160;</text><text class="terminal-r3" x="231.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">/help</text><text class="terminal-r1" x="292.8" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)">&#160;for&#160;more&#160;information</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)">

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

@ -182,7 +182,7 @@
</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="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="622.2" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">&#160;·&#160;[Subscription]&#160;Pro</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)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -181,7 +181,7 @@
</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="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="622.2" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">&#160;·&#160;[Subscription]&#160;Pro</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)">

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

@ -180,7 +180,7 @@
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type&#160;</text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -176,7 +176,7 @@
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="361.6" textLength="183" clip-path="url(#terminal-line-14)">devstral-latest</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="361.6" textLength="183" clip-path="url(#terminal-line-14)">devstral-latest</text><text class="terminal-r1" x="622.2" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="386" textLength="414.8" clip-path="url(#terminal-line-15)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type&#160;</text><text class="terminal-r3" x="231.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="292.8" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -180,7 +180,7 @@
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type&#160;</text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -179,7 +179,7 @@
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="622.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type&#160;</text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -183,7 +183,7 @@
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="508" textLength="183" clip-path="url(#terminal-line-20)">devstral-latest</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="508" textLength="183" clip-path="url(#terminal-line-20)">devstral-latest</text><text class="terminal-r1" x="622.2" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="532.4" textLength="414.8" clip-path="url(#terminal-line-21)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" 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;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">Type&#160;</text><text class="terminal-r3" x="231.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">/help</text><text class="terminal-r1" x="292.8" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -179,7 +179,7 @@
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type&#160;</text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -180,7 +180,7 @@
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">devstral-latest</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">devstral-latest</text><text class="terminal-r1" x="622.2" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type&#160;</text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -178,7 +178,7 @@
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="410.4" textLength="183" clip-path="url(#terminal-line-16)">devstral-latest</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="410.4" textLength="183" clip-path="url(#terminal-line-16)">devstral-latest</text><text class="terminal-r1" x="622.2" y="410.4" textLength="292.8" clip-path="url(#terminal-line-16)">&#160;·&#160;[API]&#160;Experiment&#160;plan</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type&#160;</text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -178,7 +178,7 @@
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="410.4" textLength="183" clip-path="url(#terminal-line-16)">devstral-latest</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="410.4" textLength="183" clip-path="url(#terminal-line-16)">devstral-latest</text><text class="terminal-r1" x="622.2" y="410.4" textLength="292.8" clip-path="url(#terminal-line-16)">&#160;·&#160;[API]&#160;Experiment&#160;plan</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type&#160;</text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

@ -6,7 +6,7 @@ from textual.widgets.text_area import TextAreaTheme
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
from tests.conftest import build_test_agent_loop, build_test_vibe_config
from tests.stubs.fake_backend import FakeBackend
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
from vibe.cli.textual_ui.app import VibeApp
from vibe.cli.textual_ui.widgets.chat_input import ChatTextArea
from vibe.core.agents.models import BuiltinAgentName
@ -47,8 +47,8 @@ class BaseSnapshotTestApp(VibeApp):
"plan_offer_gateway",
FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=True,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.CHAT,
plan_name="INDIVIDUAL",
prompt_switching_to_pro_plan=False,
)
),

View file

@ -14,7 +14,7 @@ from tests.update_notifier.adapters.fake_update_cache_repository import (
FakeUpdateCacheRepository,
)
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
from vibe.cli.update_notifier import UpdateCache
@ -52,8 +52,8 @@ class SnapshotTestAppWithPlanUpgradeCTA(SnapshotTestAppWithWhatsNew):
def __init__(self):
plan_offer_gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=True,
plan_type=WhoAmIPlanType.API,
plan_name="FREE",
prompt_switching_to_pro_plan=False,
)
)
@ -65,8 +65,8 @@ class SnapshotTestAppWithSwitchKeyCTA(SnapshotTestAppWithWhatsNew):
def __init__(self):
plan_offer_gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=False,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.API,
plan_name="FREE",
prompt_switching_to_pro_plan=True,
)
)
@ -78,8 +78,8 @@ class SnapshotTestAppWithWhatsNewNoPlanCTA(SnapshotTestAppWithWhatsNew):
def __init__(self):
plan_offer_gateway = FakeWhoAmIGateway(
WhoAmIResponse(
is_pro_plan=True,
advertise_pro_plan=False,
plan_type=WhoAmIPlanType.CHAT,
plan_name="INDIVIDUAL",
prompt_switching_to_pro_plan=False,
)
)

View file

@ -2,7 +2,11 @@ from __future__ import annotations
import pytest
from tests.conftest import build_test_agent_loop, build_test_vibe_config
from tests.conftest import (
build_test_agent_loop,
build_test_vibe_config,
make_test_models,
)
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.core.types import (
@ -21,7 +25,7 @@ async def test_auto_compact_emits_correct_events(telemetry_events: list[dict]) -
[mock_llm_chunk(content="<summary>")],
[mock_llm_chunk(content="<final>")],
])
cfg = build_test_vibe_config(auto_compact_threshold=1)
cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=1))
agent = build_test_agent_loop(config=cfg, backend=backend)
agent.stats.context_tokens = 2
@ -65,7 +69,7 @@ async def test_auto_compact_observer_sees_user_msg_not_summary() -> None:
[mock_llm_chunk(content="<summary>")],
[mock_llm_chunk(content="<final>")],
])
cfg = build_test_vibe_config(auto_compact_threshold=1)
cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=1))
agent = build_test_agent_loop(
config=cfg, message_observer=observer, backend=backend
)
@ -91,7 +95,7 @@ async def test_auto_compact_observer_does_not_see_summary_request() -> None:
[mock_llm_chunk(content="<summary>")],
[mock_llm_chunk(content="<final>")],
])
cfg = build_test_vibe_config(auto_compact_threshold=1)
cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=1))
agent = build_test_agent_loop(
config=cfg, message_observer=observer, backend=backend
)
@ -111,7 +115,7 @@ async def test_compact_replaces_messages_with_summary() -> None:
[mock_llm_chunk(content="<summary>")],
[mock_llm_chunk(content="<final>")],
])
cfg = build_test_vibe_config(auto_compact_threshold=1)
cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=1))
agent = build_test_agent_loop(config=cfg, backend=backend)
agent.stats.context_tokens = 2

View file

@ -58,7 +58,6 @@ def make_config(
tools: dict[str, BaseToolConfig] | None = None,
) -> VibeConfig:
return build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,

View file

@ -4,7 +4,11 @@ from collections.abc import Callable
import pytest
from tests.conftest import build_test_agent_loop, build_test_vibe_config
from tests.conftest import (
build_test_agent_loop,
build_test_vibe_config,
make_test_models,
)
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.core.agents.models import BuiltinAgentName
@ -49,6 +53,7 @@ def make_config(
alias="devstral-latest",
input_price=input_price,
output_price=output_price,
auto_compact_threshold=auto_compact_threshold,
),
ModelConfig(
name="devstral-small-latest",
@ -56,6 +61,7 @@ def make_config(
alias="devstral-small",
input_price=0.1,
output_price=0.3,
auto_compact_threshold=auto_compact_threshold,
),
ModelConfig(
name="strawberry",
@ -63,6 +69,7 @@ def make_config(
alias="strawberry",
input_price=2.5,
output_price=10.0,
auto_compact_threshold=auto_compact_threshold,
),
]
providers = [
@ -81,7 +88,6 @@ def make_config(
]
return build_test_vibe_config(
session_logging=SessionLoggingConfig(enabled=not disable_logging),
auto_compact_threshold=auto_compact_threshold,
system_prompt_id=system_prompt_id,
include_project_context=include_project_context,
include_prompt_detail=include_prompt_detail,
@ -505,7 +511,7 @@ class TestAutoCompactIntegration:
[mock_llm_chunk(content="<summary>")],
[mock_llm_chunk(content="<final>")],
])
cfg = build_test_vibe_config(auto_compact_threshold=1)
cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=1))
agent = build_test_agent_loop(
config=cfg, message_observer=observer, backend=backend
)

View file

@ -36,7 +36,6 @@ async def act_and_collect_events(agent_loop: AgentLoop, prompt: str) -> list[Bas
def make_config(todo_permission: ToolPermission = ToolPermission.ALWAYS) -> VibeConfig:
return build_test_vibe_config(
auto_compact_threshold=0,
enabled_tools=["todo"],
tools={"todo": BaseToolConfig(permission=todo_permission)},
system_prompt_id="tests",
@ -448,9 +447,7 @@ async def test_tool_call_can_be_interrupted() -> None:
tool_call = ToolCall(
id="call_8", index=0, function=FunctionCall(name="stub_tool", arguments="{}")
)
config = build_test_vibe_config(
auto_compact_threshold=0, enabled_tools=["stub_tool"]
)
config = build_test_vibe_config(enabled_tools=["stub_tool"])
agent_loop = build_test_agent_loop(
config=config,
agent_name=BuiltinAgentName.AUTO_APPROVE,

View file

@ -5,12 +5,10 @@ from pathlib import Path
import pytest
from tests.conftest import build_test_agent_loop, build_test_vibe_config
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.core.agents.manager import AgentManager
from vibe.core.agents.models import (
BUILTIN_AGENTS,
PLAN_AGENT_TOOLS,
AgentProfile,
AgentSafety,
AgentType,
@ -21,15 +19,7 @@ from vibe.core.config import VibeConfig
from vibe.core.paths.config_paths import ConfigPath
from vibe.core.paths.global_paths import GlobalPath
from vibe.core.tools.base import ToolPermission
from vibe.core.types import (
FunctionCall,
LLMChunk,
LLMMessage,
LLMUsage,
Role,
ToolCall,
ToolResultEvent,
)
from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
class TestDeepMerge:
@ -216,8 +206,14 @@ class TestAgentProfileOverrides:
def test_plan_agent_restricts_tools(self) -> None:
overrides = BUILTIN_AGENTS[BuiltinAgentName.PLAN].overrides
assert "enabled_tools" in overrides
assert overrides["enabled_tools"] == PLAN_AGENT_TOOLS
assert "tools" in overrides
tools = overrides["tools"]
assert "write_file" in tools
assert "search_replace" in tools
assert tools["write_file"]["permission"] == "never"
assert tools["search_replace"]["permission"] == "never"
assert len(tools["write_file"]["allowlist"]) > 0
assert len(tools["search_replace"]["allowlist"]) > 0
def test_accept_edits_agent_sets_tool_permissions(self) -> None:
overrides = BUILTIN_AGENTS[BuiltinAgentName.ACCEPT_EDITS].overrides
@ -233,9 +229,7 @@ class TestAgentManagerCycling:
@pytest.fixture
def base_config(self) -> VibeConfig:
return build_test_vibe_config(
auto_compact_threshold=0,
include_project_context=False,
include_prompt_detail=False,
include_project_context=False, include_prompt_detail=False
)
@pytest.fixture
@ -302,9 +296,7 @@ class TestAgentSwitchAgent:
@pytest.fixture
def base_config(self) -> VibeConfig:
return build_test_vibe_config(
auto_compact_threshold=0,
include_project_context=False,
include_prompt_detail=False,
include_project_context=False, include_prompt_detail=False
)
@pytest.fixture
@ -317,21 +309,26 @@ class TestAgentSwitchAgent:
])
@pytest.mark.asyncio
async def test_switch_to_plan_agent_restricts_tools(
async def test_switch_to_plan_agent_has_tools_with_restricted_permissions(
self, base_config: VibeConfig, backend: FakeBackend
) -> None:
agent = build_test_agent_loop(
config=base_config, agent_name=BuiltinAgentName.DEFAULT, backend=backend
)
initial_tool_names = set(agent.tool_manager.available_tools.keys())
assert len(initial_tool_names) > len(PLAN_AGENT_TOOLS)
await agent.switch_agent(BuiltinAgentName.PLAN)
plan_tool_names = set(agent.tool_manager.available_tools.keys())
assert plan_tool_names == set(PLAN_AGENT_TOOLS)
# Plan mode now has all tools available but with restricted permissions
assert "write_file" in plan_tool_names
assert "search_replace" in plan_tool_names
assert "grep" in plan_tool_names
assert "read_file" in plan_tool_names
assert agent.agent_profile.name == BuiltinAgentName.PLAN
# Verify write tools have "never" base permission
write_config = agent.tool_manager.get_tool_config("write_file")
assert write_config.permission == ToolPermission.NEVER
@pytest.mark.asyncio
async def test_switch_from_plan_to_default_restores_tools(
self, base_config: VibeConfig, backend: FakeBackend
@ -339,11 +336,12 @@ class TestAgentSwitchAgent:
agent = build_test_agent_loop(
config=base_config, agent_name=BuiltinAgentName.PLAN, backend=backend
)
assert len(agent.tool_manager.available_tools) == len(PLAN_AGENT_TOOLS)
await agent.switch_agent(BuiltinAgentName.DEFAULT)
assert len(agent.tool_manager.available_tools) > len(PLAN_AGENT_TOOLS)
# Write tools should revert to default ASK permission
write_config = agent.tool_manager.get_tool_config("write_file")
assert write_config.permission == ToolPermission.ASK
assert agent.agent_profile.name == BuiltinAgentName.DEFAULT
@pytest.mark.asyncio
@ -392,9 +390,7 @@ class TestAcceptEditsAgent:
async def test_accept_edits_agent_auto_approves_write_file(self) -> None:
backend = FakeBackend([])
config = build_test_vibe_config(
auto_compact_threshold=0, enabled_tools=["write_file"]
)
config = build_test_vibe_config(enabled_tools=["write_file"])
agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.ACCEPT_EDITS, backend=backend
)
@ -406,9 +402,7 @@ class TestAcceptEditsAgent:
async def test_accept_edits_agent_requires_approval_for_other_tools(self) -> None:
backend = FakeBackend([])
config = build_test_vibe_config(
auto_compact_threshold=0, enabled_tools=["bash"]
)
config = build_test_vibe_config(enabled_tools=["bash"])
agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.ACCEPT_EDITS, backend=backend
)
@ -419,52 +413,36 @@ class TestAcceptEditsAgent:
class TestPlanAgentToolRestriction:
@pytest.mark.asyncio
async def test_plan_agent_only_exposes_read_tools_to_llm(self) -> None:
async def test_plan_agent_has_all_tools_with_restricted_write_permissions(
self,
) -> None:
backend = FakeBackend([
LLMChunk(
message=LLMMessage(role=Role.assistant, content="ok"),
usage=LLMUsage(prompt_tokens=10, completion_tokens=5),
)
])
config = build_test_vibe_config(auto_compact_threshold=0)
config = build_test_vibe_config()
agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.PLAN, backend=backend
)
tool_names = set(agent.tool_manager.available_tools.keys())
assert "bash" not in tool_names
assert "write_file" not in tool_names
assert "search_replace" not in tool_names
for plan_tool in PLAN_AGENT_TOOLS:
assert plan_tool in tool_names
# Plan mode now has all tools available
assert "grep" in tool_names
assert "read_file" in tool_names
assert "write_file" in tool_names
assert "search_replace" in tool_names
@pytest.mark.asyncio
async def test_plan_agent_rejects_non_plan_tool_call(self) -> None:
tool_call = ToolCall(
id="call_1",
index=0,
function=FunctionCall(name="bash", arguments='{"command": "ls"}'),
)
backend = FakeBackend([
mock_llm_chunk(content="Let me run bash", tool_calls=[tool_call]),
mock_llm_chunk(content="Tool not available"),
])
# But write tools have restricted permissions
write_config = agent.tool_manager.get_tool_config("write_file")
assert write_config.permission == ToolPermission.NEVER
assert len(write_config.allowlist) > 0
config = build_test_vibe_config(auto_compact_threshold=0)
agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.PLAN, backend=backend
)
events = [ev async for ev in agent.act("Run ls")]
tool_result = next((e for e in events if isinstance(e, ToolResultEvent)), None)
assert tool_result is not None
assert tool_result.error is not None
assert (
"not found" in tool_result.error.lower()
or "error" in tool_result.error.lower()
)
sr_config = agent.tool_manager.get_tool_config("search_replace")
assert sr_config.permission == ToolPermission.NEVER
assert len(sr_config.allowlist) > 0
class TestAgentManagerFiltering:

View file

@ -9,12 +9,12 @@ from vibe.core.middleware import (
CHAT_AGENT_EXIT,
CHAT_AGENT_REMINDER,
PLAN_AGENT_EXIT,
PLAN_AGENT_REMINDER,
ConversationContext,
MiddlewareAction,
MiddlewarePipeline,
ReadOnlyAgentMiddleware,
ResetReason,
make_plan_agent_reminder,
)
from vibe.core.types import AgentStats, MessageList
@ -326,15 +326,19 @@ class TestReadOnlyAgentMiddleware:
assert result.action == MiddlewareAction.CONTINUE
PLAN_REMINDER_SNIPPET = "Plan mode is active"
class TestMiddlewarePipelineWithReadOnlyAgent:
@pytest.mark.asyncio
async def test_pipeline_includes_injection(self, ctx: ConversationContext) -> None:
plan_reminder = make_plan_agent_reminder("/tmp/test-plan.md")
pipeline = MiddlewarePipeline()
pipeline.add(
ReadOnlyAgentMiddleware(
lambda: BUILTIN_AGENTS[BuiltinAgentName.PLAN],
BuiltinAgentName.PLAN,
PLAN_AGENT_REMINDER,
plan_reminder,
PLAN_AGENT_EXIT,
)
)
@ -342,18 +346,19 @@ class TestMiddlewarePipelineWithReadOnlyAgent:
result = await pipeline.run_before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert PLAN_AGENT_REMINDER in (result.message or "")
assert PLAN_REMINDER_SNIPPET in (result.message or "")
@pytest.mark.asyncio
async def test_pipeline_skips_injection_when_not_target_agent(
self, ctx: ConversationContext
) -> None:
plan_reminder = make_plan_agent_reminder("/tmp/test-plan.md")
pipeline = MiddlewarePipeline()
pipeline.add(
ReadOnlyAgentMiddleware(
lambda: BUILTIN_AGENTS[BuiltinAgentName.DEFAULT],
BuiltinAgentName.PLAN,
PLAN_AGENT_REMINDER,
plan_reminder,
PLAN_AGENT_EXIT,
)
)
@ -366,13 +371,14 @@ class TestMiddlewarePipelineWithReadOnlyAgent:
async def test_direct_plan_to_chat_transition_delivers_both_messages(
self, ctx: ConversationContext
) -> None:
plan_reminder = make_plan_agent_reminder("/tmp/test-plan.md")
current_profile: AgentProfile = BUILTIN_AGENTS[BuiltinAgentName.PLAN]
pipeline = MiddlewarePipeline()
pipeline.add(
ReadOnlyAgentMiddleware(
lambda: current_profile,
BuiltinAgentName.PLAN,
PLAN_AGENT_REMINDER,
plan_reminder,
PLAN_AGENT_EXIT,
)
)
@ -387,7 +393,7 @@ class TestMiddlewarePipelineWithReadOnlyAgent:
result = await pipeline.run_before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert PLAN_AGENT_REMINDER in (result.message or "")
assert PLAN_REMINDER_SNIPPET in (result.message or "")
current_profile = CHAT
result = await pipeline.run_before_turn(ctx)
@ -399,7 +405,7 @@ class TestMiddlewarePipelineWithReadOnlyAgent:
result = await pipeline.run_before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert CHAT_AGENT_EXIT in (result.message or "")
assert PLAN_AGENT_REMINDER in (result.message or "")
assert PLAN_REMINDER_SNIPPET in (result.message or "")
def _find_plan_middleware(agent) -> ReadOnlyAgentMiddleware:
@ -417,7 +423,6 @@ class TestReadOnlyAgentMiddlewareIntegration:
self,
) -> None:
config = build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,
@ -434,7 +439,7 @@ class TestReadOnlyAgentMiddlewareIntegration:
)
result = await plan_middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
assert PLAN_REMINDER_SNIPPET in (result.message or "")
await agent.switch_agent(BuiltinAgentName.DEFAULT)
@ -451,7 +456,6 @@ class TestReadOnlyAgentMiddlewareIntegration:
@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,
@ -483,12 +487,11 @@ class TestReadOnlyAgentMiddlewareIntegration:
)
result = await plan_middleware.before_turn(ctx)
assert result.action == MiddlewareAction.INJECT_MESSAGE
assert result.message == PLAN_AGENT_REMINDER
assert PLAN_REMINDER_SNIPPET in (result.message or "")
@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,
@ -517,7 +520,6 @@ class TestReadOnlyAgentMiddlewareIntegration:
@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,
@ -549,7 +551,6 @@ class TestReadOnlyAgentMiddlewareIntegration:
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,
@ -569,7 +570,7 @@ class TestReadOnlyAgentMiddlewareIntegration:
# 1. Enter plan: inject reminder
r = await plan_middleware.before_turn(_ctx())
assert r.action == MiddlewareAction.INJECT_MESSAGE
assert r.message == PLAN_AGENT_REMINDER
assert PLAN_REMINDER_SNIPPET in (r.message or "")
# 2. Stay in plan: no injection
r = await plan_middleware.before_turn(_ctx())
@ -589,7 +590,7 @@ class TestReadOnlyAgentMiddlewareIntegration:
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
assert PLAN_REMINDER_SNIPPET in (r.message or "")
# 6. Stay in plan: no injection
r = await plan_middleware.before_turn(_ctx())

View file

@ -19,7 +19,6 @@ from vibe.core.types import AssistantEvent, LLMMessage, ReasoningEvent, Role
def make_config() -> VibeConfig:
return build_test_vibe_config(
auto_compact_threshold=0,
system_prompt_id="tests",
include_project_context=False,
include_prompt_detail=False,

View file

@ -117,3 +117,169 @@ async def test_ui_resumes_arrow_down_after_manual_move(
await pilot.press("down")
assert textarea.cursor_location[0] == 1
assert chat_input.value == "first line\nsecond line"
@pytest.mark.asyncio
async def test_ui_does_not_intercept_arrow_down_inside_wrapped_single_line_input(
vibe_app: VibeApp,
) -> None:
long_input = "0123456789 " * 20
async with vibe_app.run_test(size=(40, 20)) as pilot:
chat_input = vibe_app.query_one(ChatInputContainer)
textarea = chat_input.input_widget
assert textarea is not None
textarea.insert(long_input)
assert chat_input.value == long_input
assert textarea.wrapped_document.height > 1
textarea.action_cursor_up()
location_after_up = textarea.cursor_location
assert textarea.get_cursor_down_location() != location_after_up
await pilot.press("down")
assert chat_input.value == long_input
assert textarea.cursor_location != location_after_up
@pytest.mark.asyncio
async def test_ui_intercepts_arrow_up_only_on_first_wrapped_row(
vibe_app: VibeApp, history_file: Path
) -> None:
long_input = "abcdefghij " * 20
async with vibe_app.run_test(size=(40, 20)) as pilot:
inject_history_file(vibe_app, history_file)
chat_input = vibe_app.query_one(ChatInputContainer)
textarea = chat_input.input_widget
assert textarea is not None
textarea.insert(long_input)
assert chat_input.value == long_input
assert textarea.wrapped_document.height > 1
textarea.action_cursor_up()
assert chat_input.value == long_input
while textarea.get_cursor_up_location() != textarea.cursor_location:
textarea.action_cursor_up()
await pilot.press("up")
assert chat_input.value == "how are you?"
@pytest.mark.asyncio
async def test_ui_up_from_wrapped_top_loads_history_after_down_at_wrapped_bottom(
vibe_app: VibeApp, history_file: Path
) -> None:
long_input = "LONG " + ("x" * 160)
async with vibe_app.run_test(size=(40, 20)) as pilot:
inject_history_file(vibe_app, history_file)
chat_input = vibe_app.query_one(ChatInputContainer)
textarea = chat_input.input_widget
assert textarea is not None
textarea.insert(long_input)
assert chat_input.value == long_input
assert not textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
assert textarea.navigator.is_last_wrapped_line(textarea.cursor_location)
await pilot.press("down")
textarea.move_cursor((0, 0))
assert textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
await pilot.press("up")
assert chat_input.value == "how are you?"
@pytest.mark.asyncio
async def test_ui_down_cycles_to_next_history_without_manual_move_after_loading_multiline_entry(
vibe_app: VibeApp, tmp_path: Path
) -> None:
long_first_line = "abcdefghij " * 20
history_entry = f"{long_first_line}\nsecond line"
history_path = tmp_path / "history.jsonl"
history_path.write_text(json.dumps(history_entry) + "\n", encoding="utf-8")
async with vibe_app.run_test(size=(40, 20)) as pilot:
inject_history_file(vibe_app, history_path)
chat_input = vibe_app.query_one(ChatInputContainer)
textarea = chat_input.input_widget
assert textarea is not None
await pilot.press("up")
assert chat_input.value == history_entry
assert not textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
assert not textarea.navigator.is_last_wrapped_line(textarea.cursor_location)
await pilot.press("down")
assert chat_input.value == ""
@pytest.mark.asyncio
async def test_ui_up_continues_history_cycle_after_loading_wrapped_multiline_entry(
vibe_app: VibeApp, tmp_path: Path
) -> None:
long_first_line = "abcdefghij " * 20
wrapped_multiline = f"{long_first_line}\nsecond line"
history_path = tmp_path / "history.jsonl"
history_entries = ["older message", wrapped_multiline, "Hi there"]
history_path.write_text(
"\n".join(json.dumps(entry) for entry in history_entries) + "\n",
encoding="utf-8",
)
async with vibe_app.run_test(size=(40, 20)) as pilot:
inject_history_file(vibe_app, history_path)
chat_input = vibe_app.query_one(ChatInputContainer)
textarea = chat_input.input_widget
assert textarea is not None
await pilot.press("up")
assert chat_input.value == "Hi there"
await pilot.press("up")
assert chat_input.value == wrapped_multiline
assert not textarea.navigator.is_first_wrapped_line(textarea.cursor_location)
await pilot.press("up")
assert chat_input.value == "older message"
@pytest.mark.asyncio
async def test_ui_down_at_visual_end_resumes_history_after_manual_cursor_move(
vibe_app: VibeApp, tmp_path: Path
) -> None:
long_first_line = "abcdefghij " * 20
wrapped_multiline = f"{long_first_line}\nsecond line"
history_path = tmp_path / "history.jsonl"
history_entries = ["older message", wrapped_multiline, "Hi there"]
history_path.write_text(
"\n".join(json.dumps(entry) for entry in history_entries) + "\n",
encoding="utf-8",
)
async with vibe_app.run_test(size=(40, 20)) as pilot:
inject_history_file(vibe_app, history_path)
chat_input = vibe_app.query_one(ChatInputContainer)
textarea = chat_input.input_widget
assert textarea is not None
await pilot.press("up")
await pilot.press("up")
assert chat_input.value == wrapped_multiline
await pilot.press("left")
assert textarea.cursor_location != (0, len(long_first_line))
while not textarea.navigator.is_last_wrapped_line(textarea.cursor_location):
textarea.action_cursor_down()
textarea.move_cursor((1, len("second line")))
assert textarea.navigator.is_last_wrapped_line(textarea.cursor_location)
await pilot.press("down")
assert chat_input.value == "Hi there"

View file

@ -0,0 +1,265 @@
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from pydantic import BaseModel
import pytest
from tests.mock.utils import collect_result
from vibe.core.agents.models import AgentProfile, AgentSafety, BuiltinAgentName
from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError
from vibe.core.tools.builtins.ask_user_question import (
Answer,
AskUserQuestionArgs,
AskUserQuestionResult,
)
from vibe.core.tools.builtins.exit_plan_mode import (
ExitPlanMode,
ExitPlanModeArgs,
ExitPlanModeConfig,
)
@dataclass
class MockAgentManager:
active_profile: AgentProfile
_switched_to: list[str] = field(default_factory=list)
def switch_profile(self, name: str) -> None:
self._switched_to.append(name)
self.active_profile = AgentProfile(
name=name,
display_name=name.title(),
description="",
safety=AgentSafety.SAFE,
)
def _plan_profile() -> AgentProfile:
return AgentProfile(
name=BuiltinAgentName.PLAN,
display_name="Plan",
description="Plan mode",
safety=AgentSafety.SAFE,
)
def _default_profile() -> AgentProfile:
return AgentProfile(
name=BuiltinAgentName.DEFAULT,
display_name="Default",
description="Default mode",
safety=AgentSafety.SAFE,
)
@pytest.fixture
def tool() -> ExitPlanMode:
return ExitPlanMode(config=ExitPlanModeConfig(), state=BaseToolState())
@pytest.fixture
def plan_manager() -> MockAgentManager:
return MockAgentManager(active_profile=_plan_profile())
class MockCallback:
def __init__(self, result: AskUserQuestionResult) -> None:
self._result = result
self.received_args: BaseModel | None = None
async def __call__(self, args: BaseModel) -> BaseModel:
self.received_args = args
return self._result
class TestErrorCases:
@pytest.mark.asyncio
async def test_requires_agent_manager(self, tool: ExitPlanMode) -> None:
ctx = InvokeContext(
tool_call_id="t1",
user_input_callback=MockCallback(
AskUserQuestionResult(answers=[], cancelled=True)
),
)
with pytest.raises(ToolError, match="agent manager"):
await collect_result(tool.run(ExitPlanModeArgs(), ctx))
@pytest.mark.asyncio
async def test_requires_plan_mode(self, tool: ExitPlanMode) -> None:
manager = MockAgentManager(active_profile=_default_profile())
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=manager, # type: ignore[arg-type]
user_input_callback=MockCallback(
AskUserQuestionResult(answers=[], cancelled=True)
),
)
with pytest.raises(ToolError, match="plan mode"):
await collect_result(tool.run(ExitPlanModeArgs(), ctx))
@pytest.mark.asyncio
async def test_requires_interactive_ui(
self, tool: ExitPlanMode, plan_manager: MockAgentManager
) -> None:
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
)
with pytest.raises(ToolError, match="interactive UI"):
await collect_result(tool.run(ExitPlanModeArgs(), ctx))
class MockSwitchAgentCallback:
def __init__(self) -> None:
self.calls: list[str] = []
async def __call__(self, name: str) -> None:
self.calls.append(name)
class TestAnswerHandling:
@pytest.mark.asyncio
async def test_yes_uses_switch_agent_callback(
self, tool: ExitPlanMode, plan_manager: MockAgentManager
) -> None:
switch_cb = MockSwitchAgentCallback()
cb = MockCallback(
AskUserQuestionResult(
answers=[
Answer(
question="q",
answer="Yes, and auto approve edits",
is_other=False,
)
],
cancelled=False,
)
)
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
switch_agent_callback=switch_cb,
)
result = await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert result.switched is True
assert switch_cb.calls == [BuiltinAgentName.ACCEPT_EDITS]
assert plan_manager._switched_to == []
@pytest.mark.asyncio
async def test_yes_falls_back_to_switch_profile(
self, tool: ExitPlanMode, plan_manager: MockAgentManager
) -> None:
cb = MockCallback(
AskUserQuestionResult(
answers=[
Answer(
question="q",
answer="Yes, and auto approve edits",
is_other=False,
)
],
cancelled=False,
)
)
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
)
result = await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert result.switched is True
assert plan_manager._switched_to == [BuiltinAgentName.ACCEPT_EDITS]
@pytest.mark.asyncio
async def test_no_stays_in_plan_mode(
self, tool: ExitPlanMode, plan_manager: MockAgentManager
) -> None:
cb = MockCallback(
AskUserQuestionResult(
answers=[Answer(question="q", answer="No", is_other=False)],
cancelled=False,
)
)
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
)
result = await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert result.switched is False
assert plan_manager._switched_to == []
@pytest.mark.asyncio
async def test_cancelled_stays(
self, tool: ExitPlanMode, plan_manager: MockAgentManager
) -> None:
cb = MockCallback(AskUserQuestionResult(answers=[], cancelled=True))
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
)
result = await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert result.switched is False
assert plan_manager._switched_to == []
@pytest.mark.asyncio
async def test_other_includes_feedback(
self, tool: ExitPlanMode, plan_manager: MockAgentManager
) -> None:
cb = MockCallback(
AskUserQuestionResult(
answers=[
Answer(question="q", answer="Add error handling", is_other=True)
],
cancelled=False,
)
)
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
)
result = await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert result.switched is False
assert "Add error handling" in result.message
class TestPlanFile:
@pytest.mark.asyncio
async def test_content_passed_as_preview(
self, tool: ExitPlanMode, plan_manager: MockAgentManager, tmp_path: Path
) -> None:
plan_file = tmp_path / "plan.md"
plan_file.write_text("# My Plan\n\n- Step 1\n- Step 2\n")
cb = MockCallback(AskUserQuestionResult(answers=[], cancelled=True))
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
plan_file_path=plan_file,
)
await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert isinstance(cb.received_args, AskUserQuestionArgs)
assert cb.received_args.content_preview == "# My Plan\n\n- Step 1\n- Step 2\n"
@pytest.mark.asyncio
async def test_missing_file_means_none_preview(
self, tool: ExitPlanMode, plan_manager: MockAgentManager, tmp_path: Path
) -> None:
plan_file = tmp_path / "nonexistent.md"
cb = MockCallback(AskUserQuestionResult(answers=[], cancelled=True))
ctx = InvokeContext(
tool_call_id="t1",
agent_manager=plan_manager, # type: ignore[arg-type]
user_input_callback=cb,
plan_file_path=plan_file,
)
await collect_result(tool.run(ExitPlanModeArgs(), ctx))
assert isinstance(cb.received_args, AskUserQuestionArgs)
assert cb.received_args.content_preview is None