v2.4.0 (#470)
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>
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
254
tests/backend/test_reasoning_adapter.py
Normal 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"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
42
tests/core/test_plan_session.py
Normal 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
|
|
@ -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))
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="215.2" textLength="146.4" clip-path="url(#terminal-line-8)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="215.2" textLength="122" clip-path="url(#terminal-line-8)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="215.2" textLength="146.4" clip-path="url(#terminal-line-8)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="215.2" textLength="122" clip-path="url(#terminal-line-8)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="239.6" textLength="414.8" clip-path="url(#terminal-line-9)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="264" textLength="61" clip-path="url(#terminal-line-10)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="414.8" clip-path="url(#terminal-line-13)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="532.4" textLength="414.8" clip-path="url(#terminal-line-21)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="386" textLength="146.4" clip-path="url(#terminal-line-15)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="386" textLength="122" clip-path="url(#terminal-line-15)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="386" textLength="146.4" clip-path="url(#terminal-line-15)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="386" textLength="122" clip-path="url(#terminal-line-15)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="410.4" textLength="414.8" clip-path="url(#terminal-line-16)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="414.8" clip-path="url(#terminal-line-13)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="414.8" clip-path="url(#terminal-line-13)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="386" textLength="414.8" clip-path="url(#terminal-line-15)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="532.4" textLength="414.8" clip-path="url(#terminal-line-21)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </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)"> · [API] Experiment 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </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)"> · [API] Experiment 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </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)"> for more 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 |
|
|
@ -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,
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
265
tests/tools/test_exit_plan_mode.py
Normal 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
|
||||