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

2
.vscode/launch.json vendored
View file

@ -1,5 +1,5 @@
{ {
"version": "2.3.0", "version": "2.4.0",
"configurations": [ "configurations": [
{ {
"name": "ACP Server", "name": "ACP Server",

View file

@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.4.0] - 2026-03-09
### Added
- User plan displayed in the CLI banner
- Reasoning effort configuration and thinking blocks adapter
### Changed
- Auto-compact threshold is now per-model
- Removed expensive file scan from system prompt; cached git operations for faster agent switching
- Improved plan mode
- Updated `whoami` response handling with new plan type and name fields
### Fixed
- Space key works again in VSCode 1.110+
- Arrow-key history navigation at wrapped-line boundaries in chat input
- UTF-8 encoding enforced when reading metadata files
- Update notifier no longer crashes on unexpected response fields
## [2.3.0] - 2026-02-27 ## [2.3.0] - 2026-02-27
### Added ### Added

View file

@ -1,7 +1,7 @@
id = "mistral-vibe" id = "mistral-vibe"
name = "Mistral Vibe" name = "Mistral Vibe"
description = "Mistral's open-source coding assistant" description = "Mistral's open-source coding assistant"
version = "2.3.0" version = "2.4.0"
schema_version = 1 schema_version = 1
authors = ["Mistral AI"] authors = ["Mistral AI"]
repository = "https://github.com/mistralai/mistral-vibe" repository = "https://github.com/mistralai/mistral-vibe"
@ -11,25 +11,25 @@ name = "Mistral Vibe"
icon = "./icons/mistral_vibe.svg" icon = "./icons/mistral_vibe.svg"
[agent_servers.mistral-vibe.targets.darwin-aarch64] [agent_servers.mistral-vibe.targets.darwin-aarch64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.3.0/vibe-acp-darwin-aarch64-2.3.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-darwin-aarch64-2.4.0.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.darwin-x86_64] [agent_servers.mistral-vibe.targets.darwin-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.3.0/vibe-acp-darwin-x86_64-2.3.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-darwin-x86_64-2.4.0.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.linux-aarch64] [agent_servers.mistral-vibe.targets.linux-aarch64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.3.0/vibe-acp-linux-aarch64-2.3.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-linux-aarch64-2.4.0.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.linux-x86_64] [agent_servers.mistral-vibe.targets.linux-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.3.0/vibe-acp-linux-x86_64-2.3.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-linux-x86_64-2.4.0.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.windows-aarch64] [agent_servers.mistral-vibe.targets.windows-aarch64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.3.0/vibe-acp-windows-aarch64-2.3.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-windows-aarch64-2.4.0.zip"
cmd = "./vibe-acp.exe" cmd = "./vibe-acp.exe"
[agent_servers.mistral-vibe.targets.windows-x86_64] [agent_servers.mistral-vibe.targets.windows-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.3.0/vibe-acp-windows-x86_64-2.3.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-windows-x86_64-2.4.0.zip"
cmd = "./vibe-acp.exe" cmd = "./vibe-acp.exe"

View file

@ -1,6 +1,6 @@
[project] [project]
name = "mistral-vibe" name = "mistral-vibe"
version = "2.3.0" version = "2.4.0"
description = "Minimal CLI coding agent by Mistral" description = "Minimal CLI coding agent by Mistral"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"

View file

@ -79,7 +79,7 @@ def get_current_version() -> str:
return version_match.group(1) return version_match.group(1)
def update_changelog(new_version: str) -> None: def update_changelog(current_version: str, new_version: str) -> None:
changelog_path = Path("CHANGELOG.md") changelog_path = Path("CHANGELOG.md")
if not changelog_path.exists(): if not changelog_path.exists():
@ -104,31 +104,55 @@ def update_changelog(new_version: str) -> None:
updated_content = content[:insert_position] + new_entry + content[insert_position:] updated_content = content[:insert_position] + new_entry + content[insert_position:]
changelog_path.write_text(updated_content) changelog_path.write_text(updated_content)
print(f"Added changelog entry for version {new_version}") # Auto-fill changelog using Vibe in headless mode
print("Filling CHANGELOG.md...")
prompt = f"""Fill the new CHANGELOG.md section for version {new_version} (the one that was just added).
Rules:
- Use only commits that touch the `vibe` folder in this repo since version {current_version}. Inspect git history to list relevant changes.
- Follow the existing file convention: Keep a Changelog format with ### Added, ### Changed, ### Fixed, ### Removed. One bullet per line, concise. Match the tone and style of the entries already in the file.
- Do not mention commit hashes or PR numbers.
- Remove any subsection that has no bullets (leave no empty ### Added / ### Changed / etc)."""
try:
result = subprocess.run(
["vibe", "-p", prompt], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if result.returncode != 0:
raise RuntimeError("Failed to auto-fill CHANGELOG.md")
except Exception:
print(
"Warning: failed to auto-fill CHANGELOG.md, please fill it manually.",
file=sys.stderr,
)
def print_warning(new_version: str) -> None: def fill_whats_new_message(new_version: str) -> None:
warning = f"""
{"=" * 80}
WARNING: CHANGELOG UPDATE REQUIRED
{"=" * 80}
Don't forget to fill in the changelog entry for version {new_version} in CHANGELOG.md! 📝
Also, remember to fill in vibe/whats_new.md if needed (you can leave it blank). 📝
{"=" * 80}
"""
print(warning, file=sys.stderr)
def clean_up_whats_new_message() -> None:
whats_new_path = Path("vibe/whats_new.md") whats_new_path = Path("vibe/whats_new.md")
if not whats_new_path.exists(): if not whats_new_path.exists():
raise FileNotFoundError("whats_new.md not found in current directory") raise FileNotFoundError("whats_new.md not found in current directory")
whats_new_path.write_text("") whats_new_path.write_text("")
print("Filling whats_new.md...")
prompt = f"""Fill vibe/whats_new.md using only the CHANGELOG.md section for version {new_version}.
Rules:
- Include only the most important user-facing changes: visible CLI/UI behavior, new commands or key bindings, UX improvements. Exclude internal refactors, API-only changes, and dev/tooling updates.
- If there are no such changes, write nothing (empty file).
- Otherwise: first line must be "# What's new in v{new_version}" (no extra heading). Then one bullet per item, format: "- **Feature**: short summary" (e.g. - **Interactive resume**: Added a /resume command to choose which session to resume). One line per bullet, concise.
- Do not copy the full changelog; summarize only what matters to someone reading "what's new" in the app."""
try:
result = subprocess.run(
["vibe", "-p", prompt], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
if result.returncode != 0:
raise RuntimeError("Failed to auto-fill whats_new.md")
except Exception:
print(
"Warning: failed to auto-fill whats_new.md, please fill it manually.",
file=sys.stderr,
)
def main() -> None: def main() -> None:
os.chdir(Path(__file__).parent.parent) os.chdir(Path(__file__).parent.parent)
@ -158,7 +182,7 @@ Examples:
# Calculate new version # Calculate new version
new_version = bump_version(current_version, args.bump_type) new_version = bump_version(current_version, args.bump_type)
print(f"New version: {new_version}") print(f"New version: {new_version}\n")
# Update pyproject.toml # Update pyproject.toml
update_hard_values_files( update_hard_values_files(
@ -193,15 +217,15 @@ Examples:
[(f'version="{current_version}"', f'version="{new_version}"')], [(f'version="{current_version}"', f'version="{new_version}"')],
) )
# Update CHANGELOG.md print()
update_changelog(new_version) update_changelog(current_version=current_version, new_version=new_version)
clean_up_whats_new_message() fill_whats_new_message(new_version=new_version)
print()
subprocess.run(["uv", "lock"], check=True) subprocess.run(["uv", "lock"], check=True)
print(f"\nSuccessfully bumped version from {current_version} to {new_version}") print(f"\nSuccessfully bumped version from {current_version} to {new_version}")
print_warning(new_version)
except Exception as e: except Exception as e:
print(f"Error: {e}", file=sys.stderr) print(f"Error: {e}", file=sys.stderr)

View file

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

View file

@ -28,7 +28,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()), session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
) )
assert response.agent_info == Implementation( 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 == [] assert response.auth_methods == []
@ -52,7 +52,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()), session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
) )
assert response.agent_info == Implementation( 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 assert response.auth_methods is not None

View file

@ -114,6 +114,10 @@ class TestAcpSearchReplaceExecution:
assert isinstance(result, SearchReplaceResult) assert isinstance(result, SearchReplaceResult)
assert result.file == str(test_file) assert result.file == str(test_file)
assert result.blocks_applied == 1 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._read_text_file_called
assert mock_client._write_text_file_called assert mock_client._write_text_file_called
assert mock_client._session_update_called assert mock_client._session_update_called
@ -314,6 +318,7 @@ class TestAcpSearchReplaceSessionUpdates:
lines_changed=1, lines_changed=1,
content=search_replace_content, content=search_replace_content,
warnings=[], warnings=[],
file_content_before="old text",
) )
event = ToolResultEvent( event = ToolResultEvent(

View file

@ -106,6 +106,36 @@ class TestACPSetConfigOptionMode:
assert response is not None assert response is not None
assert acp_session.agent_loop.agent_profile.name == BuiltinAgentName.PLAN 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 @pytest.mark.asyncio
async def test_set_config_option_mode_invalid_returns_none( async def test_set_config_option_mode_invalid_returns_none(
self, acp_agent_loop: VibeAcpAgentLoop self, acp_agent_loop: VibeAcpAgentLoop

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,11 +13,16 @@ from tests.update_notifier.adapters.fake_update_cache_repository import (
FakeUpdateCacheRepository, FakeUpdateCacheRepository,
) )
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway 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.cli.textual_ui.app import CORE_VERSION, VibeApp
from vibe.core.agent_loop import AgentLoop from vibe.core.agent_loop import AgentLoop
from vibe.core.agents.models import BuiltinAgentName 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.llm.types import BackendLike
from vibe.core.paths import global_paths from vibe.core.paths import global_paths
from vibe.core.paths.config_paths import unlock_config_paths from vibe.core.paths.config_paths import unlock_config_paths
@ -125,6 +130,13 @@ def vibe_config() -> VibeConfig:
return build_test_vibe_config() 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: def build_test_vibe_config(**kwargs) -> VibeConfig:
session_logging = kwargs.pop("session_logging", None) session_logging = kwargs.pop("session_logging", None)
resolved_session_logging = ( resolved_session_logging = (
@ -136,6 +148,8 @@ def build_test_vibe_config(**kwargs) -> VibeConfig:
resolved_enable_update_checks = ( resolved_enable_update_checks = (
False if enable_update_checks is None else 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( return VibeConfig(
session_logging=resolved_session_logging, session_logging=resolved_session_logging,
enable_update_checks=resolved_enable_update_checks, enable_update_checks=resolved_enable_update_checks,
@ -184,8 +198,8 @@ def build_test_vibe_app(
resolved_plan_offer_gateway = ( resolved_plan_offer_gateway = (
FakeWhoAmIGateway( FakeWhoAmIGateway(
WhoAmIResponse( WhoAmIResponse(
is_pro_plan=True, plan_type=WhoAmIPlanType.CHAT,
advertise_pro_plan=False, plan_name="INDIVIDUAL",
prompt_switching_to_pro_plan=False, prompt_switching_to_pro_plan=False,
) )
) )

View file

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

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

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

View file

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

View file

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

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

View file

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

View file

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

View file

@ -2,7 +2,11 @@ from __future__ import annotations
import pytest 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.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend from tests.stubs.fake_backend import FakeBackend
from vibe.core.types import ( 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="<summary>")],
[mock_llm_chunk(content="<final>")], [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 = build_test_agent_loop(config=cfg, backend=backend)
agent.stats.context_tokens = 2 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="<summary>")],
[mock_llm_chunk(content="<final>")], [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( agent = build_test_agent_loop(
config=cfg, message_observer=observer, backend=backend 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="<summary>")],
[mock_llm_chunk(content="<final>")], [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( agent = build_test_agent_loop(
config=cfg, message_observer=observer, backend=backend 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="<summary>")],
[mock_llm_chunk(content="<final>")], [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 = build_test_agent_loop(config=cfg, backend=backend)
agent.stats.context_tokens = 2 agent.stats.context_tokens = 2

View file

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

View file

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

View file

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

View file

@ -5,12 +5,10 @@ from pathlib import Path
import pytest 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
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend from tests.stubs.fake_backend import FakeBackend
from vibe.core.agents.manager import AgentManager from vibe.core.agents.manager import AgentManager
from vibe.core.agents.models import ( from vibe.core.agents.models import (
BUILTIN_AGENTS, BUILTIN_AGENTS,
PLAN_AGENT_TOOLS,
AgentProfile, AgentProfile,
AgentSafety, AgentSafety,
AgentType, AgentType,
@ -21,15 +19,7 @@ from vibe.core.config import VibeConfig
from vibe.core.paths.config_paths import ConfigPath from vibe.core.paths.config_paths import ConfigPath
from vibe.core.paths.global_paths import GlobalPath from vibe.core.paths.global_paths import GlobalPath
from vibe.core.tools.base import ToolPermission from vibe.core.tools.base import ToolPermission
from vibe.core.types import ( from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
FunctionCall,
LLMChunk,
LLMMessage,
LLMUsage,
Role,
ToolCall,
ToolResultEvent,
)
class TestDeepMerge: class TestDeepMerge:
@ -216,8 +206,14 @@ class TestAgentProfileOverrides:
def test_plan_agent_restricts_tools(self) -> None: def test_plan_agent_restricts_tools(self) -> None:
overrides = BUILTIN_AGENTS[BuiltinAgentName.PLAN].overrides overrides = BUILTIN_AGENTS[BuiltinAgentName.PLAN].overrides
assert "enabled_tools" in overrides assert "tools" in overrides
assert overrides["enabled_tools"] == PLAN_AGENT_TOOLS 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: def test_accept_edits_agent_sets_tool_permissions(self) -> None:
overrides = BUILTIN_AGENTS[BuiltinAgentName.ACCEPT_EDITS].overrides overrides = BUILTIN_AGENTS[BuiltinAgentName.ACCEPT_EDITS].overrides
@ -233,9 +229,7 @@ class TestAgentManagerCycling:
@pytest.fixture @pytest.fixture
def base_config(self) -> VibeConfig: def base_config(self) -> VibeConfig:
return build_test_vibe_config( 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 @pytest.fixture
@ -302,9 +296,7 @@ class TestAgentSwitchAgent:
@pytest.fixture @pytest.fixture
def base_config(self) -> VibeConfig: def base_config(self) -> VibeConfig:
return build_test_vibe_config( 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 @pytest.fixture
@ -317,21 +309,26 @@ class TestAgentSwitchAgent:
]) ])
@pytest.mark.asyncio @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 self, base_config: VibeConfig, backend: FakeBackend
) -> None: ) -> None:
agent = build_test_agent_loop( agent = build_test_agent_loop(
config=base_config, agent_name=BuiltinAgentName.DEFAULT, backend=backend 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) await agent.switch_agent(BuiltinAgentName.PLAN)
plan_tool_names = set(agent.tool_manager.available_tools.keys()) 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 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 @pytest.mark.asyncio
async def test_switch_from_plan_to_default_restores_tools( async def test_switch_from_plan_to_default_restores_tools(
self, base_config: VibeConfig, backend: FakeBackend self, base_config: VibeConfig, backend: FakeBackend
@ -339,11 +336,12 @@ class TestAgentSwitchAgent:
agent = build_test_agent_loop( agent = build_test_agent_loop(
config=base_config, agent_name=BuiltinAgentName.PLAN, backend=backend 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) 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 assert agent.agent_profile.name == BuiltinAgentName.DEFAULT
@pytest.mark.asyncio @pytest.mark.asyncio
@ -392,9 +390,7 @@ class TestAcceptEditsAgent:
async def test_accept_edits_agent_auto_approves_write_file(self) -> None: async def test_accept_edits_agent_auto_approves_write_file(self) -> None:
backend = FakeBackend([]) backend = FakeBackend([])
config = build_test_vibe_config( config = build_test_vibe_config(enabled_tools=["write_file"])
auto_compact_threshold=0, enabled_tools=["write_file"]
)
agent = build_test_agent_loop( agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.ACCEPT_EDITS, backend=backend 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: async def test_accept_edits_agent_requires_approval_for_other_tools(self) -> None:
backend = FakeBackend([]) backend = FakeBackend([])
config = build_test_vibe_config( config = build_test_vibe_config(enabled_tools=["bash"])
auto_compact_threshold=0, enabled_tools=["bash"]
)
agent = build_test_agent_loop( agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.ACCEPT_EDITS, backend=backend config=config, agent_name=BuiltinAgentName.ACCEPT_EDITS, backend=backend
) )
@ -419,52 +413,36 @@ class TestAcceptEditsAgent:
class TestPlanAgentToolRestriction: class TestPlanAgentToolRestriction:
@pytest.mark.asyncio @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([ backend = FakeBackend([
LLMChunk( LLMChunk(
message=LLMMessage(role=Role.assistant, content="ok"), message=LLMMessage(role=Role.assistant, content="ok"),
usage=LLMUsage(prompt_tokens=10, completion_tokens=5), 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( agent = build_test_agent_loop(
config=config, agent_name=BuiltinAgentName.PLAN, backend=backend config=config, agent_name=BuiltinAgentName.PLAN, backend=backend
) )
tool_names = set(agent.tool_manager.available_tools.keys()) tool_names = set(agent.tool_manager.available_tools.keys())
assert "bash" not in tool_names # Plan mode now has all tools available
assert "write_file" not in tool_names assert "grep" in tool_names
assert "search_replace" not in tool_names assert "read_file" in tool_names
for plan_tool in PLAN_AGENT_TOOLS: assert "write_file" in tool_names
assert plan_tool in tool_names assert "search_replace" in tool_names
@pytest.mark.asyncio # But write tools have restricted permissions
async def test_plan_agent_rejects_non_plan_tool_call(self) -> None: write_config = agent.tool_manager.get_tool_config("write_file")
tool_call = ToolCall( assert write_config.permission == ToolPermission.NEVER
id="call_1", assert len(write_config.allowlist) > 0
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"),
])
config = build_test_vibe_config(auto_compact_threshold=0) sr_config = agent.tool_manager.get_tool_config("search_replace")
agent = build_test_agent_loop( assert sr_config.permission == ToolPermission.NEVER
config=config, agent_name=BuiltinAgentName.PLAN, backend=backend assert len(sr_config.allowlist) > 0
)
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()
)
class TestAgentManagerFiltering: class TestAgentManagerFiltering:

View file

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

View file

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

View file

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

View file

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

2
uv.lock generated
View file

@ -779,7 +779,7 @@ wheels = [
[[package]] [[package]]
name = "mistral-vibe" name = "mistral-vibe"
version = "2.3.0" version = "2.4.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "agent-client-protocol" }, { name = "agent-client-protocol" },

View file

@ -3,4 +3,4 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
VIBE_ROOT = Path(__file__).parent VIBE_ROOT = Path(__file__).parent
__version__ = "2.3.0" __version__ = "2.4.0"

View file

@ -213,7 +213,9 @@ class VibeAcpAgentLoop(AcpAgent):
def _load_config(self) -> VibeConfig: def _load_config(self) -> VibeConfig:
try: try:
config = VibeConfig.load(disabled_tools=["ask_user_question"]) config = VibeConfig.load(
disabled_tools=["ask_user_question", "exit_plan_mode"]
)
config.tool_paths.extend(self._get_acp_tool_overrides()) config.tool_paths.extend(self._get_acp_tool_overrides())
return config return config
except MissingAPIKeyError as e: except MissingAPIKeyError as e:
@ -512,7 +514,7 @@ class VibeAcpAgentLoop(AcpAgent):
new_config = VibeConfig.load( new_config = VibeConfig.load(
tool_paths=session.agent_loop.config.tool_paths, tool_paths=session.agent_loop.config.tool_paths,
disabled_tools=["ask_user_question"], disabled_tools=["ask_user_question", "exit_plan_mode"],
) )
await session.agent_loop.reload_with_initial_messages(base_config=new_config) await session.agent_loop.reload_with_initial_messages(base_config=new_config)

View file

@ -151,6 +151,7 @@ def run_cli(args: argparse.Namespace) -> None:
stdin_prompt = get_prompt_from_stdin() stdin_prompt = get_prompt_from_stdin()
if args.prompt is not None: if args.prompt is not None:
config.disabled_tools = [*config.disabled_tools, "ask_user_question"]
programmatic_prompt = args.prompt or stdin_prompt programmatic_prompt = args.prompt or stdin_prompt
if not programmatic_prompt: if not programmatic_prompt:
print( print(

View file

@ -34,13 +34,7 @@ class HttpWhoAmIGateway:
raise WhoAmIGatewayError(f"Unexpected status {response.status_code}") raise WhoAmIGatewayError(f"Unexpected status {response.status_code}")
payload = _safe_json(response) or {} payload = _safe_json(response) or {}
return WhoAmIResponse( return WhoAmIResponse.from_payload(payload)
is_pro_plan=_parse_bool(payload.get("is_pro_plan")),
advertise_pro_plan=_parse_bool(payload.get("advertise_pro_plan")),
prompt_switching_to_pro_plan=_parse_bool(
payload.get("prompt_switching_to_pro_plan")
),
)
def _safe_json(response: httpx.Response) -> Mapping[str, object] | None: def _safe_json(response: httpx.Response) -> Mapping[str, object] | None:
@ -49,19 +43,3 @@ def _safe_json(response: httpx.Response) -> Mapping[str, object] | None:
except ValueError: except ValueError:
return None return None
return cast(Mapping[str, object], data) if isinstance(data, dict) else None return cast(Mapping[str, object], data) if isinstance(data, dict) else None
def _parse_bool(value: object | None) -> bool:
if value is None:
return False
if isinstance(value, bool):
return value
if isinstance(value, str):
match value.strip().lower():
case "true":
return True
case "false":
return False
case _:
raise WhoAmIGatewayError("Invalid boolean string in whoami response")
raise WhoAmIGatewayError("Invalid boolean value in whoami response")

View file

@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
from enum import StrEnum
import logging import logging
from os import getenv from os import getenv
@ -8,6 +7,7 @@ from vibe.cli.plan_offer.ports.whoami_gateway import (
WhoAmIGateway, WhoAmIGateway,
WhoAmIGatewayError, WhoAmIGatewayError,
WhoAmIGatewayUnauthorized, WhoAmIGatewayUnauthorized,
WhoAmIPlanType,
WhoAmIResponse, WhoAmIResponse,
) )
from vibe.core.config import DEFAULT_MISTRAL_API_ENV_KEY, Backend, ProviderConfig from vibe.core.config import DEFAULT_MISTRAL_API_ENV_KEY, Backend, ProviderConfig
@ -19,51 +19,50 @@ UPGRADE_URL = CONSOLE_CLI_URL
SWITCH_TO_PRO_KEY_URL = CONSOLE_CLI_URL SWITCH_TO_PRO_KEY_URL = CONSOLE_CLI_URL
class PlanOfferAction(StrEnum): class PlanInfo:
NONE = "none" plan_type: WhoAmIPlanType
UPGRADE = "upgrade" plan_name: str
SWITCH_TO_PRO_KEY = "switch_to_pro_key" prompt_switching_to_pro_plan: bool
def __init__(
self,
plan_type: WhoAmIPlanType,
plan_name: str = "",
prompt_switching_to_pro_plan: bool = False,
) -> None:
self.plan_type = plan_type
self.plan_name = plan_name
self.prompt_switching_to_pro_plan = prompt_switching_to_pro_plan
@classmethod
def from_response(cls, response: WhoAmIResponse) -> PlanInfo:
return cls(
plan_type=response.plan_type,
plan_name=response.plan_name,
prompt_switching_to_pro_plan=response.prompt_switching_to_pro_plan,
)
def is_paid_api_plan(self) -> bool:
return self.plan_type == WhoAmIPlanType.API and not self.is_free_api_plan()
def is_free_api_plan(self) -> bool:
return self.plan_type == WhoAmIPlanType.API and "FREE" in self.plan_name.upper()
def is_chat_pro_plan(self) -> bool:
return self.plan_type == WhoAmIPlanType.CHAT
ACTION_TO_URL: dict[PlanOfferAction, str] = { async def decide_plan_offer(api_key: str | None, gateway: WhoAmIGateway) -> PlanInfo:
PlanOfferAction.UPGRADE: UPGRADE_URL,
PlanOfferAction.SWITCH_TO_PRO_KEY: SWITCH_TO_PRO_KEY_URL,
}
class PlanType(StrEnum):
FREE = "free"
PRO = "pro"
UNKNOWN = "unknown"
async def decide_plan_offer(
api_key: str | None, gateway: WhoAmIGateway
) -> tuple[PlanOfferAction, PlanType]:
if not api_key: if not api_key:
return PlanOfferAction.UPGRADE, PlanType.FREE return PlanInfo(WhoAmIPlanType.UNKNOWN)
try: try:
response = await gateway.whoami(api_key) response = await gateway.whoami(api_key)
return PlanInfo.from_response(response)
except WhoAmIGatewayUnauthorized: except WhoAmIGatewayUnauthorized:
return PlanOfferAction.UPGRADE, PlanType.FREE return PlanInfo(WhoAmIPlanType.UNAUTHORIZED)
except WhoAmIGatewayError: except WhoAmIGatewayError:
logger.warning("Failed to fetch plan status.", exc_info=True) logger.warning("Failed to fetch plan status.", exc_info=True)
return PlanOfferAction.NONE, PlanType.UNKNOWN return PlanInfo(WhoAmIPlanType.UNKNOWN)
return _action_and_plan_from_response(response)
def _action_and_plan_from_response(
response: WhoAmIResponse,
) -> tuple[PlanOfferAction, PlanType]:
match response:
case WhoAmIResponse(is_pro_plan=True):
return PlanOfferAction.NONE, PlanType.PRO
case WhoAmIResponse(prompt_switching_to_pro_plan=True):
return PlanOfferAction.SWITCH_TO_PRO_KEY, PlanType.PRO
case WhoAmIResponse(advertise_pro_plan=True):
return PlanOfferAction.UPGRADE, PlanType.FREE
case _:
return PlanOfferAction.NONE, PlanType.UNKNOWN
def resolve_api_key_for_plan(provider: ProviderConfig) -> str | None: def resolve_api_key_for_plan(provider: ProviderConfig) -> str | None:
@ -75,13 +74,22 @@ def resolve_api_key_for_plan(provider: ProviderConfig) -> str | None:
return getenv(api_env_key) return getenv(api_env_key)
def plan_offer_cta(action: PlanOfferAction) -> str | None: def plan_offer_cta(payload: PlanInfo | None) -> str | None:
if action is PlanOfferAction.NONE: if not payload:
return return
url = ACTION_TO_URL[action] if payload.prompt_switching_to_pro_plan:
match action: return f"### Switch to your [Le Chat Pro API key]({SWITCH_TO_PRO_KEY_URL})"
case PlanOfferAction.UPGRADE: if payload.plan_type in {WhoAmIPlanType.API, WhoAmIPlanType.UNAUTHORIZED}:
text = f"### Unlock more with Vibe - [Upgrade to Le Chat Pro]({url})" return f"### Unlock more with Vibe - [Upgrade to Le Chat Pro]({UPGRADE_URL})"
case PlanOfferAction.SWITCH_TO_PRO_KEY:
text = f"### Switch to your [Le Chat Pro API key]({url})"
return text def plan_title(payload: PlanInfo | None) -> str | None:
if not payload:
return None
if payload.is_chat_pro_plan():
return "[Subscription] Pro"
if payload.is_free_api_plan():
return "[API] Experiment plan"
if payload.is_paid_api_plan():
return "[API] Scale plan"
return None

View file

@ -1,15 +1,58 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from enum import StrEnum
from typing import Protocol from typing import Protocol
from pydantic import TypeAdapter, ValidationError
class WhoAmIPlanType(StrEnum):
API = "API"
CHAT = "CHAT"
UNKNOWN = "UNKNOWN"
UNAUTHORIZED = "UNAUTHORIZED"
@classmethod
def from_string(cls, value: str) -> WhoAmIPlanType:
try:
return cls(value.strip().upper())
except ValueError:
return cls.UNKNOWN
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class WhoAmIResponse: class WhoAmIResponse:
is_pro_plan: bool plan_type: WhoAmIPlanType
advertise_pro_plan: bool plan_name: str
prompt_switching_to_pro_plan: bool prompt_switching_to_pro_plan: bool
@classmethod
def from_payload(cls, payload: Mapping[str, object]) -> WhoAmIResponse:
plan_type = payload.get("plan_type")
plan_name = payload.get("plan_name")
if not isinstance(plan_type, str) or not isinstance(plan_name, str):
raise WhoAmIGatewayError(f"Invalid whoami response: {payload}")
return cls(
plan_type=WhoAmIPlanType.from_string(plan_type),
plan_name=plan_name.strip(),
prompt_switching_to_pro_plan=_parse_bool(
payload.get("prompt_switching_to_pro_plan")
),
)
def _parse_bool(value: object | None) -> bool:
if value is None:
return False
try:
return TypeAdapter(bool).validate_python(value)
except ValidationError as e:
raise WhoAmIGatewayError(
f"Invalid boolean value in whoami response: {value}"
) from e
class WhoAmIGatewayUnauthorized(Exception): class WhoAmIGatewayUnauthorized(Exception):
pass pass

View file

@ -25,12 +25,13 @@ from vibe.cli.clipboard import copy_selection_to_clipboard
from vibe.cli.commands import CommandRegistry from vibe.cli.commands import CommandRegistry
from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway
from vibe.cli.plan_offer.decide_plan_offer import ( from vibe.cli.plan_offer.decide_plan_offer import (
PlanType, PlanInfo,
decide_plan_offer, decide_plan_offer,
plan_offer_cta, plan_offer_cta,
plan_title,
resolve_api_key_for_plan, resolve_api_key_for_plan,
) )
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIGateway from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIGateway, WhoAmIPlanType
from vibe.cli.terminal_setup import setup_terminal from vibe.cli.terminal_setup import setup_terminal
from vibe.cli.textual_ui.handlers.event_handler import EventHandler from vibe.cli.textual_ui.handlers.event_handler import EventHandler
from vibe.cli.textual_ui.notifications import ( from vibe.cli.textual_ui.notifications import (
@ -89,7 +90,7 @@ from vibe.cli.update_notifier.update import do_update
from vibe.core.agent_loop import AgentLoop, TeleportError from vibe.core.agent_loop import AgentLoop, TeleportError
from vibe.core.agents import AgentProfile from vibe.core.agents import AgentProfile
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
from vibe.core.config import VibeConfig from vibe.core.config import Backend, VibeConfig
from vibe.core.logger import logger from vibe.core.logger import logger
from vibe.core.paths.config_paths import HISTORY_FILE from vibe.core.paths.config_paths import HISTORY_FILE
from vibe.core.session.session_loader import SessionLoader from vibe.core.session.session_loader import SessionLoader
@ -270,6 +271,7 @@ class VibeApp(App): # noqa: PLR0904
self._cached_chat: ChatScroll | None = None self._cached_chat: ChatScroll | None = None
self._cached_loading_area: Widget | None = None self._cached_loading_area: Widget | None = None
self._switch_agent_generation = 0 self._switch_agent_generation = 0
self._plan_info: PlanInfo | None = None
@property @property
def config(self) -> VibeConfig: def config(self) -> VibeConfig:
@ -319,7 +321,7 @@ class VibeApp(App): # noqa: PLR0904
def update_context_progress(stats: AgentStats) -> None: def update_context_progress(stats: AgentStats) -> None:
context_progress.tokens = TokenState( context_progress.tokens = TokenState(
max_tokens=self.config.auto_compact_threshold, max_tokens=self.config.get_active_model().auto_compact_threshold,
current_tokens=stats.context_tokens, current_tokens=stats.context_tokens,
) )
@ -332,6 +334,7 @@ class VibeApp(App): # noqa: PLR0904
chat_input_container = self.query_one(ChatInputContainer) chat_input_container = self.query_one(ChatInputContainer)
chat_input_container.focus_input() chat_input_container.focus_input()
await self._resolve_plan()
await self._show_dangerous_directory_warning() await self._show_dangerous_directory_warning()
await self._resume_history_from_messages() await self._resume_history_from_messages()
await self._check_and_show_whats_new() await self._check_and_show_whats_new()
@ -728,10 +731,7 @@ class VibeApp(App): # noqa: PLR0904
message = str(e) message = str(e)
if isinstance(e, RateLimitError): if isinstance(e, RateLimitError):
if self.plan_type == PlanType.FREE: message = self._rate_limit_message()
message = "Rate limits exceeded. Please wait a moment before trying again, or upgrade to Pro for higher rate limits and uninterrupted access."
else:
message = "Rate limits exceeded. Please wait a moment before trying again."
await self._mount_and_scroll( await self._mount_and_scroll(
ErrorMessage(message, collapsed=self._tools_collapsed) ErrorMessage(message, collapsed=self._tools_collapsed)
@ -748,6 +748,15 @@ class VibeApp(App): # noqa: PLR0904
await self._refresh_windowing_from_history() await self._refresh_windowing_from_history()
self._terminal_notifier.notify(NotificationContext.COMPLETE) self._terminal_notifier.notify(NotificationContext.COMPLETE)
def _rate_limit_message(self) -> str:
upgrade_to_pro = self._plan_info and self._plan_info.plan_type in {
WhoAmIPlanType.API,
WhoAmIPlanType.UNAUTHORIZED,
}
if upgrade_to_pro:
return "Rate limits exceeded. Please wait a moment before trying again, or upgrade to Pro for higher rate limits and uninterrupted access."
return "Rate limits exceeded. Please wait a moment before trying again."
async def _teleport_command(self) -> None: async def _teleport_command(self) -> None:
await self._handle_teleport_command(show_message=False) await self._handle_teleport_command(show_message=False)
@ -1003,9 +1012,14 @@ class VibeApp(App): # noqa: PLR0904
base_config = VibeConfig.load() base_config = VibeConfig.load()
await self.agent_loop.reload_with_initial_messages(base_config=base_config) await self.agent_loop.reload_with_initial_messages(base_config=base_config)
await self._resolve_plan()
if self._banner: if self._banner:
self._banner.set_state(base_config, self.agent_loop.skill_manager) self._banner.set_state(
base_config,
self.agent_loop.skill_manager,
plan_title(self._plan_info),
)
await self._mount_and_scroll(UserCommandMessage("Configuration reloaded.")) await self._mount_and_scroll(UserCommandMessage("Configuration reloaded."))
except Exception as e: except Exception as e:
await self._mount_and_scroll( await self._mount_and_scroll(
@ -1198,6 +1212,7 @@ class VibeApp(App): # noqa: PLR0904
self._chat_input_container.disabled = False self._chat_input_container.disabled = False
self._chat_input_container.display = True self._chat_input_container.display = True
self._current_bottom_app = BottomApp.Input self._current_bottom_app = BottomApp.Input
self._refresh_profile_widgets()
self.call_after_refresh(self._chat_input_container.focus_input) self.call_after_refresh(self._chat_input_container.focus_input)
chat = self._cached_chat or self.query_one("#chat", ChatScroll) chat = self._cached_chat or self.query_one("#chat", ChatScroll)
if chat.is_at_bottom: if chat.is_at_bottom:
@ -1371,7 +1386,9 @@ class VibeApp(App): # noqa: PLR0904
def _refresh_banner(self) -> None: def _refresh_banner(self) -> None:
if self._banner: if self._banner:
self._banner.set_state(self.config, self.agent_loop.skill_manager) self._banner.set_state(
self.config, self.agent_loop.skill_manager, plan_title(self._plan_info)
)
def _update_profile_widgets(self, profile: AgentProfile) -> None: def _update_profile_widgets(self, profile: AgentProfile) -> None:
if self._chat_input_container: if self._chat_input_container:
@ -1460,7 +1477,7 @@ class VibeApp(App): # noqa: PLR0904
content = load_whats_new_content() content = load_whats_new_content()
if content is not None: if content is not None:
whats_new_message = WhatsNewMessage(content) whats_new_message = WhatsNewMessage(content)
plan_offer = await self._plan_offer_cta() plan_offer = plan_offer_cta(self._plan_info)
if plan_offer is not None: if plan_offer is not None:
whats_new_message = WhatsNewMessage(f"{content}\n\n{plan_offer}") whats_new_message = WhatsNewMessage(f"{content}\n\n{plan_offer}")
if self._history_widget_indices: if self._history_widget_indices:
@ -1474,23 +1491,21 @@ class VibeApp(App): # noqa: PLR0904
chat.anchor() chat.anchor()
await mark_version_as_seen(self._current_version, self._update_cache_repository) await mark_version_as_seen(self._current_version, self._update_cache_repository)
async def _plan_offer_cta(self) -> str | None: async def _resolve_plan(self) -> None:
self.plan_type = PlanType.UNKNOWN
if self._plan_offer_gateway is None: if self._plan_offer_gateway is None:
self._plan_info = None
return return
try: try:
active_model = self.config.get_active_model() active_model = self.config.get_active_model()
provider = self.config.get_provider_for_model(active_model) provider = self.config.get_provider_for_model(active_model)
api_key = resolve_api_key_for_plan(provider) if provider.backend != Backend.MISTRAL:
action, plan_type = await decide_plan_offer( self._plan_info = None
api_key, self._plan_offer_gateway return
)
self.plan_type = plan_type api_key = resolve_api_key_for_plan(provider)
return plan_offer_cta(action) self._plan_info = await decide_plan_offer(api_key, self._plan_offer_gateway)
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
"Plan-offer check failed (%s).", type(exc).__name__, exc_info=True "Plan-offer check failed (%s).", type(exc).__name__, exc_info=True

View file

@ -840,6 +840,10 @@ StatusMessage {
height: auto; height: auto;
} }
#question-content.question-content-docked {
dock: bottom;
}
.question-tabs { .question-tabs {
height: auto; height: auto;
color: ansi_blue; color: ansi_blue;
@ -900,6 +904,28 @@ StatusMessage {
margin-top: 1; margin-top: 1;
} }
.question-content-preview {
width: 100%;
height: auto;
max-height: 50vh;
border: none;
border-left: wide ansi_bright_black;
padding: 0 0 0 1;
margin-bottom: 1;
}
.question-content-preview-text {
width: 100%;
height: auto;
color: ansi_default;
padding: 0;
margin: 0;
& > *:last-child {
margin-bottom: 0;
}
}
ExpandingBorder { ExpandingBorder {
width: auto; width: auto;
height: 100%; height: 100%;
@ -959,6 +985,10 @@ ContextProgress {
width: auto; width: auto;
} }
#banner-user-plan {
width: auto;
}
#banner-meta-counts { #banner-meta-counts {
width: auto; width: auto;
} }

View file

@ -21,6 +21,7 @@ class BannerState:
models_count: int = 0 models_count: int = 0
mcp_servers_count: int = 0 mcp_servers_count: int = 0
skills_count: int = 0 skills_count: int = 0
plan_description: str | None = None
class Banner(Static): class Banner(Static):
@ -36,6 +37,7 @@ class Banner(Static):
models_count=len(config.models), models_count=len(config.models),
mcp_servers_count=len(config.mcp_servers), mcp_servers_count=len(config.mcp_servers),
skills_count=len(skill_manager.available_skills), skills_count=len(skill_manager.available_skills),
plan_description=None,
) )
self._animated = not config.disable_welcome_banner_animation self._animated = not config.disable_welcome_banner_animation
@ -49,6 +51,7 @@ class Banner(Static):
yield NoMarkupStatic(" ", classes="banner-spacer") yield NoMarkupStatic(" ", classes="banner-spacer")
yield NoMarkupStatic(f"v{__version__} · ", classes="banner-meta") yield NoMarkupStatic(f"v{__version__} · ", classes="banner-meta")
yield NoMarkupStatic("", id="banner-model") yield NoMarkupStatic("", id="banner-model")
yield NoMarkupStatic("", id="banner-user-plan")
with Horizontal(classes="banner-line"): with Horizontal(classes="banner-line"):
yield NoMarkupStatic("", id="banner-meta-counts") yield NoMarkupStatic("", id="banner-meta-counts")
with Horizontal(classes="banner-line"): with Horizontal(classes="banner-line"):
@ -64,17 +67,24 @@ class Banner(Static):
self.query_one("#banner-meta-counts", NoMarkupStatic).update( self.query_one("#banner-meta-counts", NoMarkupStatic).update(
self._format_meta_counts() self._format_meta_counts()
) )
self.query_one("#banner-user-plan", NoMarkupStatic).update(self._format_plan())
def freeze_animation(self) -> None: def freeze_animation(self) -> None:
if self._animated: if self._animated:
self.query_one(PetitChat).freeze_animation() self.query_one(PetitChat).freeze_animation()
def set_state(self, config: VibeConfig, skill_manager: SkillManager) -> None: def set_state(
self,
config: VibeConfig,
skill_manager: SkillManager,
plan_description: str | None = None,
) -> None:
self.state = BannerState( self.state = BannerState(
active_model=config.active_model, active_model=config.active_model,
models_count=len(config.models), models_count=len(config.models),
mcp_servers_count=len(config.mcp_servers), mcp_servers_count=len(config.mcp_servers),
skills_count=len(skill_manager.available_skills), skills_count=len(skill_manager.available_skills),
plan_description=plan_description,
) )
def _format_meta_counts(self) -> str: def _format_meta_counts(self) -> str:
@ -83,3 +93,10 @@ class Banner(Static):
f" · {self.state.mcp_servers_count} MCP server{'s' if self.state.mcp_servers_count != 1 else ''}" f" · {self.state.mcp_servers_count} MCP server{'s' if self.state.mcp_servers_count != 1 else ''}"
f" · {self.state.skills_count} skill{'s' if self.state.skills_count != 1 else ''}" f" · {self.state.skills_count} skill{'s' if self.state.skills_count != 1 else ''}"
) )
def _format_plan(self) -> str:
return (
""
if self.state.plan_description is None
else f" · {self.state.plan_description}"
)

View file

@ -113,9 +113,10 @@ class ChatTextArea(TextArea):
self.get_full_text(), self._get_full_cursor_offset() self.get_full_text(), self._get_full_cursor_offset()
) )
def _reset_prefix(self) -> None: def _reset_prefix(self, *, clear_last_used: bool = True) -> None:
self._history_prefix = None self._history_prefix = None
self._last_used_prefix = None if clear_last_used:
self._last_used_prefix = None
def _mark_cursor_moved_if_needed(self) -> None: def _mark_cursor_moved_if_needed(self) -> None:
if ( if (
@ -124,7 +125,7 @@ class ChatTextArea(TextArea):
and self.cursor_location != self._cursor_pos_after_load and self.cursor_location != self._cursor_pos_after_load
): ):
self._cursor_moved_since_load = True self._cursor_moved_since_load = True
self._reset_prefix() self._reset_prefix(clear_last_used=False)
def _get_prefix_up_to_cursor(self) -> str: def _get_prefix_up_to_cursor(self) -> str:
cursor_row, cursor_col = self.cursor_location cursor_row, cursor_col = self.cursor_location
@ -137,8 +138,17 @@ class ChatTextArea(TextArea):
return "" return ""
def _handle_history_up(self) -> bool: def _handle_history_up(self) -> bool:
cursor_row, cursor_col = self.cursor_location _, cursor_col = self.cursor_location
if cursor_row == 0: history_loaded_and_cursor_unmoved = (
self._cursor_pos_after_load is not None
and not self._cursor_moved_since_load
)
should_intercept = (
self.navigator.is_first_wrapped_line(self.cursor_location)
or history_loaded_and_cursor_unmoved
)
if should_intercept:
if self._history_prefix is not None and cursor_col != self._last_cursor_col: if self._history_prefix is not None and cursor_col != self._last_cursor_col:
self._reset_prefix() self._reset_prefix()
self._last_cursor_col = 0 self._last_cursor_col = 0
@ -151,26 +161,45 @@ class ChatTextArea(TextArea):
return True return True
return False return False
def _handle_history_down(self) -> bool: def _is_on_loaded_history_entry(self) -> bool:
cursor_row, cursor_col = self.cursor_location return self._cursor_pos_after_load is not None
total_lines = self.text.count("\n") + 1
on_first_line_unmoved = cursor_row == 0 and not self._cursor_moved_since_load def _has_history_prefix(self) -> bool:
on_last_line = cursor_row == total_lines - 1 return self._history_prefix is not None
should_intercept = ( def _has_history_navigation_context(self) -> bool:
on_first_line_unmoved and self._history_prefix is not None return self._is_on_loaded_history_entry() or self._has_history_prefix()
) or on_last_line
if not should_intercept: def _should_intercept_history_down(self) -> bool:
history_loaded_and_cursor_unmoved = (
self._is_on_loaded_history_entry() and not self._cursor_moved_since_load
)
if history_loaded_and_cursor_unmoved and self._has_history_prefix():
return True
if not self.navigator.is_last_wrapped_line(self.cursor_location):
return False return False
return self._has_history_navigation_context()
def _handle_history_down(self) -> bool:
_, cursor_col = self.cursor_location
if not self._should_intercept_history_down():
return False
navigating_loaded_history = self._is_on_loaded_history_entry()
if self._history_prefix is not None and cursor_col != self._last_cursor_col: if self._history_prefix is not None and cursor_col != self._last_cursor_col:
self._reset_prefix() if not navigating_loaded_history:
self._reset_prefix()
self._last_cursor_col = 0 self._last_cursor_col = 0
if self._history_prefix is None: if self._history_prefix is None:
self._history_prefix = self._get_prefix_up_to_cursor() if navigating_loaded_history and self._last_used_prefix is not None:
self._history_prefix = self._last_used_prefix
else:
self._history_prefix = self._get_prefix_up_to_cursor()
self._navigating_history = True self._navigating_history = True
self.post_message(self.HistoryNext(self._history_prefix)) self.post_message(self.HistoryNext(self._history_prefix))
@ -238,6 +267,11 @@ class ChatTextArea(TextArea):
event.stop() event.stop()
return return
# Work around VS Code 1.110+ sending space as CSI u (\x1b[32u),
# which Textual parses as Key("space", character=None, is_printable=False).
if event.key == "space" and event.character is None:
event.character = " "
await super()._on_key(event) await super()._on_key(event)
self._mark_cursor_moved_if_needed() self._mark_cursor_moved_if_needed()

View file

@ -6,11 +6,12 @@ from typing import TYPE_CHECKING, ClassVar
from textual import events from textual import events
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.binding import Binding, BindingType from textual.binding import Binding, BindingType
from textual.containers import Container, Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical, VerticalScroll
from textual.message import Message from textual.message import Message
from textual.reactive import reactive from textual.reactive import reactive
from textual.widgets import Input from textual.widgets import Input
from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
if TYPE_CHECKING: if TYPE_CHECKING:
@ -108,7 +109,16 @@ class QuestionApp(Container):
) )
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
with Vertical(id="question-content"): if self.args.content_preview:
with VerticalScroll(classes="question-content-preview"):
yield AnsiMarkdown(
self.args.content_preview, classes="question-content-preview-text"
)
question_content_classes = (
"question-content-docked" if self.args.content_preview else ""
)
with Vertical(id="question-content", classes=question_content_classes):
if len(self.questions) > 1: if len(self.questions) > 1:
self.tabs_widget = NoMarkupStatic("", classes="question-tabs") self.tabs_widget = NoMarkupStatic("", classes="question-tabs")
yield self.tabs_widget yield self.tabs_widget

View file

@ -30,7 +30,6 @@ from vibe.core.middleware import (
CHAT_AGENT_EXIT, CHAT_AGENT_EXIT,
CHAT_AGENT_REMINDER, CHAT_AGENT_REMINDER,
PLAN_AGENT_EXIT, PLAN_AGENT_EXIT,
PLAN_AGENT_REMINDER,
AutoCompactMiddleware, AutoCompactMiddleware,
ContextWarningMiddleware, ContextWarningMiddleware,
ConversationContext, ConversationContext,
@ -41,7 +40,9 @@ from vibe.core.middleware import (
ReadOnlyAgentMiddleware, ReadOnlyAgentMiddleware,
ResetReason, ResetReason,
TurnLimitMiddleware, TurnLimitMiddleware,
make_plan_agent_reminder,
) )
from vibe.core.plan_session import PlanSession
from vibe.core.prompts import UtilityPrompt from vibe.core.prompts import UtilityPrompt
from vibe.core.session.session_logger import SessionLogger from vibe.core.session.session_logger import SessionLogger
from vibe.core.session.session_migration import migrate_sessions_entrypoint from vibe.core.session.session_migration import migrate_sessions_entrypoint
@ -154,6 +155,7 @@ class AgentLoop:
self._base_config = config self._base_config = config
self._max_turns = max_turns self._max_turns = max_turns
self._max_price = max_price self._max_price = max_price
self._plan_session = PlanSession()
self.agent_manager = AgentManager( self.agent_manager = AgentManager(
lambda: self._base_config, initial_agent=agent_name lambda: self._base_config, initial_agent=agent_name
@ -343,20 +345,21 @@ class AgentLoop:
if self._max_price is not None: if self._max_price is not None:
self.middleware_pipeline.add(PriceLimitMiddleware(self._max_price)) self.middleware_pipeline.add(PriceLimitMiddleware(self._max_price))
if self.config.auto_compact_threshold > 0: active_model = self.config.get_active_model()
if active_model.auto_compact_threshold > 0:
self.middleware_pipeline.add( self.middleware_pipeline.add(
AutoCompactMiddleware(self.config.auto_compact_threshold) AutoCompactMiddleware(active_model.auto_compact_threshold)
) )
if self.config.context_warnings: if self.config.context_warnings:
self.middleware_pipeline.add( self.middleware_pipeline.add(
ContextWarningMiddleware(0.5, self.config.auto_compact_threshold) ContextWarningMiddleware(0.5, active_model.auto_compact_threshold)
) )
self.middleware_pipeline.add( self.middleware_pipeline.add(
ReadOnlyAgentMiddleware( ReadOnlyAgentMiddleware(
lambda: self.agent_profile, lambda: self.agent_profile,
BuiltinAgentName.PLAN, BuiltinAgentName.PLAN,
PLAN_AGENT_REMINDER, lambda: make_plan_agent_reminder(self._plan_session.plan_file_path_str),
PLAN_AGENT_EXIT, PLAN_AGENT_EXIT,
) )
) )
@ -391,7 +394,7 @@ class AgentLoop:
"old_tokens", self.stats.context_tokens "old_tokens", self.stats.context_tokens
) )
threshold = result.metadata.get( threshold = result.metadata.get(
"threshold", self.config.auto_compact_threshold "threshold", self.config.get_active_model().auto_compact_threshold
) )
tool_call_id = str(uuid4()) tool_call_id = str(uuid4())
@ -615,6 +618,8 @@ class AgentLoop:
approval_callback=self.approval_callback, approval_callback=self.approval_callback,
user_input_callback=self.user_input_callback, user_input_callback=self.user_input_callback,
sampling_callback=self._sampling_handler, sampling_callback=self._sampling_handler,
plan_file_path=self._plan_session.plan_file_path,
switch_agent_callback=self.switch_agent,
), ),
**tool_call.args_dict, **tool_call.args_dict,
): ):

View file

@ -8,7 +8,6 @@ from vibe.core.agents.models import (
DEFAULT, DEFAULT,
EXPLORE, EXPLORE,
PLAN, PLAN,
PLAN_AGENT_TOOLS,
AgentProfile, AgentProfile,
AgentSafety, AgentSafety,
AgentType, AgentType,
@ -22,7 +21,6 @@ __all__ = [
"DEFAULT", "DEFAULT",
"EXPLORE", "EXPLORE",
"PLAN", "PLAN",
"PLAN_AGENT_TOOLS",
"AgentManager", "AgentManager",
"AgentProfile", "AgentProfile",
"AgentSafety", "AgentSafety",

View file

@ -6,6 +6,8 @@ from pathlib import Path
import tomllib import tomllib
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from vibe.core.paths.global_paths import PLANS_DIR
if TYPE_CHECKING: if TYPE_CHECKING:
from vibe.core.config import VibeConfig from vibe.core.config import VibeConfig
@ -71,7 +73,17 @@ class AgentProfile:
CHAT_AGENT_TOOLS = ["grep", "read_file", "ask_user_question", "task"] CHAT_AGENT_TOOLS = ["grep", "read_file", "ask_user_question", "task"]
PLAN_AGENT_TOOLS = ["grep", "read_file", "todo", "ask_user_question", "task"]
def _plan_overrides() -> dict[str, Any]:
plans_pattern = str(PLANS_DIR.path / "*")
return {
"tools": {
"write_file": {"permission": "never", "allowlist": [plans_pattern]},
"search_replace": {"permission": "never", "allowlist": [plans_pattern]},
}
}
DEFAULT = AgentProfile( DEFAULT = AgentProfile(
BuiltinAgentName.DEFAULT, BuiltinAgentName.DEFAULT,
@ -84,7 +96,7 @@ PLAN = AgentProfile(
"Plan", "Plan",
"Read-only agent for exploration and planning", "Read-only agent for exploration and planning",
AgentSafety.SAFE, AgentSafety.SAFE,
overrides={"auto_approve": True, "enabled_tools": PLAN_AGENT_TOOLS}, overrides=_plan_overrides(),
) )
CHAT = AgentProfile( CHAT = AgentProfile(
BuiltinAgentName.CHAT, BuiltinAgentName.CHAT,

View file

@ -71,16 +71,6 @@ class MissingPromptFileError(RuntimeError):
self.prompt_dir = prompt_dir self.prompt_dir = prompt_dir
class WrongBackendError(RuntimeError):
def __init__(self, backend: Backend, is_mistral_api: bool) -> None:
super().__init__(
f"Wrong backend '{backend}' for {'' if is_mistral_api else 'non-'}"
f"mistral API. Use '{Backend.MISTRAL}' for mistral API and '{Backend.GENERIC}' for others."
)
self.backend = backend
self.is_mistral_api = is_mistral_api
class TomlFileSettingsSource(PydanticBaseSettingsSource): class TomlFileSettingsSource(PydanticBaseSettingsSource):
def __init__(self, settings_cls: type[BaseSettings]) -> None: def __init__(self, settings_cls: type[BaseSettings]) -> None:
super().__init__(settings_cls) super().__init__(settings_cls)
@ -108,13 +98,10 @@ class TomlFileSettingsSource(PydanticBaseSettingsSource):
class ProjectContextConfig(BaseSettings): class ProjectContextConfig(BaseSettings):
max_chars: int = 40_000 model_config = SettingsConfigDict(extra="ignore")
default_commit_count: int = 5 default_commit_count: int = 5
max_doc_bytes: int = 32 * 1024 max_doc_bytes: int = 32 * 1024
truncation_buffer: int = 1_000
max_depth: int = 3
max_files: int = 1000
max_dirs_per_level: int = 20
timeout_seconds: float = 2.0 timeout_seconds: float = 2.0
@ -258,6 +245,7 @@ class ModelConfig(BaseModel):
input_price: float = 0.0 # Price per million input tokens input_price: float = 0.0 # Price per million input tokens
output_price: float = 0.0 # Price per million output tokens output_price: float = 0.0 # Price per million output tokens
thinking: Literal["off", "low", "medium", "high"] = "off" thinking: Literal["off", "low", "medium", "high"] = "off"
auto_compact_threshold: int = 200_000
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @classmethod
@ -317,7 +305,6 @@ class VibeConfig(BaseSettings):
autocopy_to_clipboard: bool = True autocopy_to_clipboard: bool = True
file_watcher_for_autocomplete: bool = False file_watcher_for_autocomplete: bool = False
displayed_workdir: str = "" displayed_workdir: str = ""
auto_compact_threshold: int = 200_000
context_warnings: bool = False context_warnings: bool = False
auto_approve: bool = False auto_approve: bool = False
enable_telemetry: bool = True enable_telemetry: bool = True
@ -497,27 +484,6 @@ class VibeConfig(BaseSettings):
pass pass
return self return self
@model_validator(mode="after")
def _check_api_backend_compatibility(self) -> VibeConfig:
try:
active_model = self.get_active_model()
provider = self.get_provider_for_model(active_model)
MISTRAL_API_BASES = [
"https://codestral.mistral.ai",
"https://api.mistral.ai",
]
is_mistral_api = any(
provider.api_base.startswith(api_base) for api_base in MISTRAL_API_BASES
)
if (is_mistral_api and provider.backend != Backend.MISTRAL) or (
not is_mistral_api and provider.backend != Backend.GENERIC
):
raise WrongBackendError(provider.backend, is_mistral_api)
except ValueError:
pass
return self
@field_validator("tool_paths", mode="before") @field_validator("tool_paths", mode="before")
@classmethod @classmethod
def _expand_tool_paths(cls, v: Any) -> list[Path]: def _expand_tool_paths(cls, v: Any) -> list[Path]:

View file

@ -10,6 +10,7 @@ import httpx
from vibe.core.llm.backend.anthropic import AnthropicAdapter from vibe.core.llm.backend.anthropic import AnthropicAdapter
from vibe.core.llm.backend.base import APIAdapter, PreparedRequest from vibe.core.llm.backend.base import APIAdapter, PreparedRequest
from vibe.core.llm.backend.reasoning_adapter import ReasoningAdapter
from vibe.core.llm.backend.vertex import VertexAnthropicAdapter from vibe.core.llm.backend.vertex import VertexAnthropicAdapter
from vibe.core.llm.exceptions import BackendErrorBuilder from vibe.core.llm.exceptions import BackendErrorBuilder
from vibe.core.llm.message_utils import merge_consecutive_user_messages from vibe.core.llm.message_utils import merge_consecutive_user_messages
@ -159,6 +160,7 @@ ADAPTERS: dict[str, APIAdapter] = {
"openai": OpenAIAdapter(), "openai": OpenAIAdapter(),
"anthropic": AnthropicAdapter(), "anthropic": AnthropicAdapter(),
"vertex-anthropic": VertexAnthropicAdapter(), "vertex-anthropic": VertexAnthropicAdapter(),
"reasoning": ReasoningAdapter(),
} }

View file

@ -0,0 +1,227 @@
from __future__ import annotations
from collections.abc import Sequence
import json
from typing import Any, ClassVar
from vibe.core.config import ProviderConfig
from vibe.core.llm.backend.base import APIAdapter, PreparedRequest
from vibe.core.llm.message_utils import merge_consecutive_user_messages
from vibe.core.types import (
AvailableTool,
FunctionCall,
LLMChunk,
LLMMessage,
LLMUsage,
Role,
StrToolChoice,
ToolCall,
)
class ReasoningAdapter(APIAdapter):
endpoint: ClassVar[str] = "/chat/completions"
def _convert_message(self, msg: LLMMessage) -> dict[str, Any]:
match msg.role:
case Role.system:
return {"role": "system", "content": msg.content or ""}
case Role.user:
return {"role": "user", "content": msg.content or ""}
case Role.assistant:
return self._convert_assistant_message(msg)
case Role.tool:
result: dict[str, Any] = {
"role": "tool",
"content": msg.content or "",
"tool_call_id": msg.tool_call_id,
}
if msg.name:
result["name"] = msg.name
return result
def _convert_assistant_message(self, msg: LLMMessage) -> dict[str, Any]:
result: dict[str, Any] = {"role": "assistant"}
if msg.reasoning_content:
content: list[dict[str, Any]] = [
{
"type": "thinking",
"thinking": [{"type": "text", "text": msg.reasoning_content}],
},
{"type": "text", "text": msg.content or ""},
]
result["content"] = content
else:
result["content"] = msg.content or ""
if msg.tool_calls:
result["tool_calls"] = [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.function.name or "",
"arguments": tc.function.arguments or "",
},
**({"index": tc.index} if tc.index is not None else {}),
}
for tc in msg.tool_calls
]
return result
def _build_payload(
self,
*,
model_name: str,
messages: list[dict[str, Any]],
temperature: float,
tools: list[AvailableTool] | None,
max_tokens: int | None,
tool_choice: StrToolChoice | AvailableTool | None,
thinking: str,
) -> dict[str, Any]:
payload: dict[str, Any] = {
"model": model_name,
"messages": messages,
"temperature": temperature,
}
if thinking != "off":
payload["reasoning_effort"] = thinking
if tools:
payload["tools"] = [tool.model_dump(exclude_none=True) for tool in tools]
if tool_choice:
payload["tool_choice"] = (
tool_choice
if isinstance(tool_choice, str)
else tool_choice.model_dump()
)
if max_tokens is not None:
payload["max_tokens"] = max_tokens
return payload
def prepare_request( # noqa: PLR0913
self,
*,
model_name: str,
messages: Sequence[LLMMessage],
temperature: float,
tools: list[AvailableTool] | None,
max_tokens: int | None,
tool_choice: StrToolChoice | AvailableTool | None,
enable_streaming: bool,
provider: ProviderConfig,
api_key: str | None = None,
thinking: str = "off",
) -> PreparedRequest:
merged_messages = merge_consecutive_user_messages(messages)
converted_messages = [self._convert_message(msg) for msg in merged_messages]
payload = self._build_payload(
model_name=model_name,
messages=converted_messages,
temperature=temperature,
tools=tools,
max_tokens=max_tokens,
tool_choice=tool_choice,
thinking=thinking,
)
if enable_streaming:
payload["stream"] = True
payload["stream_options"] = {
"include_usage": True,
"stream_tool_calls": True,
}
headers: dict[str, str] = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
return PreparedRequest(self.endpoint, headers, body)
@staticmethod
def _parse_content_blocks(
content: str | list[dict[str, Any]],
) -> tuple[str | None, str | None]:
if isinstance(content, str):
return content or None, None
text_parts: list[str] = []
thinking_parts: list[str] = []
for block in content:
block_type = block.get("type")
if block_type == "text":
text_parts.append(block.get("text", ""))
elif block_type == "thinking":
for inner in block.get("thinking", []):
if isinstance(inner, dict) and inner.get("type") == "text":
thinking_parts.append(inner.get("text", ""))
elif isinstance(inner, str):
thinking_parts.append(inner)
return ("".join(text_parts) or None, "".join(thinking_parts) or None)
@staticmethod
def _parse_tool_calls(
tool_calls: list[dict[str, Any]] | None,
) -> list[ToolCall] | None:
if not tool_calls:
return None
return [
ToolCall(
id=tc.get("id"),
index=tc.get("index"),
function=FunctionCall(
name=tc.get("function", {}).get("name"),
arguments=tc.get("function", {}).get("arguments", ""),
),
)
for tc in tool_calls
]
def _parse_message_dict(self, msg_dict: dict[str, Any]) -> LLMMessage:
content = msg_dict.get("content")
text_content: str | None = None
reasoning_content: str | None = None
if content is not None:
text_content, reasoning_content = self._parse_content_blocks(content)
return LLMMessage(
role=Role.assistant,
content=text_content,
reasoning_content=reasoning_content,
tool_calls=self._parse_tool_calls(msg_dict.get("tool_calls")),
)
def parse_response(
self, data: dict[str, Any], provider: ProviderConfig
) -> LLMChunk:
message: LLMMessage | None = None
if data.get("choices"):
choice = data["choices"][0]
if "message" in choice:
message = self._parse_message_dict(choice["message"])
elif "delta" in choice:
message = self._parse_message_dict(choice["delta"])
if message is None:
message = LLMMessage(role=Role.assistant, content="")
usage_data = data.get("usage") or {}
usage = LLMUsage(
prompt_tokens=usage_data.get("prompt_tokens", 0),
completion_tokens=usage_data.get("completion_tokens", 0),
)
return LLMChunk(message=message, usage=usage)

View file

@ -129,9 +129,20 @@ class ContextWarningMiddleware:
self.has_warned = False self.has_warned = False
PLAN_AGENT_REMINDER = f"""<{VIBE_WARNING_TAG}>Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits). Instead, you should: def make_plan_agent_reminder(plan_file_path: str) -> str:
1. Answer the user's query comprehensively return f"""<{VIBE_WARNING_TAG}>Plan mode is active. You MUST NOT make any edits (except to the plan file below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
2. When you're done researching, present your plan by giving the full plan and not doing further tool calls to return input to the user. Do NOT make any file changes or run any tools that modify the system state in any way until the user has confirmed the plan.</{VIBE_WARNING_TAG}>"""
## Plan File Info
Create or edit your plan at {plan_file_path} using the write_file and search_replace tools.
Build your plan incrementally by writing to or editing this file.
This is the only file you are allowed to edit. Make sure to create it early and edit as soon as you internally update your plan.
## Instructions
1. Research the user's query using read-only tools (grep, read_file, etc.)
2. If you are unsure about requirements or approach, use the ask_user_question tool to clarify before finalizing your plan
3. Write your plan to the plan file above
4. When your plan is complete, call the exit_plan_mode tool to request user approval and switch to implementation mode</{VIBE_WARNING_TAG}>"""
PLAN_AGENT_EXIT = f"""<{VIBE_WARNING_TAG}>Plan mode has ended. If you have a plan ready, you can now start executing it. If not, you can now use editing tools and make changes to the system.</{VIBE_WARNING_TAG}>""" PLAN_AGENT_EXIT = f"""<{VIBE_WARNING_TAG}>Plan mode has ended. If you have a plan ready, you can now start executing it. If not, you can now use editing tools and make changes to the system.</{VIBE_WARNING_TAG}>"""
@ -149,15 +160,19 @@ class ReadOnlyAgentMiddleware:
self, self,
profile_getter: Callable[[], AgentProfile], profile_getter: Callable[[], AgentProfile],
agent_name: str, agent_name: str,
reminder: str, reminder: str | Callable[[], str],
exit_message: str, exit_message: str,
) -> None: ) -> None:
self._profile_getter = profile_getter self._profile_getter = profile_getter
self._agent_name = agent_name self._agent_name = agent_name
self.reminder = reminder self._reminder = reminder
self.exit_message = exit_message self.exit_message = exit_message
self._was_active = False self._was_active = False
@property
def reminder(self) -> str:
return self._reminder() if callable(self._reminder) else self._reminder
def _is_active(self) -> bool: def _is_active(self) -> bool:
return self._profile_getter().name == self._agent_name return self._profile_getter().name == self._agent_name

View file

@ -36,5 +36,6 @@ SESSION_LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs" / "session")
TRUSTED_FOLDERS_FILE = GlobalPath(lambda: VIBE_HOME.path / "trusted_folders.toml") TRUSTED_FOLDERS_FILE = GlobalPath(lambda: VIBE_HOME.path / "trusted_folders.toml")
LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs") LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs")
LOG_FILE = GlobalPath(lambda: VIBE_HOME.path / "logs" / "vibe.log") LOG_FILE = GlobalPath(lambda: VIBE_HOME.path / "logs" / "vibe.log")
PLANS_DIR = GlobalPath(lambda: VIBE_HOME.path / "plans")
DEFAULT_TOOL_DIR = GlobalPath(lambda: VIBE_ROOT / "core" / "tools" / "builtins") DEFAULT_TOOL_DIR = GlobalPath(lambda: VIBE_ROOT / "core" / "tools" / "builtins")

24
vibe/core/plan_session.py Normal file
View file

@ -0,0 +1,24 @@
from __future__ import annotations
from pathlib import Path
import time
from vibe.core.paths.global_paths import PLANS_DIR
from vibe.core.slug import create_slug
class PlanSession:
def __init__(self) -> None:
self._plan_file_path: Path | None = None
@property
def plan_file_path(self) -> Path:
if self._plan_file_path is None:
slug = create_slug()
timestamp = int(time.time())
self._plan_file_path = PLANS_DIR.path / f"{timestamp}-{slug}.md"
return self._plan_file_path
@property
def plan_file_path_str(self) -> str:
return str(self.plan_file_path)

View file

@ -1,7 +1,3 @@
directoryStructure: Below is a snapshot of this project's file structure at the start of the conversation. This snapshot will NOT update during the conversation. It skips over .gitignore patterns.{large_repo_warning}
{structure}
Absolute path: {abs_path} Absolute path: {abs_path}
gitStatus: This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation. gitStatus: This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.

View file

@ -181,7 +181,7 @@ class SessionLoader:
raise ValueError(f"Session metadata not found at {session_dir}") raise ValueError(f"Session metadata not found at {session_dir}")
try: try:
metadata_content = metadata_path.read_text() metadata_content = metadata_path.read_text("utf-8", errors="ignore")
return SessionMetadata.model_validate_json(metadata_content) return SessionMetadata.model_validate_json(metadata_content)
except ValueError: except ValueError:
raise raise

113
vibe/core/slug.py Normal file
View file

@ -0,0 +1,113 @@
from __future__ import annotations
import random
_ADJECTIVES = [
"bold",
"brave",
"bright",
"calm",
"clever",
"cool",
"cosmic",
"crisp",
"curious",
"daring",
"eager",
"fair",
"fierce",
"gentle",
"golden",
"grand",
"happy",
"keen",
"kind",
"lively",
"lucky",
"merry",
"mighty",
"noble",
"proud",
"quick",
"quiet",
"rapid",
"rustic",
"serene",
"sharp",
"shiny",
"silent",
"smooth",
"snowy",
"steady",
"swift",
"tiny",
"vivid",
"warm",
"wild",
"wise",
"witty",
"zesty",
]
_NOUNS = [
"arch",
"bloom",
"breeze",
"brook",
"cabin",
"canyon",
"cedar",
"cliff",
"cloud",
"comet",
"coral",
"crane",
"creek",
"dawn",
"dune",
"ember",
"falcon",
"fern",
"flame",
"flint",
"forest",
"frost",
"glen",
"grove",
"harbor",
"hawk",
"lake",
"lark",
"maple",
"marsh",
"meadow",
"mesa",
"mist",
"moon",
"oak",
"orbit",
"peak",
"pine",
"pixel",
"pond",
"reef",
"ridge",
"river",
"rocket",
"sage",
"shore",
"spark",
"stone",
"storm",
"trail",
"vale",
"wave",
"willow",
"wolf",
]
def create_slug() -> str:
adj1, adj2 = random.sample(_ADJECTIVES, 2)
noun = random.choice(_NOUNS)
return f"{adj1}-{adj2}-{noun}"

View file

@ -1,13 +1,10 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Generator
import fnmatch
import html import html
import os import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import sys import sys
import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from vibe.core.prompts import UtilityPrompt from vibe.core.prompts import UtilityPrompt
@ -20,6 +17,8 @@ if TYPE_CHECKING:
from vibe.core.skills.manager import SkillManager from vibe.core.skills.manager import SkillManager
from vibe.core.tools.manager import ToolManager from vibe.core.tools.manager import ToolManager
_git_status_cache: dict[Path, str] = {}
def _load_project_doc(workdir: Path, max_bytes: int) -> str: def _load_project_doc(workdir: Path, max_bytes: int) -> str:
if not trusted_folders_manager.is_trusted(workdir): if not trusted_folders_manager.is_trusted(workdir):
@ -39,162 +38,16 @@ class ProjectContextProvider:
) -> None: ) -> None:
self.root_path = Path(root_path).resolve() self.root_path = Path(root_path).resolve()
self.config = config self.config = config
self.gitignore_patterns = self._load_gitignore_patterns()
self._file_count = 0
self._start_time = 0.0
def _load_gitignore_patterns(self) -> list[str]:
gitignore_path = self.root_path / ".gitignore"
patterns = []
if gitignore_path.exists():
try:
patterns.extend(
line.strip()
for line in gitignore_path.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.startswith("#")
)
except Exception as e:
print(f"Warning: Could not read .gitignore: {e}", file=sys.stderr)
default_patterns = [
".git",
".git/*",
"*.pyc",
"__pycache__",
"node_modules",
"node_modules/*",
".env",
".DS_Store",
"*.log",
".vscode/settings.json",
".idea/*",
"dist",
"build",
"target",
".next",
".nuxt",
"coverage",
".nyc_output",
"*.egg-info",
".pytest_cache",
".tox",
"vendor",
"third_party",
"deps",
"*.min.js",
"*.min.css",
"*.bundle.js",
"*.chunk.js",
".cache",
"tmp",
"temp",
"logs",
]
return patterns + default_patterns
def _is_ignored(self, path: Path) -> bool:
try:
relative_path = path.relative_to(self.root_path)
path_str = str(relative_path)
for pattern in self.gitignore_patterns:
if pattern.endswith("/"):
if path.is_dir() and fnmatch.fnmatch(f"{path_str}/", pattern):
return True
elif fnmatch.fnmatch(path_str, pattern):
return True
elif "*" in pattern or "?" in pattern:
if fnmatch.fnmatch(path_str, pattern):
return True
return False
except (ValueError, OSError):
return True
def _should_stop(self) -> bool:
return (
self._file_count >= self.config.max_files
or (time.time() - self._start_time) > self.config.timeout_seconds
)
def _build_tree_structure_iterative(self) -> Generator[str]:
self._start_time = time.time()
self._file_count = 0
yield from self._process_directory(self.root_path, "", 0, is_root=True)
def _process_directory(
self, path: Path, prefix: str, depth: int, is_root: bool = False
) -> Generator[str]:
if depth > self.config.max_depth or self._should_stop():
return
try:
all_items = list(path.iterdir())
items = [item for item in all_items if not self._is_ignored(item)]
items.sort(key=lambda p: (not p.is_dir(), p.name.lower()))
show_truncation = len(items) > self.config.max_dirs_per_level
if show_truncation:
items = items[: self.config.max_dirs_per_level]
for i, item in enumerate(items):
if self._should_stop():
break
is_last = i == len(items) - 1 and not show_truncation
connector = "└── " if is_last else "├── "
name = f"{item.name}{'/' if item.is_dir() else ''}"
yield f"{prefix}{connector}{name}"
self._file_count += 1
if item.is_dir() and depth < self.config.max_depth:
child_prefix = prefix + (" " if is_last else "")
yield from self._process_directory(item, child_prefix, depth + 1)
if show_truncation and not self._should_stop():
remaining = len(all_items) - len(items)
yield f"{prefix}└── ... ({remaining} more items)"
except (PermissionError, OSError):
pass
def get_directory_structure(self) -> str:
lines = []
header = f"Directory structure of {self.root_path.name} (depth≤{self.config.max_depth}, max {self.config.max_files} items):\n"
try:
for line in self._build_tree_structure_iterative():
lines.append(line)
current_text = header + "\n".join(lines)
if (
len(current_text)
> self.config.max_chars - self.config.truncation_buffer
):
break
except Exception as e:
lines.append(f"Error building structure: {e}")
structure = header + "\n".join(lines)
if self._file_count >= self.config.max_files:
structure += f"\n... (truncated at {self.config.max_files} files limit)"
elif (time.time() - self._start_time) > self.config.timeout_seconds:
structure += (
f"\n... (truncated due to {self.config.timeout_seconds}s timeout)"
)
elif len(structure) > self.config.max_chars:
structure += f"\n... (truncated at {self.config.max_chars} characters)"
return structure
def get_git_status(self) -> str: def get_git_status(self) -> str:
if self.root_path in _git_status_cache:
return _git_status_cache[self.root_path]
result = self._fetch_git_status()
_git_status_cache[self.root_path] = result
return result
def _fetch_git_status(self) -> str:
try: try:
timeout = min(self.config.timeout_seconds, 10.0) timeout = min(self.config.timeout_seconds, 10.0)
num_commits = self.config.default_commit_count num_commits = self.config.default_commit_count
@ -294,23 +147,10 @@ class ProjectContextProvider:
return f"Error getting git status: {e}" return f"Error getting git status: {e}"
def get_full_context(self) -> str: def get_full_context(self) -> str:
structure = self.get_directory_structure()
git_status = self.get_git_status() git_status = self.get_git_status()
large_repo_warning = ""
if len(structure) >= self.config.max_chars - self.config.truncation_buffer:
large_repo_warning = (
f" Large repository detected - showing summary view with depth limit {self.config.max_depth}. "
f"Use the LS tool (passing a specific path), Bash tool, and other tools to explore nested directories in detail."
)
template = UtilityPrompt.PROJECT_CONTEXT.read() template = UtilityPrompt.PROJECT_CONTEXT.read()
return template.format( return template.format(abs_path=self.root_path, git_status=git_status)
large_repo_warning=large_repo_warning,
structure=structure,
abs_path=self.root_path,
git_status=git_status,
)
def _get_platform_name() -> str: def _get_platform_name() -> str:

View file

@ -28,7 +28,12 @@ from vibe.core.types import ToolStreamEvent
if TYPE_CHECKING: if TYPE_CHECKING:
from vibe.core.agents.manager import AgentManager from vibe.core.agents.manager import AgentManager
from vibe.core.tools.mcp_sampling import MCPSamplingHandler from vibe.core.tools.mcp_sampling import MCPSamplingHandler
from vibe.core.types import ApprovalCallback, EntrypointMetadata, UserInputCallback from vibe.core.types import (
ApprovalCallback,
EntrypointMetadata,
SwitchAgentCallback,
UserInputCallback,
)
ARGS_COUNT = 4 ARGS_COUNT = 4
@ -44,6 +49,8 @@ class InvokeContext:
sampling_callback: MCPSamplingHandler | None = field(default=None) sampling_callback: MCPSamplingHandler | None = field(default=None)
session_dir: Path | None = field(default=None) session_dir: Path | None = field(default=None)
entrypoint_metadata: EntrypointMetadata | None = field(default=None) entrypoint_metadata: EntrypointMetadata | None = field(default=None)
plan_file_path: Path | None = field(default=None)
switch_agent_callback: SwitchAgentCallback | None = field(default=None)
class ToolError(Exception): class ToolError(Exception):

View file

@ -50,6 +50,10 @@ class AskUserQuestionArgs(BaseModel):
min_length=1, min_length=1,
max_length=4, max_length=4,
) )
content_preview: str | None = Field(
default=None,
description="Optional text content to display in a scrollable area above the questions.",
)
class Answer(BaseModel): class Answer(BaseModel):

View file

@ -0,0 +1,145 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from typing import ClassVar, cast
from pydantic import BaseModel
from vibe.core.agents.models import BuiltinAgentName
from vibe.core.tools.base import (
BaseTool,
BaseToolConfig,
BaseToolState,
InvokeContext,
ToolError,
ToolPermission,
)
from vibe.core.tools.builtins.ask_user_question import (
AskUserQuestionArgs,
AskUserQuestionResult,
Choice,
Question,
)
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
class ExitPlanModeArgs(BaseModel):
pass
class ExitPlanModeResult(BaseModel):
switched: bool
message: str
class ExitPlanModeConfig(BaseToolConfig):
permission: ToolPermission = ToolPermission.ALWAYS
class ExitPlanMode(
BaseTool[ExitPlanModeArgs, ExitPlanModeResult, ExitPlanModeConfig, BaseToolState],
ToolUIData[ExitPlanModeArgs, ExitPlanModeResult],
):
description: ClassVar[str] = (
"Signal that your plan is complete and you are ready to start implementing. "
"This will ask the user to confirm switching from plan mode to accept-edits mode. "
"Only use this tool when you have finished writing your plan to the plan file "
"and are ready for user approval to begin implementation."
)
@classmethod
def format_call_display(cls, args: ExitPlanModeArgs) -> ToolCallDisplay:
return ToolCallDisplay(summary="Ready to exit plan mode")
@classmethod
def format_result_display(cls, result: ExitPlanModeResult) -> ToolResultDisplay:
return ToolResultDisplay(success=result.switched, message=result.message)
@classmethod
def get_status_text(cls) -> str:
return "Waiting for user confirmation"
async def run(
self, args: ExitPlanModeArgs, ctx: InvokeContext | None = None
) -> AsyncGenerator[ExitPlanModeResult, None]:
if ctx is None or ctx.agent_manager is None:
raise ToolError("ExitPlanMode requires an agent manager context.")
if ctx.agent_manager.active_profile.name != BuiltinAgentName.PLAN:
raise ToolError("ExitPlanMode can only be used in plan mode.")
if ctx.user_input_callback is None:
raise ToolError("ExitPlanMode requires an interactive UI.")
plan_content: str | None = None
if ctx.plan_file_path and ctx.plan_file_path.is_file():
try:
plan_content = ctx.plan_file_path.read_text()
except OSError as e:
raise ToolError(
f"Failed to read plan file at {ctx.plan_file_path}: {e}"
) from e
confirmation = AskUserQuestionArgs(
questions=[
Question(
question="Plan is complete. Switch to accept-edits mode and start implementing?",
header="Plan ready",
options=[
Choice(
label="Yes, and auto approve edits",
description="Switch to accept-edits mode with auto-approve permissions",
),
Choice(
label="Yes, and request approval for edits",
description="Switch to default agent mode (manual approval for edits)",
),
Choice(
label="No",
description="Stay in plan mode and continue planning",
),
],
)
],
content_preview=plan_content,
)
result = await ctx.user_input_callback(confirmation)
result = cast(AskUserQuestionResult, result)
if result.cancelled or not result.answers:
yield ExitPlanModeResult(
switched=False, message="User cancelled. Staying in plan mode."
)
return
answer = result.answers[0]
answer_lower = answer.answer.lower()
if answer_lower == "yes, and auto approve edits":
if ctx.switch_agent_callback:
await ctx.switch_agent_callback(BuiltinAgentName.ACCEPT_EDITS)
else:
ctx.agent_manager.switch_profile(BuiltinAgentName.ACCEPT_EDITS)
yield ExitPlanModeResult(
switched=True,
message="Switched to accept-edits mode. You can now start implementing the plan.",
)
elif answer_lower == "yes, and request approval for edits":
if ctx.switch_agent_callback:
await ctx.switch_agent_callback(BuiltinAgentName.DEFAULT)
else:
ctx.agent_manager.switch_profile(BuiltinAgentName.DEFAULT)
yield ExitPlanModeResult(
switched=True,
message="Switched to default agent mode. Edits will require your approval.",
)
elif answer.is_other:
yield ExitPlanModeResult(
switched=False,
message=f"Staying in plan mode. User feedback: {answer.answer}",
)
else:
yield ExitPlanModeResult(
switched=False,
message="Staying in plan mode. Continue refining the plan.",
)

View file

@ -62,6 +62,7 @@ class SearchReplaceResult(BaseModel):
lines_changed: int lines_changed: int
content: str content: str
warnings: list[str] = Field(default_factory=list) warnings: list[str] = Field(default_factory=list)
file_content_before: str
class SearchReplaceConfig(BaseToolConfig): class SearchReplaceConfig(BaseToolConfig):
@ -162,6 +163,7 @@ class SearchReplace(
lines_changed=lines_changed, lines_changed=lines_changed,
warnings=block_result.warnings, warnings=block_result.warnings,
content=args.content, content=args.content,
file_content_before=original_content,
) )
@final @final

View file

@ -33,6 +33,7 @@ class WriteFileResult(BaseModel):
bytes_written: int bytes_written: int
file_existed: bool file_existed: bool
content: str content: str
file_content_before: str | None = None
class WriteFileConfig(BaseToolConfig): class WriteFileConfig(BaseToolConfig):
@ -84,6 +85,14 @@ class WriteFile(
) -> AsyncGenerator[ToolStreamEvent | WriteFileResult, None]: ) -> AsyncGenerator[ToolStreamEvent | WriteFileResult, None]:
file_path, file_existed, content_bytes = self._prepare_and_validate_path(args) file_path, file_existed, content_bytes = self._prepare_and_validate_path(args)
file_content_before: str | None = None
if file_existed and args.overwrite:
try:
async with await anyio.Path(file_path).open(encoding="utf-8") as f:
file_content_before = await f.read(524_288) # 512kb
except Exception:
pass
await self._write_file(args, file_path) await self._write_file(args, file_path)
yield WriteFileResult( yield WriteFileResult(
@ -91,6 +100,7 @@ class WriteFile(
bytes_written=content_bytes, bytes_written=content_bytes,
file_existed=file_existed, file_existed=file_existed,
content=args.content, content=args.content,
file_content_before=file_content_before,
) )
def _prepare_and_validate_path(self, args: WriteFileArgs) -> tuple[Path, bool, int]: def _prepare_and_validate_path(self, args: WriteFileArgs) -> tuple[Path, bool, int]:

View file

@ -56,8 +56,24 @@ class ToolUIData[TArgs: BaseModel, TResult: BaseModel](ABC):
return cls.format_call_display(cast(TArgs, event.args)) return cls.format_call_display(cast(TArgs, event.args))
@classmethod @classmethod
@abstractmethod def format_result_display(cls, result: TResult) -> ToolResultDisplay:
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay: ... return ToolResultDisplay(success=True, message="Success")
@classmethod
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
if event.result is None:
return ToolResultDisplay(success=True, message="Success")
introspect = cast(
Callable[[], tuple[type, ...]] | None,
getattr(cls, "_get_tool_args_results", None),
)
if introspect is not None:
expected_type = introspect()[1]
if not isinstance(event.result, expected_type):
return ToolResultDisplay(success=True, message="Success")
return cls.format_result_display(cast(TResult, event.result))
@classmethod @classmethod
@abstractmethod @abstractmethod

View file

@ -415,6 +415,8 @@ type ApprovalCallback = AsyncApprovalCallback | SyncApprovalCallback
type UserInputCallback = Callable[[BaseModel], Awaitable[BaseModel]] type UserInputCallback = Callable[[BaseModel], Awaitable[BaseModel]]
type SwitchAgentCallback = Callable[[str], Awaitable[None]]
class MessageList(Sequence[LLMMessage]): class MessageList(Sequence[LLMMessage]):
def __init__( def __init__(

View file

@ -1,6 +1,4 @@
# What's new # What's new in v2.4.0
- **Plan mode**: Plan can be reviewed before switching to accept mode to apply edits.
- **Interactive resume**: Added a /resume command to choose which session to resume - **Pricing plan**: Your current plan appears in the CLI banner
- **Web Search & Web Fetch**: New tools to search the web and fetch content from URLs directly from your session. - **Per-model auto-compact**: Auto-compact threshold is configurable per model
- **MCP Sampling**: MCP servers can now request LLM completions through the sampling protocol.
- **Notification Indicator**: Terminal bell and window title update when Vibe needs your attention or completes a task.