Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com>
Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Pierre Rossinès 2026-04-16 11:10:26 +02:00 committed by GitHub
parent e1a25caa52
commit 95336528f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 3992 additions and 403 deletions

2
.vscode/launch.json vendored
View file

@ -1,5 +1,5 @@
{
"version": "2.7.5",
"version": "2.7.6",
"configurations": [
{
"name": "ACP Server",

View file

@ -154,3 +154,12 @@ guidelines:
- Use the workspace snippets: type the prefix (e.g. acpschema, acphelpers, vibetypes, vibeconfig) and accept the suggestion to insert the import line, then change the symbol name.
- Or ask Cursor: select the undefined symbol, then Cmd+K and request "Add the missing import for <symbol>".
- Or copy the import from an existing file in the repo (e.g. acp.schema, acp.helpers, vibe.core.*).
- title: "No Docstrings in Tests"
description: >
Do not add docstrings to test functions, test methods, or test classes.
Test names should be descriptive enough to convey intent (e.g.,
`test_create_user_returns_403_when_unauthorized`). Docstrings in tests add
noise, duplicate the function name, and can suppress pytest's default output
(pytest displays the docstring instead of the node id when one is present).
Use inline comments sparingly for non-obvious setup or assertions instead.

View file

@ -5,6 +5,31 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.7.6] - 2026-04-16
### Added
- `MergeStrategy` enum and merge logic for configuration
- `call_source=vibe_code` field in LLM request metadata
- "Other" task type for non-code requests in CLI prompt
### Changed
- Parallelized git subprocess calls during startup
- Extracted command registry and refactored skill resolution
- 1M context window and thinking budget max for opus
- Updated default telemetry URL to `api.mistral.ai`
### Fixed
- Markdown fence context loss causing streaming rendering problems
- Proxy chain URLs in `api_base` parsing
### Removed
- Alt+Left / Alt+Right key bindings from chat input
## [2.7.5] - 2026-04-14
### Changed

View file

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

View file

@ -1,6 +1,6 @@
[project]
name = "mistral-vibe"
version = "2.7.5"
version = "2.7.6"
description = "Minimal CLI coding agent by Mistral"
readme = "README.md"
requires-python = ">=3.12"
@ -40,7 +40,7 @@ dependencies = [
"keyring>=25.6.0",
"markdownify>=1.2.2",
"mcp>=1.14.0",
"mistralai==2.1.3",
"mistralai==2.3.2",
"opentelemetry-api>=1.39.1",
"opentelemetry-exporter-otlp-proto-http>=1.39.1",
"opentelemetry-sdk>=1.39.1",

279
tests/acp/test_commands.py Normal file
View file

@ -0,0 +1,279 @@
"""Tests for ACP slash command handlers on VibeAcpAgentLoop."""
from __future__ import annotations
import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, patch
from acp.schema import AgentMessageChunk, AvailableCommandsUpdate, TextContentBlock
import pytest
from tests.acp.conftest import _create_acp_agent
from tests.skills.conftest import create_skill
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.core.agent_loop import AgentLoop
from vibe.core.config import SessionLoggingConfig
def _get_client(agent: VibeAcpAgentLoop) -> FakeClient:
assert isinstance(agent.client, FakeClient)
return agent.client
def _get_message_texts(agent: VibeAcpAgentLoop) -> list[str]:
"""Extract text content from all AgentMessageChunk session updates."""
return [
u.update.content.text
for u in _get_client(agent)._session_updates
if isinstance(u.update, AgentMessageChunk)
]
async def _new_session_and_clear(agent: VibeAcpAgentLoop) -> str:
"""Create a new session, drain the startup updates, return session_id."""
resp = await agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
await asyncio.sleep(0) # let background tasks (available_commands) complete
_get_client(agent)._session_updates.clear()
return resp.session_id
async def _prompt(agent: VibeAcpAgentLoop, session_id: str, text: str):
return await agent.prompt(
prompt=[TextContentBlock(type="text", text=text)], session_id=session_id
)
def _make_patched_agent_loop(
backend: FakeBackend,
*,
skill_paths: list[Path] | None = None,
session_logging: SessionLoggingConfig | None = None,
) -> type[AgentLoop]:
"""Create a PatchedAgentLoop class that injects config overrides."""
config_updates: dict = {}
if skill_paths is not None:
config_updates["skill_paths"] = skill_paths
if session_logging is not None:
config_updates["session_logging"] = session_logging
class PatchedAgentLoop(AgentLoop):
def __init__(self, *args, **kwargs) -> None:
if config_updates and "config" in kwargs and kwargs["config"] is not None:
kwargs["config"] = kwargs["config"].model_copy(update=config_updates)
super().__init__(*args, **{**kwargs, "backend": backend})
return PatchedAgentLoop
@pytest.fixture
def acp_agent_loop(backend: FakeBackend) -> VibeAcpAgentLoop:
patched = _make_patched_agent_loop(backend)
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=patched).start()
return _create_acp_agent()
@pytest.fixture
def skills_dir(tmp_path: Path) -> Path:
d = tmp_path / "skills"
d.mkdir()
return d
@pytest.fixture
def acp_agent_loop_with_skills(
backend: FakeBackend, skills_dir: Path
) -> VibeAcpAgentLoop:
# Skills must exist in skills_dir BEFORE new_session() is called.
patched = _make_patched_agent_loop(backend, skill_paths=[skills_dir])
patch("vibe.acp.acp_agent_loop.AgentLoop", side_effect=patched).start()
return _create_acp_agent()
class TestHandleHelp:
@pytest.mark.asyncio
async def test_lists_all_registered_commands(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
response = await _prompt(acp_agent_loop, session_id, "/help")
assert response.stop_reason == "end_turn"
texts = _get_message_texts(acp_agent_loop)
assert len(texts) == 1
content = texts[0]
main_commands = ["help", "compact", "reload", "proxy-setup"]
for cmd in main_commands:
assert f"/{cmd}" in content
@pytest.mark.asyncio
async def test_includes_user_invocable_skills(
self, acp_agent_loop_with_skills: VibeAcpAgentLoop, skills_dir: Path
) -> None:
# Create skills before new_session so SkillManager discovers them
create_skill(skills_dir, "my-skill", "Does something useful")
create_skill(skills_dir, "hidden-skill", "Secret", user_invocable=False)
session_id = await _new_session_and_clear(acp_agent_loop_with_skills)
await _prompt(acp_agent_loop_with_skills, session_id, "/help")
content = _get_message_texts(acp_agent_loop_with_skills)[0]
assert "/my-skill" in content
assert "Does something useful" in content
assert "hidden-skill" not in content
class TestHandleCompact:
@pytest.mark.asyncio
async def test_empty_history_does_not_compact(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
session = acp_agent_loop.sessions[session_id]
with patch.object(
session.agent_loop, "compact", new_callable=AsyncMock
) as mock_compact:
await _prompt(acp_agent_loop, session_id, "/compact")
mock_compact.assert_not_called()
@pytest.mark.asyncio
async def test_compact_calls_agent_loop_compact(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
# Have a conversation first to create history
await _prompt(acp_agent_loop, session_id, "Hello, tell me something")
_get_client(acp_agent_loop)._session_updates.clear()
session = acp_agent_loop.sessions[session_id]
with patch.object(
session.agent_loop, "compact", new_callable=AsyncMock
) as mock_compact:
response = await _prompt(acp_agent_loop, session_id, "/compact")
assert response.stop_reason == "end_turn"
mock_compact.assert_called_once()
class TestHandleReload:
@pytest.mark.asyncio
async def test_reload_calls_reload_with_initial_messages(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
session = acp_agent_loop.sessions[session_id]
with patch.object(
session.agent_loop, "reload_with_initial_messages", new_callable=AsyncMock
) as mock_reload:
response = await _prompt(acp_agent_loop, session_id, "/reload")
assert response.stop_reason == "end_turn"
mock_reload.assert_called_once()
@pytest.mark.asyncio
async def test_reload_notifies_commands_changed(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
session = acp_agent_loop.sessions[session_id]
with patch.object(
session.command_registry, "notify_changed", new_callable=AsyncMock
) as mock_notify:
await _prompt(acp_agent_loop, session_id, "/reload")
mock_notify.assert_called_once()
class TestCommandFallthrough:
@pytest.mark.asyncio
async def test_unknown_slash_command_reaches_agent(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
response = await _prompt(acp_agent_loop, session_id, "/nonexistent")
# The agent loop should have processed it (FakeBackend returns "Hi")
assert response.stop_reason == "end_turn"
texts = _get_message_texts(acp_agent_loop)
# Should contain the LLM response, not a command reply
assert any("Hi" in t for t in texts)
@pytest.mark.asyncio
async def test_regular_message_reaches_agent(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
response = await _prompt(acp_agent_loop, session_id, "Hello world")
assert response.stop_reason == "end_turn"
texts = _get_message_texts(acp_agent_loop)
assert any("Hi" in t for t in texts)
class TestAvailableCommandsWithSkills:
@pytest.mark.asyncio
async def test_skills_appear_in_available_commands(
self, acp_agent_loop_with_skills: VibeAcpAgentLoop, skills_dir: Path
) -> None:
create_skill(skills_dir, "my-skill", "A useful skill")
await acp_agent_loop_with_skills.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
await asyncio.sleep(0)
updates = _get_client(acp_agent_loop_with_skills)._session_updates
available = [
u for u in updates if isinstance(u.update, AvailableCommandsUpdate)
]
assert len(available) == 1
cmd_names = [c.name for c in available[0].update.available_commands]
assert "my-skill" in cmd_names
# Built-in commands should also be present
assert "help" in cmd_names
@pytest.mark.asyncio
async def test_non_invocable_skills_excluded_from_available_commands(
self, acp_agent_loop_with_skills: VibeAcpAgentLoop, skills_dir: Path
) -> None:
create_skill(skills_dir, "visible-skill", "Visible")
create_skill(skills_dir, "hidden-skill", "Hidden", user_invocable=False)
await acp_agent_loop_with_skills.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
await asyncio.sleep(0)
updates = _get_client(acp_agent_loop_with_skills)._session_updates
available = [
u for u in updates if isinstance(u.update, AvailableCommandsUpdate)
]
cmd_names = [c.name for c in available[0].update.available_commands]
assert "visible-skill" in cmd_names
assert "hidden-skill" not in cmd_names
class TestCommandCaseInsensitivity:
@pytest.mark.asyncio
async def test_uppercase_command(self, acp_agent_loop: VibeAcpAgentLoop) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
response = await _prompt(acp_agent_loop, session_id, "/HELP")
assert response.stop_reason == "end_turn"
content = _get_message_texts(acp_agent_loop)[0]
assert "Available Commands" in content
@pytest.mark.asyncio
async def test_mixed_case_command(self, acp_agent_loop: VibeAcpAgentLoop) -> None:
session_id = await _new_session_and_clear(acp_agent_loop)
response = await _prompt(acp_agent_loop, session_id, "/Help")
assert response.stop_reason == "end_turn"
content = _get_message_texts(acp_agent_loop)[0]
assert "Available Commands" in content

View file

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

View file

@ -142,7 +142,9 @@ class TestMCPAppInit:
app = MCPApp(mcp_servers=servers, tool_manager=mgr)
app._viewing_server = "srv"
render_calls: list[str | None] = []
app._refresh_view = lambda server_name: render_calls.append(server_name)
app._refresh_view = lambda server_name, *, kind=None: render_calls.append(
server_name
)
app.action_back()
assert render_calls == [None]
@ -151,6 +153,8 @@ class TestMCPAppInit:
app = MCPApp(mcp_servers=[], tool_manager=mgr)
app._viewing_server = None
render_calls: list[str | None] = []
app._refresh_view = lambda server_name: render_calls.append(server_name)
app._refresh_view = lambda server_name, *, kind=None: render_calls.append(
server_name
)
app.action_back()
assert render_calls == []

View file

@ -278,6 +278,65 @@ class TestQuestionAppActions:
assert app._get_other_text(0) == "Text for Q1"
assert app._get_other_text(1) == ""
def test_switch_question_restores_cursor_to_answered_option(
self, multi_question_args
):
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(multi_question_args)
# Answer question 0 with MongoDB (index 1)
app.answers[0] = ("MongoDB", False)
# Switch away then back
app._switch_question(1)
app._switch_question(0)
assert app.selected_option == 1
def test_switch_question_restores_cursor_to_other(self, multi_question_args):
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(multi_question_args)
# Answer question 0 with Other
app.answers[0] = ("Custom DB", True)
app._switch_question(1)
app._switch_question(0)
assert app.selected_option == len(app.questions[0].options)
assert app._is_other_selected
def test_switch_question_defaults_to_zero_if_unanswered(self, multi_question_args):
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(multi_question_args)
app._switch_question(1)
assert app.selected_option == 0
def test_switch_question_restores_cursor_multi_select(self):
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
args = AskUserQuestionArgs(
questions=[
Question(
question="Q1?", options=[Choice(label="A"), Choice(label="B")]
),
Question(
question="Q2?",
options=[Choice(label="X"), Choice(label="Y"), Choice(label="Z")],
multi_select=True,
),
]
)
app = QuestionApp(args)
# Select Y (1) and Z (2) for question 1
app.multi_selections[1] = {1, 2}
app._switch_question(1)
assert app.selected_option == 1 # min of {1, 2}
class TestMultiSelectOtherBehavior:
def test_multi_select_other_does_not_advance_on_save(self, multi_select_args):
@ -489,6 +548,152 @@ class TestMultiSelectAutoSelect:
assert app._other_option_idx in app.multi_selections[0]
class TestNumberKeyShortcuts:
def test_number_key_selects_predefined_option(self, single_question_args):
from unittest.mock import patch
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(single_question_args)
app.selected_option = 0
# Press "2" -> should select MongoDB (index 1) and save
event = events.Key("2", "2")
with patch.object(app, "_advance_or_submit"):
app._handle_number_key(event)
assert app.selected_option == 1
assert 0 in app.answers
answer_text, is_other = app.answers[0]
assert answer_text == "MongoDB"
assert is_other is False
def test_number_key_first_option(self, single_question_args):
from unittest.mock import patch
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(single_question_args)
event = events.Key("1", "1")
with patch.object(app, "_advance_or_submit"):
app._handle_number_key(event)
assert app.selected_option == 0
assert 0 in app.answers
answer_text, _ = app.answers[0]
assert answer_text == "PostgreSQL"
def test_number_key_other_empty_focuses(self, single_question_args):
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(single_question_args)
# Other is option index 2 -> key "3"
event = events.Key("3", "3")
app._handle_number_key(event)
# Should navigate to Other but not validate (no text)
assert app.selected_option == 2
assert app._is_other_selected is True
assert 0 not in app.answers
def test_number_key_other_with_text_focuses_without_validating(
self, single_question_args
):
from unittest.mock import MagicMock
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(single_question_args)
app.other_texts[0] = "SQLite"
app.other_input = MagicMock()
app.other_input.has_focus = False
event = events.Key("3", "3")
app._handle_number_key(event)
# Should navigate to Other but not validate — just focus the input
assert app.selected_option == 2
assert app._is_other_selected is True
assert 0 not in app.answers
def test_number_key_out_of_range_ignored(self, single_question_args):
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(single_question_args)
# 3 options total (2 + Other), so "4" is out of range
event = events.Key("4", "4")
app._handle_number_key(event)
assert app.selected_option == 0
assert 0 not in app.answers
def test_number_key_zero_ignored(self, single_question_args):
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(single_question_args)
event = events.Key("0", "0")
app._handle_number_key(event)
assert app.selected_option == 0
assert 0 not in app.answers
def test_number_key_ignored_when_input_focused(self, single_question_args):
from unittest.mock import MagicMock
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(single_question_args)
app.other_input = MagicMock()
app.other_input.has_focus = True
event = events.Key("1", "1")
app._handle_number_key(event)
assert app.selected_option == 0
assert 0 not in app.answers
def test_number_key_multi_select_toggles(self, multi_select_args):
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(multi_select_args)
# Press "1" in multi-select -> should toggle Auth
event = events.Key("1", "1")
app._handle_number_key(event)
assert 0 in app.multi_selections.get(0, set())
def test_number_key_multi_select_submit_ignored(self, multi_select_args):
from textual import events
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
app = QuestionApp(multi_select_args)
# Submit is at index 4 -> key "5"
event = events.Key("5", "5")
app._handle_number_key(event)
# Should not navigate to submit
assert app.selected_option == 0
class TestMultiSelectSubmit:
def test_navigate_to_submit(self, multi_select_args):
from vibe.cli.textual_ui.widgets.question_app import QuestionApp

163
tests/core/test_merge.py Normal file
View file

@ -0,0 +1,163 @@
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from vibe.core.utils.merge import MergeConflictError, MergeStrategy
class TestMergeStrategyEnum:
def test_values_are_lowercase_strings(self) -> None:
assert MergeStrategy.REPLACE == "replace"
assert MergeStrategy.CONCAT == "concat"
assert MergeStrategy.UNION == "union"
assert MergeStrategy.MERGE == "merge"
assert MergeStrategy.CONFLICT == "conflict"
def test_all_strategies_exist(self) -> None:
assert len(MergeStrategy) == 5
def test_unknown_strategy_raises_not_implemented(self) -> None:
fake = MagicMock(spec=MergeStrategy)
with pytest.raises(NotImplementedError, match="not implemented"):
MergeStrategy.apply(fake, "a", "b")
class TestReplace:
def test_override_wins(self) -> None:
assert MergeStrategy.REPLACE.apply(1, 2) == 2
def test_base_returned_when_override_none(self) -> None:
assert MergeStrategy.REPLACE.apply(1, None) == 1
def test_both_none_returns_none(self) -> None:
assert MergeStrategy.REPLACE.apply(None, None) is None
def test_override_none_preserves_base_type(self) -> None:
assert MergeStrategy.REPLACE.apply([1, 2], None) == [1, 2]
assert MergeStrategy.REPLACE.apply({"a": 1}, None) == {"a": 1}
def test_override_can_be_falsy(self) -> None:
assert MergeStrategy.REPLACE.apply(1, 0) == 0
assert MergeStrategy.REPLACE.apply(1, "") == ""
assert MergeStrategy.REPLACE.apply(1, False) is False
class TestConcat:
def test_lists_concatenated(self) -> None:
assert MergeStrategy.CONCAT.apply([1, 2], [3, 4]) == [1, 2, 3, 4]
def test_base_none_returns_override(self) -> None:
assert MergeStrategy.CONCAT.apply(None, [1]) == [1]
def test_override_none_returns_base(self) -> None:
assert MergeStrategy.CONCAT.apply([1], None) == [1]
def test_both_none_returns_none(self) -> None:
assert MergeStrategy.CONCAT.apply(None, None) is None
def test_empty_lists(self) -> None:
assert MergeStrategy.CONCAT.apply([], []) == []
def test_raises_type_error_for_non_list(self) -> None:
with pytest.raises(TypeError, match="CONCAT requires list operands"):
MergeStrategy.CONCAT.apply("a", [1])
with pytest.raises(TypeError, match="CONCAT requires list operands"):
MergeStrategy.CONCAT.apply([1], "b")
class TestUnion:
def test_merge_by_key_override_wins(self) -> None:
base = [{"name": "a", "v": 1}, {"name": "b", "v": 2}]
override = [{"name": "b", "v": 99}]
result = MergeStrategy.UNION.apply(base, override, key_fn=lambda x: x["name"])
assert result == [{"name": "a", "v": 1}, {"name": "b", "v": 99}]
def test_preserves_order_base_first(self) -> None:
base = [{"name": "a"}, {"name": "b"}]
override = [{"name": "c"}]
result = MergeStrategy.UNION.apply(base, override, key_fn=lambda x: x["name"])
assert [x["name"] for x in result] == ["a", "b", "c"]
def test_new_keys_from_override_appended(self) -> None:
base = [{"name": "a"}]
override = [{"name": "b"}, {"name": "c"}]
result = MergeStrategy.UNION.apply(base, override, key_fn=lambda x: x["name"])
assert len(result) == 3
def test_base_none_returns_override(self) -> None:
assert MergeStrategy.UNION.apply(None, [1], key_fn=str) == [1]
def test_override_none_returns_base(self) -> None:
assert MergeStrategy.UNION.apply([1], None, key_fn=str) == [1]
def test_both_none_returns_none(self) -> None:
assert MergeStrategy.UNION.apply(None, None) is None
def test_raises_without_key_fn(self) -> None:
with pytest.raises(ValueError, match="UNION strategy requires key_fn"):
MergeStrategy.UNION.apply([1], [2])
def test_raises_type_error_for_non_list(self) -> None:
with pytest.raises(TypeError, match="UNION requires list operands"):
MergeStrategy.UNION.apply("a", [1], key_fn=str)
class TestMerge:
def test_dicts_merged_one_level(self) -> None:
result = MergeStrategy.MERGE.apply({"a": 1, "b": 2}, {"b": 3, "c": 4})
assert result == {"a": 1, "b": 3, "c": 4}
def test_nested_dicts_not_recursed(self) -> None:
base = {"a": {"x": 1, "y": 2}}
override = {"a": {"y": 3, "z": 4}}
result = MergeStrategy.MERGE.apply(base, override)
assert result == {"a": {"y": 3, "z": 4}}
def test_base_none_returns_override(self) -> None:
assert MergeStrategy.MERGE.apply(None, {"a": 1}) == {"a": 1}
def test_override_none_returns_base(self) -> None:
assert MergeStrategy.MERGE.apply({"a": 1}, None) == {"a": 1}
def test_both_none_returns_none(self) -> None:
assert MergeStrategy.MERGE.apply(None, None) is None
def test_raises_type_error_for_non_dict(self) -> None:
with pytest.raises(TypeError, match="MERGE requires dict operands"):
MergeStrategy.MERGE.apply([1], {"a": 1})
def test_does_not_mutate_inputs(self) -> None:
base = {"a": 1}
override = {"b": 2}
MergeStrategy.MERGE.apply(base, override)
assert base == {"a": 1}
assert override == {"b": 2}
class TestConflict:
def test_raises_when_both_provided(self) -> None:
with pytest.raises(MergeConflictError):
MergeStrategy.CONFLICT.apply(1, 2)
def test_base_only_returns_base(self) -> None:
assert MergeStrategy.CONFLICT.apply(1, None) == 1
def test_override_only_returns_override(self) -> None:
assert MergeStrategy.CONFLICT.apply(None, 2) == 2
def test_both_none_returns_none(self) -> None:
assert MergeStrategy.CONFLICT.apply(None, None) is None
class TestMergeConflictError:
def test_default_message(self) -> None:
err = MergeConflictError()
assert str(err) == "Merge conflict"
assert err.field_name == ""
def test_message_with_field_name(self) -> None:
err = MergeConflictError("active_model")
assert str(err) == "Merge conflict on field 'active_model'"
assert err.field_name == "active_model"

View file

@ -489,7 +489,7 @@ class TestTelemetryClient:
assert (
client._get_telemetry_url("not-a-valid-url")
== "https://codestral.mistral.ai/v1/datalake/events"
== "https://api.mistral.ai/v1/datalake/events"
)
def test_is_active_false_when_mistral_provider_exists_but_no_api_key(

View file

@ -518,3 +518,77 @@ class TestSkillUserInvocable:
assert skills["visible-skill"].user_invocable is True
assert skills["hidden-skill"].user_invocable is False
assert skills["default-skill"].user_invocable is True
class TestParseSkillCommand:
def test_plain_text_returns_none(self, skill_manager: SkillManager) -> None:
assert skill_manager.parse_skill_command("hello world") is None
def test_unknown_skill_returns_none(self, skill_manager: SkillManager) -> None:
assert skill_manager.parse_skill_command("/nonexistent") is None
def test_slash_only_returns_none(self, skill_manager: SkillManager) -> None:
assert skill_manager.parse_skill_command("/") is None
def test_parses_skill_without_args(
self, skills_dir: Path, skill_config: VibeConfig
) -> None:
create_skill(skills_dir, "my-skill", body="Do the thing.")
manager = SkillManager(lambda: skill_config)
parsed = manager.parse_skill_command("/my-skill")
assert parsed is not None
assert parsed.name == "my-skill"
assert "Do the thing." in parsed.content
assert parsed.extra_instructions is None
def test_parses_skill_with_args(
self, skills_dir: Path, skill_config: VibeConfig
) -> None:
create_skill(skills_dir, "my-skill", body="Do the thing.")
manager = SkillManager(lambda: skill_config)
parsed = manager.parse_skill_command("/my-skill fix the bug")
assert parsed is not None
assert parsed.name == "my-skill"
assert parsed.extra_instructions == "fix the bug"
def test_case_insensitive(self, skills_dir: Path, skill_config: VibeConfig) -> None:
create_skill(skills_dir, "my-skill", body="Do the thing.")
manager = SkillManager(lambda: skill_config)
parsed = manager.parse_skill_command("/MY-SKILL")
assert parsed is not None
assert parsed.name == "my-skill"
def test_raises_on_unreadable_skill(
self, skills_dir: Path, skill_config: VibeConfig
) -> None:
create_skill(skills_dir, "bad-skill", body="content")
manager = SkillManager(lambda: skill_config)
(skills_dir / "bad-skill" / "SKILL.md").unlink()
with pytest.raises(OSError):
manager.parse_skill_command("/bad-skill")
class TestBuildSkillPrompt:
def test_without_args(self, skills_dir: Path, skill_config: VibeConfig) -> None:
create_skill(skills_dir, "my-skill", body="Do the thing.")
manager = SkillManager(lambda: skill_config)
parsed = manager.parse_skill_command("/my-skill")
assert parsed is not None
prompt = SkillManager.build_skill_prompt("/my-skill", parsed)
assert prompt == parsed.content
def test_with_args(self, skills_dir: Path, skill_config: VibeConfig) -> None:
create_skill(skills_dir, "my-skill", body="Do the thing.")
manager = SkillManager(lambda: skill_config)
text = "/my-skill fix the bug"
parsed = manager.parse_skill_command(text)
assert parsed is not None
prompt = SkillManager.build_skill_prompt(text, parsed)
assert prompt.startswith("/my-skill fix the bug")
assert "Do the thing." in prompt

View file

@ -39,6 +39,8 @@
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #9cafbd;font-weight: bold }
.terminal-r9 { fill: #868887 }
</style>
<defs>
@ -160,7 +162,7 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="733.5" width="427" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="463.6" y="733.5" width="963.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#608ab1" x="36.6" y="733.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="183" y="733.5" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="292.8" y="733.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="390.4" y="733.5" width="1037" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
@ -179,21 +181,21 @@
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="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="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;2&#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="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;2&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type&#160;</text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</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-r4" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r2" x="24.4" y="581.2" textLength="183" clip-path="url(#terminal-line-23)">/mcp&#160;filesystem</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r5" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="48.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">MCP&#160;servers&#160;opened...</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="183" clip-path="url(#terminal-line-22)">/mcp&#160;filesystem</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="48.8" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">MCP&#160;servers&#160;opened...</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="1464" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r6" x="24.4" y="703.2" textLength="134.2" clip-path="url(#terminal-line-28)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="36.6" y="752" textLength="427" clip-path="url(#terminal-line-30)">filesystem&#160;&#160;[stdio]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="366" clip-path="url(#terminal-line-31)">search&#160;&#160;[http]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="1464" clip-path="url(#terminal-line-26)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r6" x="24.4" y="678.8" textLength="134.2" clip-path="url(#terminal-line-27)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="207.4" clip-path="url(#terminal-line-29)">Local&#160;MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="36.6" y="752" textLength="146.4" clip-path="url(#terminal-line-30)">&#160;&#160;filesystem</text><text class="terminal-r8" x="183" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">&#160;&#160;[stdio]</text><text class="terminal-r8" x="292.8" y="752" textLength="97.6" clip-path="url(#terminal-line-30)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="146.4" clip-path="url(#terminal-line-31)">&#160;&#160;search&#160;&#160;&#160;&#160;</text><text class="terminal-r9" x="183" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">&#160;&#160;[http]&#160;</text><text class="terminal-r9" x="292.8" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="488" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Show&#160;tools&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -39,6 +39,8 @@
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #9cafbd;font-weight: bold }
.terminal-r9 { fill: #868887 }
</style>
<defs>
@ -160,7 +162,7 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="709.1" width="427" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="463.6" y="709.1" width="963.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#608ab1" x="36.6" y="709.1" width="183" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="219.6" y="709.1" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="329.4" y="709.1" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="427" y="709.1" width="1000.4" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
@ -178,22 +180,22 @@
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="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="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;2&#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="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="256.2" clip-path="url(#terminal-line-16)">&#160;·&#160;[Subscription]&#160;Pro</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;2&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type&#160;</text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</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-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="48.8" clip-path="url(#terminal-line-22)">/mcp</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="48.8" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">MCP&#160;servers&#160;opened...</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r4" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r2" x="24.4" y="532.4" textLength="48.8" clip-path="url(#terminal-line-21)">/mcp</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r5" x="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="48.8" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">MCP&#160;servers&#160;opened...</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="1464" clip-path="url(#terminal-line-26)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r6" x="24.4" y="678.8" textLength="134.2" clip-path="url(#terminal-line-27)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="427" clip-path="url(#terminal-line-29)">filesystem&#160;&#160;[stdio]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="36.6" y="752" textLength="427" clip-path="url(#terminal-line-30)">broken-server&#160;&#160;[stdio]&#160;&#160;unavailable</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="366" clip-path="url(#terminal-line-31)">search&#160;&#160;[http]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="630" textLength="1464" clip-path="url(#terminal-line-25)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r6" x="24.4" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r7" x="36.6" y="703.2" textLength="207.4" clip-path="url(#terminal-line-28)">Local&#160;MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="183" clip-path="url(#terminal-line-29)">&#160;&#160;filesystem&#160;&#160;&#160;</text><text class="terminal-r8" x="219.6" y="727.6" textLength="109.8" clip-path="url(#terminal-line-29)">&#160;&#160;[stdio]</text><text class="terminal-r8" x="329.4" y="727.6" textLength="97.6" clip-path="url(#terminal-line-29)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="36.6" y="752" textLength="183" clip-path="url(#terminal-line-30)">&#160;&#160;broken-server</text><text class="terminal-r9" x="219.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">&#160;&#160;[stdio]</text><text class="terminal-r9" x="329.4" y="752" textLength="122" clip-path="url(#terminal-line-30)">&#160;&#160;no&#160;tools</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="183" clip-path="url(#terminal-line-31)">&#160;&#160;search&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-r9" x="219.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">&#160;&#160;[http]&#160;</text><text class="terminal-r9" x="329.4" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="488" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Show&#160;tools&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -0,0 +1,206 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #9cafbd;font-weight: bold }
.terminal-r9 { fill: #868887 }
.terminal-r10 { fill: #98a84b }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">SnapshotTestAppWithConnectors</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="660.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="183" y="660.3" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="292.8" y="660.3" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="390.4" y="660.3" width="1037" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="361.6" textLength="183" clip-path="url(#terminal-line-14)">devstral-latest</text><text class="terminal-r1" x="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="585.6" clip-path="url(#terminal-line-15)">1&#160;model&#160;·&#160;2&#160;connectors&#160;·&#160;1&#160;MCP&#160;server&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type&#160;</text><text class="terminal-r3" x="231.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="292.8" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r4" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r2" x="24.4" y="483.6" textLength="48.8" clip-path="url(#terminal-line-19)">/mcp</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r5" x="24.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="48.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">MCP&#160;servers&#160;opened...</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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="0" y="581.2" textLength="1464" 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-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r6" x="24.4" y="605.6" textLength="292.8" clip-path="url(#terminal-line-24)">MCP&#160;Servers&#160;&amp;&#160;Connectors</text><text class="terminal-r5" x="1451.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r5" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r5" x="1451.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r7" x="36.6" y="654.4" textLength="207.4" clip-path="url(#terminal-line-26)">Local&#160;MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r7" x="36.6" y="678.8" textLength="146.4" clip-path="url(#terminal-line-27)">&#160;&#160;filesystem</text><text class="terminal-r8" x="183" y="678.8" textLength="109.8" clip-path="url(#terminal-line-27)">&#160;&#160;[stdio]</text><text class="terminal-r8" x="292.8" y="678.8" textLength="97.6" clip-path="url(#terminal-line-27)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="244" clip-path="url(#terminal-line-29)">Workspace&#160;Connectors</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="36.6" y="752" textLength="85.4" clip-path="url(#terminal-line-30)">&#160;&#160;gmail</text><text class="terminal-r9" x="122" y="752" textLength="158.6" clip-path="url(#terminal-line-30)">&#160;&#160;[connector]</text><text class="terminal-r9" x="280.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">&#160;&#160;3&#160;tools</text><text class="terminal-r10" x="414.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r9" x="427" y="752" textLength="122" clip-path="url(#terminal-line-30)">&#160;connected</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">&#160;&#160;slack</text><text class="terminal-r9" x="122" y="776.4" textLength="158.6" clip-path="url(#terminal-line-31)">&#160;&#160;[connector]</text><text class="terminal-r9" x="280.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">&#160;&#160;2&#160;tools</text><text class="terminal-r10" x="414.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r9" x="427" y="776.4" textLength="122" clip-path="url(#terminal-line-31)">&#160;connected</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="488" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Show&#160;tools&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,207 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #9cafbd;font-weight: bold }
.terminal-r9 { fill: #98a84b;font-weight: bold }
.terminal-r10 { fill: #868887 }
.terminal-r11 { fill: #98a84b }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">SnapshotTestAppConnectorsOnly</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="733.5" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="122" y="733.5" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="280.6" y="733.5" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="390.4" y="733.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="414.8" y="733.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="427" y="733.5" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="549" y="733.5" width="878.4" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="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="597.8" clip-path="url(#terminal-line-18)">1&#160;model&#160;·&#160;2&#160;connectors&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type&#160;</text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="48.8" clip-path="url(#terminal-line-22)">/mcp</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="48.8" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">MCP&#160;servers&#160;opened...</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="1464" clip-path="url(#terminal-line-26)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r6" x="24.4" y="678.8" textLength="292.8" clip-path="url(#terminal-line-27)">MCP&#160;Servers&#160;&amp;&#160;Connectors</text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="244" clip-path="url(#terminal-line-29)">Workspace&#160;Connectors</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="36.6" y="752" textLength="85.4" clip-path="url(#terminal-line-30)">&#160;&#160;gmail</text><text class="terminal-r8" x="122" y="752" textLength="158.6" clip-path="url(#terminal-line-30)">&#160;&#160;[connector]</text><text class="terminal-r8" x="280.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">&#160;&#160;3&#160;tools</text><text class="terminal-r9" x="414.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r8" x="427" y="752" textLength="122" clip-path="url(#terminal-line-30)">&#160;connected</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">&#160;&#160;slack</text><text class="terminal-r10" x="122" y="776.4" textLength="158.6" clip-path="url(#terminal-line-31)">&#160;&#160;[connector]</text><text class="terminal-r10" x="280.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">&#160;&#160;2&#160;tools</text><text class="terminal-r11" x="414.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r10" x="427" y="776.4" textLength="122" clip-path="url(#terminal-line-31)">&#160;connected</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="488" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Show&#160;tools&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,203 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">SnapshotTestAppWithConnectors</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="733.5" width="183" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="219.6" y="733.5" width="317.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="536.8" y="733.5" width="890.6" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="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="585.6" clip-path="url(#terminal-line-19)">1&#160;model&#160;·&#160;2&#160;connectors&#160;·&#160;1&#160;MCP&#160;server&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type&#160;</text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r4" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r2" x="24.4" y="581.2" textLength="48.8" clip-path="url(#terminal-line-23)">/mcp</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r5" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="48.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">MCP&#160;servers&#160;opened...</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="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="1464" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r6" x="24.4" y="703.2" textLength="195.2" clip-path="url(#terminal-line-28)">Connector:&#160;slack</text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="36.6" y="752" textLength="183" clip-path="url(#terminal-line-30)">search_messages</text><text class="terminal-r7" x="219.6" y="752" textLength="317.2" clip-path="url(#terminal-line-30)">&#160;&#160;-&#160;&#160;Search&#160;Slack&#160;messages</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r7" x="36.6" y="776.4" textLength="146.4" clip-path="url(#terminal-line-31)">send_message</text><text class="terminal-r1" x="183" y="776.4" textLength="305" clip-path="url(#terminal-line-31)">&#160;&#160;-&#160;&#160;Send&#160;a&#160;Slack&#160;message</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="463.6" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Backspace&#160;Back&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -39,6 +39,8 @@
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #9cafbd;font-weight: bold }
.terminal-r9 { fill: #868887 }
</style>
<defs>
@ -160,7 +162,7 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="733.5" width="427" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="463.6" y="733.5" width="963.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#608ab1" x="36.6" y="733.5" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="183" y="733.5" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="292.8" y="733.5" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="390.4" y="733.5" width="1037" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
@ -179,21 +181,21 @@
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="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="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;2&#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="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;2&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type&#160;</text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</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-r4" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r2" x="24.4" y="581.2" textLength="48.8" clip-path="url(#terminal-line-23)">/mcp</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r5" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="48.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">MCP&#160;servers&#160;opened...</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="48.8" clip-path="url(#terminal-line-22)">/mcp</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="48.8" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">MCP&#160;servers&#160;opened...</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="1464" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r6" x="24.4" y="703.2" textLength="134.2" clip-path="url(#terminal-line-28)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="36.6" y="752" textLength="427" clip-path="url(#terminal-line-30)">filesystem&#160;&#160;[stdio]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="366" clip-path="url(#terminal-line-31)">search&#160;&#160;[http]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="1464" clip-path="url(#terminal-line-26)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r6" x="24.4" y="678.8" textLength="134.2" clip-path="url(#terminal-line-27)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="207.4" clip-path="url(#terminal-line-29)">Local&#160;MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="36.6" y="752" textLength="146.4" clip-path="url(#terminal-line-30)">&#160;&#160;filesystem</text><text class="terminal-r8" x="183" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">&#160;&#160;[stdio]</text><text class="terminal-r8" x="292.8" y="752" textLength="97.6" clip-path="url(#terminal-line-30)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="146.4" clip-path="url(#terminal-line-31)">&#160;&#160;search&#160;&#160;&#160;&#160;</text><text class="terminal-r9" x="183" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">&#160;&#160;[http]&#160;</text><text class="terminal-r9" x="292.8" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="488" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Show&#160;tools&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -39,6 +39,8 @@
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #868887 }
.terminal-r9 { fill: #9cafbd;font-weight: bold }
</style>
<defs>
@ -160,7 +162,7 @@
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="757.9" width="366" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="402.6" y="757.9" width="1024.8" height="24.65" shape-rendering="crispEdges"/>
<rect fill="#608ab1" x="36.6" y="757.9" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="183" y="757.9" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="292.8" y="757.9" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="390.4" y="757.9" width="1037" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
@ -179,21 +181,21 @@
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="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="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;2&#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="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;2&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type&#160;</text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</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-r4" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r2" x="24.4" y="581.2" textLength="48.8" clip-path="url(#terminal-line-23)">/mcp</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r5" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="48.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">MCP&#160;servers&#160;opened...</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r4" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="24.4" y="556.8" textLength="48.8" clip-path="url(#terminal-line-22)">/mcp</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="48.8" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">MCP&#160;servers&#160;opened...</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="1464" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r6" x="24.4" y="703.2" textLength="134.2" clip-path="url(#terminal-line-28)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="36.6" y="752" textLength="427" clip-path="url(#terminal-line-30)">filesystem&#160;&#160;[stdio]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r7" x="36.6" y="776.4" textLength="366" clip-path="url(#terminal-line-31)">search&#160;&#160;[http]&#160;&#160;1&#160;tool&#160;enabled</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="1464" clip-path="url(#terminal-line-26)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r6" x="24.4" y="678.8" textLength="134.2" clip-path="url(#terminal-line-27)">MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="207.4" clip-path="url(#terminal-line-29)">Local&#160;MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="36.6" y="752" textLength="146.4" clip-path="url(#terminal-line-30)">&#160;&#160;filesystem</text><text class="terminal-r8" x="183" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">&#160;&#160;[stdio]</text><text class="terminal-r8" x="292.8" y="752" textLength="97.6" clip-path="url(#terminal-line-30)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r7" x="36.6" y="776.4" textLength="146.4" clip-path="url(#terminal-line-31)">&#160;&#160;search&#160;&#160;&#160;&#160;</text><text class="terminal-r9" x="183" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">&#160;&#160;[http]&#160;</text><text class="terminal-r9" x="292.8" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="488" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Show&#160;tools&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -0,0 +1,206 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #ff8205 }
.terminal-r5 { fill: #9a9b99 }
.terminal-r6 { fill: #608ab1;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #9cafbd;font-weight: bold }
.terminal-r9 { fill: #868887 }
.terminal-r10 { fill: #98a84b }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">SnapshotTestAppWithConnectors</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#608ab1" x="36.6" y="660.3" width="146.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="183" y="660.3" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="292.8" y="660.3" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="390.4" y="660.3" width="1037" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="361.6" textLength="183" clip-path="url(#terminal-line-14)">devstral-latest</text><text class="terminal-r1" x="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="585.6" clip-path="url(#terminal-line-15)">1&#160;model&#160;·&#160;2&#160;connectors&#160;·&#160;1&#160;MCP&#160;server&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type&#160;</text><text class="terminal-r3" x="231.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="292.8" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r4" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r2" x="24.4" y="483.6" textLength="48.8" clip-path="url(#terminal-line-19)">/mcp</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r5" x="24.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="48.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">MCP&#160;servers&#160;opened...</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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r5" x="0" y="581.2" textLength="1464" 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-r5" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r6" x="24.4" y="605.6" textLength="292.8" clip-path="url(#terminal-line-24)">MCP&#160;Servers&#160;&amp;&#160;Connectors</text><text class="terminal-r5" x="1451.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r5" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r5" x="1451.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r7" x="36.6" y="654.4" textLength="207.4" clip-path="url(#terminal-line-26)">Local&#160;MCP&#160;Servers</text><text class="terminal-r5" x="1451.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r7" x="36.6" y="678.8" textLength="146.4" clip-path="url(#terminal-line-27)">&#160;&#160;filesystem</text><text class="terminal-r8" x="183" y="678.8" textLength="109.8" clip-path="url(#terminal-line-27)">&#160;&#160;[stdio]</text><text class="terminal-r8" x="292.8" y="678.8" textLength="97.6" clip-path="url(#terminal-line-27)">&#160;&#160;1&#160;tool</text><text class="terminal-r5" x="1451.8" 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)">
</text><text class="terminal-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="36.6" y="727.6" textLength="244" clip-path="url(#terminal-line-29)">Workspace&#160;Connectors</text><text class="terminal-r5" x="1451.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r5" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="36.6" y="752" textLength="85.4" clip-path="url(#terminal-line-30)">&#160;&#160;gmail</text><text class="terminal-r9" x="122" y="752" textLength="158.6" clip-path="url(#terminal-line-30)">&#160;&#160;[connector]</text><text class="terminal-r9" x="280.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">&#160;&#160;3&#160;tools</text><text class="terminal-r10" x="414.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r9" x="427" y="752" textLength="122" clip-path="url(#terminal-line-30)">&#160;connected</text><text class="terminal-r5" x="1451.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r5" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="36.6" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">&#160;&#160;slack</text><text class="terminal-r9" x="122" y="776.4" textLength="158.6" clip-path="url(#terminal-line-31)">&#160;&#160;[connector]</text><text class="terminal-r9" x="280.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">&#160;&#160;2&#160;tools</text><text class="terminal-r10" x="414.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r9" x="427" y="776.4" textLength="122" clip-path="url(#terminal-line-31)">&#160;connected</text><text class="terminal-r5" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r5" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r5" x="24.4" y="825.2" textLength="488" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Show&#160;tools&#160;&#160;Esc&#160;Close</text><text class="terminal-r5" x="1451.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r5" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View file

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

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -6,14 +6,29 @@ from textual.pilot import Pilot
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp, default_config
from tests.snapshots.snap_compare import SnapCompare
from tests.stubs.fake_connector_registry import FakeConnectorRegistry
from tests.stubs.fake_mcp_registry import (
FakeMCPRegistry,
FakeMCPRegistryWithBrokenServer,
)
from vibe.core.config import MCPHttp, MCPStdio
from vibe.core.tools.connectors import CONNECTORS_ENV_VAR
from vibe.core.tools.mcp.tools import RemoteTool
_MCP_PATCH = "vibe.core.agent_loop.MCPRegistry"
_FAKE_CONNECTORS = {
"gmail": [
RemoteTool(name="gmail_search", description="Search emails in Gmail"),
RemoteTool(name="gmail_draft_create", description="Draft an email in Gmail"),
RemoteTool(name="gmail_open_message", description="Open a mail on Gmail"),
],
"slack": [
RemoteTool(name="search_messages", description="Search Slack messages"),
RemoteTool(name="send_message", description="Send a Slack message"),
],
}
class SnapshotTestAppNoMcpServers(BaseSnapshotTestApp):
def __init__(self) -> None:
@ -157,3 +172,105 @@ def test_snapshot_mcp_escape_closes(snap_compare: SnapCompare) -> None:
terminal_size=(120, 36),
run_before=run_before,
)
# ---------------------------------------------------------------------------
# Apps with connectors
# ---------------------------------------------------------------------------
class SnapshotTestAppWithConnectors(BaseSnapshotTestApp):
def __init__(self) -> None:
config = default_config()
config.mcp_servers = [
MCPStdio(name="filesystem", transport="stdio", command="npx")
]
super().__init__(config=config)
registry = FakeConnectorRegistry(connectors=_FAKE_CONNECTORS)
self.agent_loop.connector_registry = registry
self.agent_loop.tool_manager._connector_registry = registry
self.agent_loop.tool_manager.integrate_connectors()
class SnapshotTestAppConnectorsOnly(BaseSnapshotTestApp):
def __init__(self) -> None:
config = default_config()
config.mcp_servers = []
super().__init__(config=config)
registry = FakeConnectorRegistry(connectors=_FAKE_CONNECTORS)
self.agent_loop.connector_registry = registry
self.agent_loop.tool_manager._connector_registry = registry
self.agent_loop.tool_manager.integrate_connectors()
# ---------------------------------------------------------------------------
# Connector snapshot tests
# ---------------------------------------------------------------------------
@patch.dict("os.environ", {CONNECTORS_ENV_VAR: "1"})
def test_snapshot_mcp_with_connectors_overview(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await _run_mcp_command(pilot, "/mcp")
with patch(_MCP_PATCH, FakeMCPRegistry):
assert snap_compare(
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors",
terminal_size=(120, 36),
run_before=run_before,
)
@patch.dict("os.environ", {CONNECTORS_ENV_VAR: "1"})
def test_snapshot_mcp_connectors_only(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await _run_mcp_command(pilot, "/mcp")
assert snap_compare(
"test_ui_snapshot_mcp_command.py:SnapshotTestAppConnectorsOnly",
terminal_size=(120, 36),
run_before=run_before,
)
@patch.dict("os.environ", {CONNECTORS_ENV_VAR: "1"})
def test_snapshot_mcp_drill_into_connector(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await _run_mcp_command(pilot, "/mcp")
# Navigate past the MCP server to the first connector
await pilot.press("down") # skip filesystem server
await pilot.pause(0.1)
await pilot.press("down") # first connector
await pilot.pause(0.1)
await pilot.press("enter") # drill in
await pilot.pause(0.1)
with patch(_MCP_PATCH, FakeMCPRegistry):
assert snap_compare(
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors",
terminal_size=(120, 36),
run_before=run_before,
)
@patch.dict("os.environ", {CONNECTORS_ENV_VAR: "1"})
def test_snapshot_mcp_connector_back_to_overview(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await _run_mcp_command(pilot, "/mcp")
await pilot.press("down", "down")
await pilot.pause(0.1)
await pilot.press("enter")
await pilot.pause(0.1)
await pilot.press("backspace")
await pilot.pause(0.1)
with patch(_MCP_PATCH, FakeMCPRegistry):
assert snap_compare(
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors",
terminal_size=(120, 36),
run_before=run_before,
)

View file

@ -0,0 +1,61 @@
"""Regression test for streaming code-fenced ASCII art rendering.
Textual's Markdown.update() sets _last_parsed_line past fence openers,
causing append() to re-parse without fence context and collapse content
into paragraph text. This test streams a code-fenced diagram in small
chunks and verifies the rendered output preserves the fence block.
"""
from __future__ import annotations
from textual.pilot import Pilot
from tests.conftest import build_test_agent_loop
from tests.mock.utils import mock_llm_chunk
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp, default_config
from tests.snapshots.snap_compare import SnapCompare
from tests.stubs.fake_backend import FakeBackend
# Chunks are sized so the first one contains the fence opener (```)
# plus content past the first newline — the exact pattern that triggers
# the bug.
_CHUNKS = [
# First chunk: fence opener + content past newline (triggers the bug)
"```\n Client\n |\n | POST",
" /api/orders\n v\n+---------+\n| Gateway ",
"|\n+---------+\n |\n v\n+---------+\n| Service ",
"|\n+---------+\n |\n v\n+----+----+\n| DB ",
"|\n+---------+\n```",
]
class StreamingCodeFenceApp(BaseSnapshotTestApp):
def __init__(self) -> None:
config = default_config()
fake_backend = FakeBackend(
chunks=[mock_llm_chunk(content=chunk) for chunk in _CHUNKS]
)
super().__init__(config=config)
self.agent_loop = build_test_agent_loop(
config=config,
agent_name=self._current_agent_name,
enable_streaming=True,
backend=fake_backend,
)
def test_snapshot_streaming_code_fence_preserved(snap_compare: SnapCompare) -> None:
"""Verify that a streamed code-fenced diagram renders as a code block,
not as collapsed paragraph text.
"""
async def run_before(pilot: Pilot) -> None:
await pilot.press(*"show diagram")
await pilot.press("enter")
await pilot.pause(0.5)
assert snap_compare(
"test_ui_snapshot_streaming_code_fence.py:StreamingCodeFenceApp",
terminal_size=(120, 36),
run_before=run_before,
)

View file

@ -0,0 +1,46 @@
from __future__ import annotations
from vibe.core.tools.base import BaseTool
from vibe.core.tools.connectors.connector_registry import (
ConnectorRegistry,
RemoteTool,
_normalize_name,
create_connector_proxy_tool_class,
)
class FakeConnectorRegistry(ConnectorRegistry):
"""Test double that returns canned connector tools without hitting the API."""
def __init__(self, connectors: dict[str, list[RemoteTool]] | None = None) -> None:
super().__init__(api_key="fake-key")
self._fake_connectors = connectors or {}
self._build_cache()
def _build_cache(self) -> None:
self._cache = {}
self._connector_names = []
self._connector_connected = {}
for connector_name, tools in self._fake_connectors.items():
alias = _normalize_name(connector_name)
connector_id = f"fake-id-{connector_name}"
tool_map: dict[str, type[BaseTool]] = {}
for remote in tools:
proxy_cls = create_connector_proxy_tool_class(
connector_name=connector_name,
connector_alias=alias,
connector_id=connector_id,
remote=remote,
api_key="fake-key",
)
tool_map[proxy_cls.get_name()] = proxy_cls
self._cache[connector_id] = tool_map
self._connector_names.append(alias)
self._connector_connected[alias] = bool(tool_map)
def get_tools(self) -> dict[str, type[BaseTool]]:
result: dict[str, type[BaseTool]] = {}
if self._cache:
for tools in self._cache.values():
result.update(tools)
return result

View file

@ -18,6 +18,11 @@ _BROKEN_SERVER_NAME = "broken-server"
class FakeMCPRegistry(MCPRegistry):
async def get_tools_async(
self, servers: list[MCPServer]
) -> dict[str, type[BaseTool]]:
return self.get_tools(servers)
def get_tools(self, servers: list[MCPServer]) -> dict[str, type[BaseTool]]:
result: dict[str, type[BaseTool]] = {}
for srv in servers:

View file

@ -131,6 +131,7 @@ async def test_passes_session_id_to_backend(vibe_config: VibeConfig):
assert "message_id" in meta
assert meta["is_user_prompt"] == "true"
assert meta["call_type"] == "main_call"
assert meta["call_source"] == "vibe_code"
@pytest.mark.asyncio
@ -162,6 +163,7 @@ async def test_passes_entrypoint_metadata_to_backend(vibe_config: VibeConfig):
assert "message_id" in meta
assert meta["is_user_prompt"] == "true"
assert meta["call_type"] == "main_call"
assert meta["call_source"] == "vibe_code"
@pytest.mark.asyncio

View file

@ -31,7 +31,7 @@ class TestCompleteInit:
loop = build_test_agent_loop(defer_heavy_init=True)
error = RuntimeError("mcp boom")
with patch.object(loop.tool_manager, "integrate_mcp", side_effect=error):
with patch.object(loop.tool_manager, "integrate_all", side_effect=error):
loop._complete_init()
assert loop.is_initialized
@ -44,7 +44,7 @@ class TestCompleteInit:
with patch.object(
loop.tool_manager._mcp_registry,
"get_tools",
"get_tools_async",
side_effect=RuntimeError("mcp discovery boom"),
):
loop._complete_init()
@ -84,7 +84,7 @@ class TestWaitForInit:
loop = build_test_agent_loop(defer_heavy_init=True)
error = RuntimeError("init failed")
with patch.object(loop.tool_manager, "integrate_mcp", side_effect=error):
with patch.object(loop.tool_manager, "integrate_all", side_effect=error):
loop._complete_init()
with pytest.raises(RuntimeError, match="init failed"):
@ -95,7 +95,7 @@ class TestWaitForInit:
loop = build_test_agent_loop(defer_heavy_init=True)
error = RuntimeError("once only")
with patch.object(loop.tool_manager, "integrate_mcp", side_effect=error):
with patch.object(loop.tool_manager, "integrate_all", side_effect=error):
loop._complete_init()
with pytest.raises(RuntimeError):

View file

@ -123,13 +123,13 @@ async def test_ui_alt_left_and_alt_right_move_by_word(vibe_app: VibeApp) -> None
await pilot.press(*"hello brave world")
assert textarea.cursor_location == (0, len("hello brave world"))
await pilot.press("alt+left")
await pilot.press("ctrl+left")
assert textarea.cursor_location == (0, len("hello brave "))
await pilot.press("alt+left")
await pilot.press("ctrl+left")
assert textarea.cursor_location == (0, len("hello "))
await pilot.press("alt+right")
await pilot.press("ctrl+right")
assert textarea.cursor_location == (0, len("hello brave"))
assert chat_input.value == "hello brave world"

View file

@ -0,0 +1,401 @@
from __future__ import annotations
import os
from typing import cast
from unittest.mock import AsyncMock, patch
import httpx
import pytest
from tests.stubs.fake_connector_registry import FakeConnectorRegistry
from tests.stubs.fake_mcp_registry import FakeMCPRegistry
from vibe.core.config import VibeConfig
from vibe.core.tools.base import BaseToolConfig, ToolError
from vibe.core.tools.connectors import CONNECTORS_ENV_VAR
from vibe.core.tools.connectors.connector_registry import (
RemoteTool,
_connector_error_message,
_normalize_name,
_unwrap_http_status_error,
create_connector_proxy_tool_class,
)
from vibe.core.tools.manager import ToolManager
from vibe.core.tools.mcp.tools import MCPTool, MCPToolResult
# ---------------------------------------------------------------------------
# Unit tests for helper functions
# ---------------------------------------------------------------------------
class TestNormalizeName:
def test_basic(self) -> None:
assert _normalize_name("my_connector") == "my_connector"
def test_special_chars(self) -> None:
assert _normalize_name("my.connector!v2") == "my_connector_v2"
def test_strip_edges(self) -> None:
assert _normalize_name("--hello--") == "hello"
# ---------------------------------------------------------------------------
# Unit tests for proxy tool class creation
# ---------------------------------------------------------------------------
class TestCreateConnectorProxyTool:
def test_tool_name(self) -> None:
remote = RemoteTool(name="search", description="Search docs")
cls = create_connector_proxy_tool_class(
connector_name="deepwiki",
connector_alias="deepwiki",
connector_id="abc-123",
remote=remote,
api_key="key",
)
assert cls.get_name() == "connector_deepwiki_search"
def test_is_connector_flag(self) -> None:
remote = RemoteTool(name="read", description="Read file")
cls = create_connector_proxy_tool_class(
connector_name="myconn",
connector_alias="myconn",
connector_id="id-1",
remote=remote,
api_key="key",
)
assert issubclass(cls, MCPTool)
assert cls._is_connector is True
assert cls.is_connector() is True
def test_description_includes_alias(self) -> None:
remote = RemoteTool(name="fetch", description="Fetch page")
cls = create_connector_proxy_tool_class(
connector_name="web_tool",
connector_alias="web_tool",
connector_id="id-2",
remote=remote,
api_key="key",
)
assert cls.description.startswith("[web_tool]")
assert "Fetch page" in cls.description
def test_parameters_from_schema(self) -> None:
schema = {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
}
remote = RemoteTool.model_validate({
"name": "search",
"description": "Search",
"inputSchema": schema,
})
cls = create_connector_proxy_tool_class(
connector_name="conn",
connector_alias="conn",
connector_id="id-3",
remote=remote,
api_key="key",
)
params = cls.get_parameters()
assert params["properties"]["query"]["type"] == "string"
def test_server_name_is_connector_alias(self) -> None:
remote = RemoteTool(name="tool1", description="A tool")
cls = cast(
type[MCPTool],
create_connector_proxy_tool_class(
connector_name="my-connector",
connector_alias="my-connector",
connector_id="id-4",
remote=remote,
api_key="key",
),
)
assert cls.get_server_name() == "my-connector"
# ---------------------------------------------------------------------------
# FakeConnectorRegistry tests
# ---------------------------------------------------------------------------
class TestFakeConnectorRegistry:
def test_get_tools(self) -> None:
registry = FakeConnectorRegistry(
connectors={
"wiki": [RemoteTool(name="search", description="Search wiki")],
"mail": [
RemoteTool(name="send", description="Send email"),
RemoteTool(name="read", description="Read email"),
],
}
)
tools = registry.get_tools()
assert "connector_wiki_search" in tools
assert "connector_mail_send" in tools
assert "connector_mail_read" in tools
assert registry.connector_count == 2
def test_connector_names(self) -> None:
registry = FakeConnectorRegistry(connectors={"alpha": [], "beta": []})
assert set(registry.get_connector_names()) == {"alpha", "beta"}
# ---------------------------------------------------------------------------
# Integration: ToolManager + ConnectorRegistry
# ---------------------------------------------------------------------------
class TestToolManagerConnectorIntegration:
@staticmethod
def _make_config() -> VibeConfig:
"""Minimal VibeConfig-like stub for ToolManager."""
return cast(
VibeConfig,
type(
"_Cfg",
(),
{
"mcp_servers": [],
"enabled_tools": [],
"disabled_tools": [],
"tools": {},
"tool_paths": [],
},
)(),
)
def test_connector_tools_registered(self) -> None:
registry = FakeConnectorRegistry(
connectors={"myconn": [RemoteTool(name="ping", description="Ping")]}
)
config = self._make_config()
tm = ToolManager(
config_getter=lambda: config,
mcp_registry=FakeMCPRegistry(),
connector_registry=registry,
)
assert "connector_myconn_ping" in tm.registered_tools
def test_no_connector_registry(self) -> None:
config = self._make_config()
tm = ToolManager(
config_getter=lambda: config,
mcp_registry=FakeMCPRegistry(),
connector_registry=None,
)
# No connector tools, but no crash
connector_tools = [
name
for name, cls in tm.registered_tools.items()
if issubclass(cls, MCPTool) and cls.is_connector()
]
assert connector_tools == []
# ---------------------------------------------------------------------------
# ConnectorRegistry env var gating (tested via agent_loop helper logic)
# ---------------------------------------------------------------------------
class TestConnectorRegistryEnvGating:
def test_disabled_without_env_var(self) -> None:
with patch.dict(os.environ, {}, clear=False):
os.environ.pop(CONNECTORS_ENV_VAR, None)
assert os.getenv(CONNECTORS_ENV_VAR) != "1"
def test_enabled_with_env_var(self) -> None:
with patch.dict(os.environ, {CONNECTORS_ENV_VAR: "1"}):
assert os.getenv(CONNECTORS_ENV_VAR) == "1"
# ---------------------------------------------------------------------------
# Error message helpers
# ---------------------------------------------------------------------------
def _make_http_status_error(status: int, body: str = "") -> httpx.HTTPStatusError:
response = httpx.Response(
status, text=body, request=httpx.Request("POST", "http://example.com")
)
return httpx.HTTPStatusError("error", request=response.request, response=response)
class TestConnectorErrorMessage:
def test_timeout(self) -> None:
exc = httpx.ReadTimeout("timed out")
msg = _connector_error_message(exc, "id-1", "myconn")
assert "timed out" in msg.lower()
assert "myconn" in msg
def test_connect_error(self) -> None:
exc = httpx.ConnectError("connection refused")
msg = _connector_error_message(exc, "id-1", "myconn")
assert "network" in msg.lower()
assert "myconn" in msg
def test_exception_group(self) -> None:
exc = ExceptionGroup("errors", [ValueError("a"), RuntimeError("b")])
msg = _connector_error_message(exc, "id-1", "myconn")
assert "multiple errors" in msg.lower()
assert "myconn" in msg
def test_generic_error(self) -> None:
exc = RuntimeError("something broke")
msg = _connector_error_message(exc, "id-1", "myconn")
assert "something broke" in msg
assert "myconn" in msg
def test_http_401_surfaces_auth_message(self) -> None:
exc = _make_http_status_error(401, "Unauthorized")
msg = _connector_error_message(exc, "id-1", "myconn")
assert "authentication failed" in msg.lower()
assert "401" in msg
assert "Unauthorized" in msg
def test_http_400_surfaces_response_body(self) -> None:
exc = _make_http_status_error(400, '{"error": "missing field X"}')
msg = _connector_error_message(exc, "id-1", "myconn")
assert "400" in msg
assert "missing field X" in msg
def test_http_404_surfaces_not_found(self) -> None:
exc = _make_http_status_error(404)
msg = _connector_error_message(exc, "id-1", "myconn")
assert "not found" in msg.lower()
def test_http_error_wrapped_in_exception_group(self) -> None:
inner = _make_http_status_error(400, "bad request detail")
exc = ExceptionGroup("errors", [inner])
msg = _connector_error_message(exc, "id-1", "myconn")
assert "400" in msg
assert "bad request detail" in msg
class TestUnwrapHttpStatusError:
def test_direct(self) -> None:
exc = _make_http_status_error(400)
assert _unwrap_http_status_error(exc) is exc
def test_from_exception_group(self) -> None:
inner = _make_http_status_error(500)
exc = ExceptionGroup("g", [ValueError("x"), inner])
assert _unwrap_http_status_error(exc) is inner
def test_from_cause(self) -> None:
inner = _make_http_status_error(403)
outer = RuntimeError("wrapper")
outer.__cause__ = inner
assert _unwrap_http_status_error(outer) is inner
def test_none_when_absent(self) -> None:
assert _unwrap_http_status_error(RuntimeError("no http")) is None
# ---------------------------------------------------------------------------
# ConnectorProxyTool.run() via MCP proxy
# ---------------------------------------------------------------------------
class TestConnectorProxyToolRun:
@staticmethod
def _make_tool_class() -> type[MCPTool]:
remote = RemoteTool(name="search", description="Search docs")
return cast(
type[MCPTool],
create_connector_proxy_tool_class(
connector_name="wiki",
connector_alias="wiki",
connector_id="conn-123",
remote=remote,
api_key="test-key",
server_url="https://custom.api.example.com",
),
)
@staticmethod
def _make_tool(tool_cls: type[MCPTool]) -> MCPTool:
return cast(MCPTool, tool_cls.from_config(lambda: BaseToolConfig()))
@pytest.mark.asyncio
async def test_run_calls_mcp_proxy(self) -> None:
cls = self._make_tool_class()
tool = self._make_tool(cls)
expected = MCPToolResult(
ok=True, server="test", tool="search", text="result text"
)
with patch(
"vibe.core.tools.connectors.connector_registry.call_tool_http",
new_callable=AsyncMock,
return_value=expected,
) as mock_call:
results = [r async for r in tool.invoke(query="hello")]
mock_call.assert_awaited_once()
call_args = mock_call.call_args
assert "/v1/experimental/connectors/conn-123/mcp" in call_args.args[0]
assert call_args.args[1] == "search"
assert call_args.kwargs["headers"]["Authorization"] == "Bearer test-key"
assert len(results) == 1
assert results[0] == expected
@pytest.mark.asyncio
async def test_run_uses_default_base_url(self) -> None:
remote = RemoteTool(name="ping", description="Ping")
cls = cast(
type[MCPTool],
create_connector_proxy_tool_class(
connector_name="svc",
connector_alias="svc",
connector_id="c-1",
remote=remote,
api_key="key",
server_url=None,
),
)
tool = self._make_tool(cls)
expected = MCPToolResult(ok=True, server="s", tool="ping", text="pong")
with patch(
"vibe.core.tools.connectors.connector_registry.call_tool_http",
new_callable=AsyncMock,
return_value=expected,
) as mock_call:
[_ async for _ in tool.invoke()]
url = mock_call.call_args.args[0]
assert url.startswith("https://api.mistral.ai/")
@pytest.mark.asyncio
async def test_run_surfaces_timeout_error(self) -> None:
cls = self._make_tool_class()
tool = self._make_tool(cls)
with (
patch(
"vibe.core.tools.connectors.connector_registry.call_tool_http",
new_callable=AsyncMock,
side_effect=httpx.ReadTimeout("timed out"),
),
pytest.raises(ToolError, match="timed out"),
):
[_ async for _ in tool.invoke(query="hello")]
@pytest.mark.asyncio
async def test_run_surfaces_connect_error(self) -> None:
cls = self._make_tool_class()
tool = self._make_tool(cls)
with (
patch(
"vibe.core.tools.connectors.connector_registry.call_tool_http",
new_callable=AsyncMock,
side_effect=httpx.ConnectError("refused"),
),
pytest.raises(ToolError, match="network"),
):
[_ async for _ in tool.invoke(query="hello")]

12
uv.lock generated
View file

@ -3,7 +3,7 @@ revision = 3
requires-python = ">=3.12"
[options]
exclude-newer = "2026-04-06T21:12:11.186502Z"
exclude-newer = "2026-04-09T07:58:59.192295Z"
exclude-newer-span = "P7D"
[options.exclude-newer-package]
@ -808,7 +808,7 @@ wheels = [
[[package]]
name = "mistral-vibe"
version = "2.7.5"
version = "2.7.6"
source = { editable = "." }
dependencies = [
{ name = "agent-client-protocol" },
@ -886,7 +886,7 @@ requires-dist = [
{ name = "keyring", specifier = ">=25.6.0" },
{ name = "markdownify", specifier = ">=1.2.2" },
{ name = "mcp", specifier = ">=1.14.0" },
{ name = "mistralai", specifier = "==2.1.3" },
{ name = "mistralai", specifier = "==2.3.2" },
{ name = "opentelemetry-api", specifier = ">=1.39.1" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.39.1" },
{ name = "opentelemetry-sdk", specifier = ">=1.39.1" },
@ -935,7 +935,7 @@ dev = [
[[package]]
name = "mistralai"
version = "2.1.3"
version = "2.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "eval-type-backport" },
@ -947,9 +947,9 @@ dependencies = [
{ name = "python-dateutil" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/98/5fe39d514c19477f06a07e088ce4a44c2e60ac9deebefb9e2c8ed8ef87d2/mistralai-2.1.3.tar.gz", hash = "sha256:0c5de4855b043cd0582406d5c1ddfd91e176f484a158e6ee0b4a0054231be266", size = 331929, upload-time = "2026-03-23T15:00:29.579Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/08/f8fed64eab35ad1ded82f92bf834160302db19f5bbda5a904df7d4dbf3c4/mistralai-2.3.2.tar.gz", hash = "sha256:a02c7e90ac165e8680c849551ff5fe9788e9fc10b7dbe71817443dc63cc5e9c9", size = 394665, upload-time = "2026-04-10T14:05:36.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/7c/f91a26bf469c1cff57325379afa112baeb113ac577d28e69dd408cee5745/mistralai-2.1.3-py3-none-any.whl", hash = "sha256:26daac3bdc69fc2dd58f2c421710eb34131be7883b44a9ea81904a6306e6a90a", size = 754931, upload-time = "2026-03-23T15:00:30.934Z" },
{ url = "https://files.pythonhosted.org/packages/83/e0/9f246d18f8fe6e815f7cae88c437c986b342d3ff568607c8e0d53f4710d0/mistralai-2.3.2-py3-none-any.whl", hash = "sha256:dec05f446502289b76add9796d75465c070fc539b3c01d6f7d3297833bb64981", size = 935914, upload-time = "2026-04-10T14:05:37.845Z" },
]
[[package]]

View file

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

View file

@ -33,6 +33,7 @@ from acp.schema import (
AvailableCommandInput,
ClientCapabilities,
CloseSessionResponse,
ConfigOptionUpdate,
ContentToolCallContent,
Cost,
EnvVarAuthMethod,
@ -44,6 +45,8 @@ from acp.schema import (
PromptCapabilities,
ResumeSessionResponse,
SessionCapabilities,
SessionConfigOptionBoolean,
SessionConfigOptionSelect,
SessionInfo,
SessionListCapabilities,
SetSessionConfigOptionResponse,
@ -57,10 +60,11 @@ from acp.schema import (
Usage,
UsageUpdate,
)
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, SkipValidation
from vibe import VIBE_ROOT, __version__
from vibe.acp.acp_logger import acp_message_observer
from vibe.acp.commands import AcpCommandRegistry
from vibe.acp.exceptions import (
ConfigurationError,
ConversationLimitError,
@ -109,6 +113,7 @@ from vibe.core.proxy_setup import (
unset_proxy_var,
)
from vibe.core.session.session_loader import SessionLoader
from vibe.core.skills.manager import SkillManager
from vibe.core.tools.permissions import RequiredPermission
from vibe.core.types import (
AgentProfileChangedEvent,
@ -143,6 +148,7 @@ class AcpSessionLoop(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
id: str
agent_loop: AgentLoop
command_registry: SkipValidation[AcpCommandRegistry]
task: asyncio.Task[None] | None = None
@ -254,9 +260,16 @@ class VibeAcpAgentLoop(AcpAgent):
async def _create_acp_session(
self, session_id: str, agent_loop: AgentLoop
) -> AcpSessionLoop:
session = AcpSessionLoop(id=session_id, agent_loop=agent_loop)
command_registry = AcpCommandRegistry()
session = AcpSessionLoop(
id=session_id, agent_loop=agent_loop, command_registry=command_registry
)
self.sessions[session.id] = session
command_registry.set_on_changed(
lambda: self._send_available_commands(session.id)
)
if not agent_loop.auto_approve:
agent_loop.set_approval_callback(self._create_approval_callback(session.id))
@ -463,137 +476,41 @@ class VibeAcpAgentLoop(AcpAgent):
)
async def _send_available_commands(self, session_id: str) -> None:
commands = [
session = self._get_session(session_id)
commands: list[AvailableCommand] = []
for cmd in session.command_registry.commands.values():
input_spec = (
AvailableCommandInput(
root=UnstructuredCommandInput(hint=cmd.input_hint)
)
if cmd.input_hint
else None
)
commands.append(
AvailableCommand(
name="proxy-setup",
description="Configure proxy and SSL certificate settings",
name=cmd.name, description=cmd.description, input=input_spec
)
)
builtin_names = set(session.command_registry.commands)
for skill in session.agent_loop.skill_manager.available_skills.values():
if not skill.user_invocable or skill.name in builtin_names:
continue
commands.append(
AvailableCommand(
name=skill.name,
description=skill.description,
input=AvailableCommandInput(
root=UnstructuredCommandInput(
hint="KEY value to set, KEY to unset, or empty for help"
)
),
),
AvailableCommand(
name="leanstall",
description="Install the Lean 4 agent (leanstral)",
input=AvailableCommandInput(root=UnstructuredCommandInput(hint="")),
),
AvailableCommand(
name="data-retention",
description="Show data retention information",
input=AvailableCommandInput(root=UnstructuredCommandInput(hint="")),
),
AvailableCommand(
name="unleanstall",
description="Uninstall the Lean 4 agent",
input=AvailableCommandInput(root=UnstructuredCommandInput(hint="")),
),
]
update = update_available_commands(commands)
await self.client.session_update(session_id=session_id, update=update)
async def _handle_proxy_setup_command(
self, session_id: str, text_prompt: str, message_id: str
) -> PromptResponse:
args = text_prompt.strip()[len("/proxy-setup") :].strip()
try:
if not args:
message = get_proxy_help_text()
else:
key, value = parse_proxy_command(args)
if value is not None:
set_proxy_var(key, value)
message = f"Set `{key}={value}` in ~/.vibe/.env\n\nPlease start a new chat for changes to take effect."
else:
unset_proxy_var(key)
message = f"Removed `{key}` from ~/.vibe/.env\n\nPlease start a new chat for changes to take effect."
except ProxySetupError as e:
message = f"Error: {e}"
await self.client.session_update(
session_id=session_id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=message),
message_id=str(uuid4()),
root=UnstructuredCommandInput(hint="instructions for the skill")
),
)
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
async def _handle_leanstall_command(
self, session_id: str, message_id: str
) -> PromptResponse:
session = self._get_session(session_id)
current = list(session.agent_loop.base_config.installed_agents)
if "lean" in current:
message = "Lean agent is already installed."
else:
VibeConfig.save_updates({"installed_agents": [*current, "lean"]})
new_config = VibeConfig.load(
tool_paths=session.agent_loop.config.tool_paths,
disabled_tools=["ask_user_question"],
)
await session.agent_loop.reload_with_initial_messages(
base_config=new_config
)
message = (
"Lean agent installed. Start a new session to switch to Lean mode."
)
await self.client.session_update(
session_id=session_id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=message),
message_id=str(uuid4()),
),
session_id=session_id, update=update_available_commands(commands)
)
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
async def _handle_data_retention_command(
self, session_id: str, message_id: str
) -> PromptResponse:
await self.client.session_update(
session_id=session_id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=DATA_RETENTION_MESSAGE),
message_id=str(uuid4()),
),
)
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
async def _handle_unleanstall_command(
self, session_id: str, message_id: str
) -> PromptResponse:
session = self._get_session(session_id)
current = list(session.agent_loop.base_config.installed_agents)
if "lean" not in current:
message = "Lean agent is not installed."
else:
VibeConfig.save_updates({
"installed_agents": [a for a in current if a != "lean"]
})
new_config = VibeConfig.load(
tool_paths=session.agent_loop.config.tool_paths,
disabled_tools=["ask_user_question"],
)
await session.agent_loop.reload_with_initial_messages(
base_config=new_config
)
message = "Lean agent uninstalled."
await self.client.session_update(
session_id=session_id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=message),
message_id=str(uuid4()),
),
)
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
@override
async def load_session(
@ -723,16 +640,8 @@ class VibeAcpAgentLoop(AcpAgent):
if not success:
return None
profiles = list(session.agent_loop.agent_manager.available_agents.values())
_, modes_config = make_mode_response(
profiles, session.agent_loop.agent_profile.name
)
_, models_config = make_model_response(
session.agent_loop.config.models, session.agent_loop.config.active_model
)
return SetSessionConfigOptionResponse(
config_options=[modes_config, models_config]
config_options=self._build_config_options(session)
)
@override
@ -779,23 +688,21 @@ class VibeAcpAgentLoop(AcpAgent):
text_prompt = self._build_text_prompt(prompt)
resolved_message_id = _resolved_user_message_id(message_id)
if text_prompt.strip().lower().startswith("/proxy-setup"):
return await self._handle_proxy_setup_command(
session_id, text_prompt, resolved_message_id
)
if command_response := await self._maybe_handle_builtin_command(
session, text_prompt, resolved_message_id
):
return command_response
if text_prompt.strip().lower().startswith("/unleanstall"):
return await self._handle_unleanstall_command(
session_id, resolved_message_id
)
try:
skill = session.agent_loop.skill_manager.parse_skill_command(text_prompt)
except OSError as e:
raise InternalError(f"Failed to read skill file: {e}") from e
if text_prompt.strip().lower().startswith("/leanstall"):
return await self._handle_leanstall_command(session_id, resolved_message_id)
if text_prompt.strip().lower().startswith("/data-retention"):
return await self._handle_data_retention_command(
session_id, resolved_message_id
if skill:
session.agent_loop.telemetry_client.send_slash_command_used(
skill.name, "skill"
)
text_prompt = SkillManager.build_skill_prompt(text_prompt, skill)
async def agent_loop_task() -> None:
async for update in self._run_agent_loop(
@ -881,6 +788,22 @@ class VibeAcpAgentLoop(AcpAgent):
)
return text_prompt
async def _maybe_handle_builtin_command(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse | None:
normalized = text_prompt.strip().lower()
parts = normalized.split(None, 1)
if not parts or not parts[0].startswith("/"):
return None
cmd_name = parts[0][1:] # strip leading "/"
command = session.command_registry.get(cmd_name)
if command is None:
return None
handler = getattr(self, command.handler)
return await handler(session, text_prompt, message_id)
async def _run_agent_loop(
self, session: AcpSessionLoop, prompt: str, client_message_id: str | None = None
) -> AsyncGenerator[SessionUpdate]:
@ -990,6 +913,218 @@ class VibeAcpAgentLoop(AcpAgent):
def on_connect(self, conn: Client) -> None:
self.client = conn
# -- Command handlers ------------------------------------------------------
async def _command_reply(
self, session: AcpSessionLoop, text: str, message_id: str
) -> PromptResponse:
"""Send a text message to the client and return an end-turn response."""
await self.client.session_update(
session_id=session.id,
update=AgentMessageChunk(
session_update="agent_message_chunk",
content=TextContentBlock(type="text", text=text),
message_id=str(uuid4()),
),
)
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
async def _handle_help(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
lines = ["### Available Commands", ""]
for cmd in session.command_registry.commands.values():
hint = f" `<{cmd.input_hint}>`" if cmd.input_hint else ""
lines.append(f"- `/{cmd.name}`{hint}: {cmd.description}")
builtin_names = set(session.command_registry.commands)
invocable = {
n: s
for n, s in session.agent_loop.skill_manager.available_skills.items()
if s.user_invocable and n not in builtin_names
}
if invocable:
lines.extend(["", "### Available Skills", ""])
for name, info in invocable.items():
lines.append(f"- `/{name}`: {info.description}")
return await self._command_reply(session, "\n".join(lines), message_id)
async def _handle_compact(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
if len(session.agent_loop.messages) <= 1:
return await self._command_reply(
session, "No conversation history to compact yet.", message_id
)
tool_call_id = str(uuid4())
old_tokens = session.agent_loop.stats.context_tokens
start_event = CompactStartEvent(
current_context_tokens=old_tokens or 0,
threshold=0,
tool_call_id=tool_call_id,
)
await self.client.session_update(
session_id=session.id,
update=create_compact_start_session_update(start_event),
)
await session.agent_loop.compact()
new_tokens = session.agent_loop.stats.context_tokens
end_event = CompactEndEvent(
old_context_tokens=old_tokens or 0,
new_context_tokens=new_tokens or 0,
summary_length=0,
tool_call_id=tool_call_id,
)
await self.client.session_update(
session_id=session.id, update=create_compact_end_session_update(end_event)
)
return PromptResponse(stop_reason="end_turn", user_message_id=message_id)
async def _reload_session_config(self, session: AcpSessionLoop) -> None:
"""Reload config from disk and reinitialize the agent loop."""
new_config = VibeConfig.load(
tool_paths=session.agent_loop.config.tool_paths,
disabled_tools=["ask_user_question"],
)
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
async def _handle_reload(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
try:
await self._reload_session_config(session)
except Exception as e:
return await self._command_reply(
session, f"Failed to reload config: {e}", message_id
)
try:
await session.command_registry.notify_changed()
except Exception as e:
return await self._command_reply(
session,
f"Configuration reloaded, but failed to advertise updated commands: {e}",
message_id,
)
return await self._command_reply(
session,
"Configuration reloaded (includes agent instructions and skills).",
message_id,
)
async def _handle_log(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
logger = session.agent_loop.session_logger
if not logger.enabled:
return await self._command_reply(
session, "Session logging is disabled in configuration.", message_id
)
return await self._command_reply(
session,
f"## Current Log Directory\n\n`{logger.session_dir}`\n\n"
"You can send this directory to share your interaction.",
message_id,
)
async def _handle_proxy_setup(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
parts = text_prompt.strip().split(None, 1)
args = parts[1] if len(parts) > 1 else ""
try:
if not args:
message = get_proxy_help_text()
else:
key, value = parse_proxy_command(args)
if value is not None:
set_proxy_var(key, value)
message = (
f"Set `{key}={value}` in ~/.vibe/.env\n\n"
"Please start a new chat for changes to take effect."
)
else:
unset_proxy_var(key)
message = (
f"Removed `{key}` from ~/.vibe/.env\n\n"
"Please start a new chat for changes to take effect."
)
except ProxySetupError as e:
message = f"Error: {e}"
return await self._command_reply(session, message, message_id)
def _build_config_options(
self, session: AcpSessionLoop
) -> list[SessionConfigOptionSelect | SessionConfigOptionBoolean]:
"""Build the current modes + models config options for a session."""
profiles = list(session.agent_loop.agent_manager.available_agents.values())
_, modes_config = make_mode_response(
profiles, session.agent_loop.agent_profile.name
)
_, models_config = make_model_response(
session.agent_loop.config.models, session.agent_loop.config.active_model
)
return [modes_config, models_config]
async def _send_config_option_update(self, session: AcpSessionLoop) -> None:
"""Push updated config options (modes, models) to the client."""
await self.client.session_update(
session_id=session.id,
update=ConfigOptionUpdate(
session_update="config_option_update",
config_options=self._build_config_options(session),
),
)
async def _handle_leanstall(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
current = list(session.agent_loop.base_config.installed_agents)
if "lean" in current:
return await self._command_reply(
session, "Lean agent is already installed.", message_id
)
VibeConfig.save_updates({"installed_agents": [*current, "lean"]})
await self._reload_session_config(session)
await self._send_config_option_update(session)
return await self._command_reply(
session,
"Lean agent installed. Start a new session to switch to Lean mode.",
message_id,
)
async def _handle_unleanstall(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
current = list(session.agent_loop.base_config.installed_agents)
if "lean" not in current:
return await self._command_reply(
session, "Lean agent is not installed.", message_id
)
VibeConfig.save_updates({
"installed_agents": [a for a in current if a != "lean"]
})
await self._reload_session_config(session)
await self._send_config_option_update(session)
return await self._command_reply(session, "Lean agent uninstalled.", message_id)
async def _handle_data_retention(
self, session: AcpSessionLoop, text_prompt: str, message_id: str
) -> PromptResponse:
return await self._command_reply(session, DATA_RETENTION_MESSAGE, message_id)
def run_acp_server() -> None:
try:

View file

@ -0,0 +1,5 @@
from __future__ import annotations
from vibe.acp.commands.registry import AcpCommandRegistry
__all__ = ["AcpCommandRegistry"]

View file

@ -0,0 +1,88 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
type OnCommandsChanged = Callable[[], Awaitable[None]]
@dataclass(frozen=True)
class AcpCommand:
"""Command advertised to ACP clients via available_commands_update."""
name: str
description: str
handler: str
input_hint: str | None = None
@dataclass
class AcpCommandRegistry:
"""Registry of ACP commands. Notifies listeners when commands change."""
_commands: dict[str, AcpCommand] = field(default_factory=dict)
_on_changed: OnCommandsChanged | None = None
def __post_init__(self) -> None:
if not self._commands:
self._commands = _build_commands()
def set_on_changed(self, callback: OnCommandsChanged) -> None:
self._on_changed = callback
@property
def commands(self) -> dict[str, AcpCommand]:
return self._commands
def get(self, name: str) -> AcpCommand | None:
return self._commands.get(name)
async def notify_changed(self) -> None:
if self._on_changed is not None:
await self._on_changed()
def _build_commands() -> dict[str, AcpCommand]:
return {
"help": AcpCommand(
name="help",
description="Show available commands and keyboard shortcuts",
handler="_handle_help",
),
"compact": AcpCommand(
name="compact",
description="Compact conversation history by summarizing",
handler="_handle_compact",
),
"reload": AcpCommand(
name="reload",
description="Reload configuration, agent instructions, and skills from disk",
handler="_handle_reload",
),
"log": AcpCommand(
name="log",
description="Show path to current session log directory",
handler="_handle_log",
),
"proxy-setup": AcpCommand(
name="proxy-setup",
description="Configure proxy and SSL certificate settings",
handler="_handle_proxy_setup",
input_hint="KEY value to set, KEY to unset, or empty for help",
),
"leanstall": AcpCommand(
name="leanstall",
description="Install the Lean 4 agent (leanstral)",
handler="_handle_leanstall",
),
"unleanstall": AcpCommand(
name="unleanstall",
description="Uninstall the Lean 4 agent",
handler="_handle_unleanstall",
),
"data-retention": AcpCommand(
name="data-retention",
description="Show data retention information",
handler="_handle_data_retention",
),
}

View file

@ -95,6 +95,11 @@ class CommandRegistry:
description="Display available MCP servers. Pass the name of a server to list its tools",
handler="_show_mcp",
),
"connectors": Command(
aliases=frozenset(["/connectors"]),
description="Manage workspace connectors. Subcommands: refresh",
handler="_handle_connectors",
),
"voice": Command(
aliases=frozenset(["/voice"]),
description="Configure voice settings",

View file

@ -126,6 +126,7 @@ from vibe.core.session.resume_sessions import (
short_session_id,
)
from vibe.core.session.session_loader import SessionLoader
from vibe.core.skills.manager import SkillManager
from vibe.core.teleport.types import (
TeleportAuthCompleteEvent,
TeleportAuthRequiredEvent,
@ -144,6 +145,8 @@ from vibe.core.tools.builtins.ask_user_question import (
Choice,
Question,
)
from vibe.core.tools.connectors import CONNECTORS_ENV_VAR, connectors_enabled
from vibe.core.tools.mcp.tools import MCPTool
from vibe.core.tools.permissions import RequiredPermission
from vibe.core.transcribe import make_transcribe_client
from vibe.core.types import (
@ -161,7 +164,6 @@ from vibe.core.utils import (
get_user_cancellation_message,
is_dangerous_directory,
)
from vibe.core.utils.io import read_safe
class BottomApp(StrEnum):
@ -332,6 +334,8 @@ class VibeApp(App): # noqa: PLR0904
excluded_commands = []
if not self.config.nuage_enabled:
excluded_commands.append("teleport")
if not connectors_enabled():
excluded_commands.append("connectors")
self.commands = CommandRegistry(excluded_commands=excluded_commands)
self._chat_input_container: ChatInputContainer | None = None
@ -376,10 +380,17 @@ class VibeApp(App): # noqa: PLR0904
def config(self) -> VibeConfig:
return self.agent_loop.config
@property
def _connectors_enabled(self) -> bool:
return connectors_enabled() and self.agent_loop.connector_registry is not None
def compose(self) -> ComposeResult:
with ChatScroll(id="chat"):
self._banner = Banner(
self.config, self.agent_loop.skill_manager, self.agent_loop.mcp_registry
config=self.config,
skill_manager=self.agent_loop.skill_manager,
mcp_registry=self.agent_loop.mcp_registry,
connector_registry=self.agent_loop.connector_registry,
)
yield self._banner
yield VerticalGroup(id="messages")
@ -774,25 +785,11 @@ class VibeApp(App): # noqa: PLR0904
]
async def _handle_skill(self, user_input: str) -> bool:
if not user_input.startswith("/"):
return False
if not self.agent_loop:
return False
parts = user_input[1:].strip().split(None, 1)
if not parts:
return False
skill_name = parts[0].lower()
skill_info = self.agent_loop.skill_manager.get_skill(skill_name)
if not skill_info:
return False
self.agent_loop.telemetry_client.send_slash_command_used(skill_name, "skill")
try:
skill_content = read_safe(skill_info.skill_path).text
skill = self.agent_loop.skill_manager.parse_skill_command(user_input)
except OSError as e:
await self._mount_and_scroll(
ErrorMessage(
@ -801,10 +798,12 @@ class VibeApp(App): # noqa: PLR0904
)
return True
if len(parts) > 1:
skill_content = f"{user_input}\n\n{skill_content}"
if skill is None:
return False
await self._handle_user_message(skill_content)
self.agent_loop.telemetry_client.send_slash_command_used(skill.name, "skill")
prompt = SkillManager.build_skill_prompt(user_input, skill)
await self._handle_user_message(prompt)
return True
async def _handle_bash_command(self, command: str) -> None:
@ -1038,12 +1037,15 @@ class VibeApp(App): # noqa: PLR0904
return
if before is not None:
await messages_area.mount_all(widgets, before=before)
return
if after is not None:
elif after is not None:
await messages_area.mount_all(widgets, after=after)
return
else:
await messages_area.mount_all(widgets)
for widget in widgets:
if isinstance(widget, StreamingMessageBase):
await widget.write_initial_content()
def _is_tool_enabled_in_main_agent(self, tool: str) -> bool:
return tool in self.agent_loop.tool_manager.available_tools
@ -1328,19 +1330,36 @@ class VibeApp(App): # noqa: PLR0904
async def _show_mcp(self, cmd_args: str = "", **kwargs: Any) -> None:
mcp_servers = self.config.mcp_servers
if not mcp_servers:
await self._mount_and_scroll(
UserCommandMessage("No MCP servers configured.")
connector_registry = (
self.agent_loop.connector_registry if self._connectors_enabled else None
)
has_connectors = (
connector_registry is not None and connector_registry.connector_count > 0
)
if not mcp_servers and not has_connectors:
msg = (
"No MCP servers or connectors configured."
if self._connectors_enabled
else "No MCP servers configured."
)
await self._mount_and_scroll(UserCommandMessage(msg))
return
if self._current_bottom_app == BottomApp.MCP:
return
name = cmd_args.strip()
if name and not any(s.name == name for s in mcp_servers):
connector_names = (
connector_registry.get_connector_names() if connector_registry else []
)
if (
name
and not any(s.name == name for s in mcp_servers)
and name not in connector_names
):
all_names = [s.name for s in mcp_servers] + connector_names
entity = "MCP server or connector" if has_connectors else "MCP server"
await self._mount_and_scroll(
ErrorMessage(
f"Unknown MCP server: {name}. Known servers: "
+ ", ".join(s.name for s in mcp_servers),
f"Unknown {entity}: {name}. Known: " + ", ".join(all_names),
collapsed=self._tools_collapsed,
)
)
@ -1351,6 +1370,49 @@ class VibeApp(App): # noqa: PLR0904
mcp_servers=mcp_servers,
tool_manager=self.agent_loop.tool_manager,
initial_server=name,
connector_registry=connector_registry,
)
)
async def _handle_connectors(self, cmd_args: str = "", **kwargs: Any) -> None:
if not self._connectors_enabled:
await self._mount_and_scroll(
UserCommandMessage(
f"Connectors are disabled. Set {CONNECTORS_ENV_VAR}=1 to enable."
)
)
return
registry = self.agent_loop.connector_registry
assert registry is not None # guaranteed by _connectors_enabled
subcmd = cmd_args.strip().lower()
match subcmd:
case "refresh":
registry.clear()
tool_manager = self.agent_loop.tool_manager
await tool_manager.integrate_connectors_async()
self.agent_loop.refresh_system_prompt()
self._refresh_banner()
count = registry.connector_count
tools = sum(
1
for cls in tool_manager.registered_tools.values()
if issubclass(cls, MCPTool) and cls.is_connector()
)
await self._mount_and_scroll(
UserCommandMessage(
f"Connectors refreshed: {count} connectors, {tools} tools"
)
)
case "":
await self._mount_and_scroll(
UserCommandMessage("Usage: /connectors refresh")
)
case _:
await self._mount_and_scroll(
ErrorMessage(
f"Unknown subcommand: {subcmd}. Available: refresh",
collapsed=self._tools_collapsed,
)
)
@ -1598,7 +1660,8 @@ class VibeApp(App): # noqa: PLR0904
base_config,
self.agent_loop.skill_manager,
self.agent_loop.mcp_registry,
plan_title(self._plan_info),
connector_registry=self.agent_loop.connector_registry,
plan_description=plan_title(self._plan_info),
)
await self._mount_and_scroll(
UserCommandMessage(
@ -2314,7 +2377,8 @@ class VibeApp(App): # noqa: PLR0904
self.config,
self.agent_loop.skill_manager,
self.agent_loop.mcp_registry,
plan_title(self._plan_info),
connector_registry=self.agent_loop.connector_registry,
plan_description=plan_title(self._plan_info),
)
def _update_profile_widgets(self, profile: AgentProfile) -> None:

View file

@ -13,14 +13,24 @@ from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.config import VibeConfig
from vibe.core.skills.manager import SkillManager
from vibe.core.tools.connectors import ConnectorRegistry
from vibe.core.tools.mcp.registry import MCPRegistry
def _pluralize(count: int, singular: str) -> str:
return f"{count} {singular}{'s' if count != 1 else ''}"
def _connector_count(registry: ConnectorRegistry | None) -> int:
return registry.connector_count if registry else 0
@dataclass
class BannerState:
active_model: str = ""
models_count: int = 0
mcp_servers_count: int = 0
connectors_count: int = 0
skills_count: int = 0
plan_description: str | None = None
@ -33,6 +43,7 @@ class Banner(Static):
config: VibeConfig,
skill_manager: SkillManager,
mcp_registry: MCPRegistry,
connector_registry: ConnectorRegistry | None = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
@ -41,6 +52,7 @@ class Banner(Static):
active_model=config.active_model,
models_count=len(config.models),
mcp_servers_count=mcp_registry.count_loaded(config.mcp_servers),
connectors_count=_connector_count(connector_registry),
skills_count=len(skill_manager.available_skills),
plan_description=None,
)
@ -85,22 +97,25 @@ class Banner(Static):
config: VibeConfig,
skill_manager: SkillManager,
mcp_registry: MCPRegistry,
connector_registry: ConnectorRegistry | None = None,
plan_description: str | None = None,
) -> None:
self.state = BannerState(
active_model=config.active_model,
models_count=len(config.models),
mcp_servers_count=mcp_registry.count_loaded(config.mcp_servers),
connectors_count=_connector_count(connector_registry),
skills_count=len(skill_manager.available_skills),
plan_description=plan_description,
)
def _format_meta_counts(self) -> str:
return (
f"{self.state.models_count} model{'s' if self.state.models_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 ''}"
)
parts = [_pluralize(self.state.models_count, "model")]
if self.state.connectors_count > 0:
parts.append(_pluralize(self.state.connectors_count, "connector"))
parts.append(_pluralize(self.state.mcp_servers_count, "MCP server"))
parts.append(_pluralize(self.state.skills_count, "skill"))
return " · ".join(parts)
def _format_plan(self) -> str:
return (

View file

@ -31,8 +31,6 @@ class ChatTextArea(TextArea):
show=False,
priority=True,
),
Binding("alt+left", "cursor_word_left", "Cursor word left", show=False),
Binding("alt+right", "cursor_word_right", "Cursor word right", show=False),
Binding("ctrl+g", "open_external_editor", "External Editor", show=False),
]

View file

@ -13,6 +13,7 @@ from textual.widgets import OptionList
from textual.widgets.option_list import Option
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.tools.connectors import ConnectorRegistry, connectors_enabled
from vibe.core.tools.mcp.tools import MCPTool
if TYPE_CHECKING:
@ -22,26 +23,34 @@ if TYPE_CHECKING:
class MCPToolIndex(NamedTuple):
server_tools: dict[str, list[tuple[str, type[MCPTool]]]]
connector_tools: dict[str, list[tuple[str, type[MCPTool]]]]
enabled_tools: dict[str, type[Any]]
def collect_mcp_tool_index(
mcp_servers: Sequence[MCPServer], tool_manager: ToolManager
mcp_servers: Sequence[MCPServer],
tool_manager: ToolManager,
connector_names: Sequence[str] = (),
) -> MCPToolIndex:
registered = tool_manager.registered_tools
available = tool_manager.available_tools
configured_servers = {server.name for server in mcp_servers}
connector_set = set(connector_names) if connectors_enabled() else set()
server_tools: dict[str, list[tuple[str, type[MCPTool]]]] = {}
connector_tools: dict[str, list[tuple[str, type[MCPTool]]]] = {}
for tool_name, cls in registered.items():
if not issubclass(cls, MCPTool):
continue
server_name = cls.get_server_name()
if server_name is None or server_name not in configured_servers:
if server_name is None:
continue
if cls.is_connector() and server_name in connector_set:
connector_tools.setdefault(server_name, []).append((tool_name, cls))
elif server_name in configured_servers:
server_tools.setdefault(server_name, []).append((tool_name, cls))
return MCPToolIndex(server_tools, enabled_tools=available)
return MCPToolIndex(server_tools, connector_tools, enabled_tools=available)
class MCPApp(Container):
@ -59,12 +68,21 @@ class MCPApp(Container):
mcp_servers: Sequence[MCPServer],
tool_manager: ToolManager,
initial_server: str = "",
connector_registry: ConnectorRegistry | None = None,
) -> None:
super().__init__(id="mcp-app")
self._mcp_servers = mcp_servers
self._connector_registry = connector_registry
connector_names = (
connector_registry.get_connector_names() if connector_registry else []
)
self._connector_names = connector_names
self._tool_manager = tool_manager
self._index = collect_mcp_tool_index(mcp_servers, tool_manager)
self._index = collect_mcp_tool_index(mcp_servers, tool_manager, connector_names)
# Track both the name and the kind ("server" or "connector") to
# disambiguate entries that share the same normalised name.
self._viewing_server: str | None = initial_server.strip() or None
self._viewing_kind: str | None = None
def compose(self) -> ComposeResult:
with Vertical(id="mcp-content"):
@ -80,8 +98,12 @@ class MCPApp(Container):
def refresh_index(self) -> None:
"""Re-snapshot the tool index (e.g. after deferred MCP discovery)."""
self._index = collect_mcp_tool_index(self._mcp_servers, self._tool_manager)
self._refresh_view(self._viewing_server)
if self._connector_registry:
self._connector_names = self._connector_registry.get_connector_names()
self._index = collect_mcp_tool_index(
self._mcp_servers, self._tool_manager, self._connector_names
)
self._refresh_view(self._viewing_server, kind=self._viewing_kind)
def on_descendant_blur(self, _event: DescendantBlur) -> None:
self.query_one(OptionList).focus()
@ -89,7 +111,9 @@ class MCPApp(Container):
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
option_id = event.option.id or ""
if option_id.startswith("server:"):
self._refresh_view(option_id.removeprefix("server:"))
self._refresh_view(option_id.removeprefix("server:"), kind="server")
elif option_id.startswith("connector:"):
self._refresh_view(option_id.removeprefix("connector:"), kind="connector")
def action_back(self) -> None:
if self._viewing_server is not None:
@ -98,50 +122,150 @@ class MCPApp(Container):
def action_close(self) -> None:
self.post_message(self.MCPClosed())
def _refresh_view(self, server_name: str | None) -> None:
# ── list view ────────────────────────────────────────────────────
def _refresh_view(
self, server_name: str | None, *, kind: str | None = None
) -> None:
index = self._index
option_list = self.query_one(OptionList)
option_list.clear_options()
server_names = {s.name for s in self._mcp_servers}
if server_name is None or server_name not in server_names:
all_names = server_names | set(self._connector_names)
if server_name is None or server_name not in all_names:
self._show_list_view(option_list, index)
return
# Infer kind when not provided (e.g. initial_server from /mcp <name>).
# Prefer server over connector when the name is ambiguous.
if kind is None:
if server_name in server_names:
kind = "server"
else:
kind = "connector"
self._show_detail_view(server_name, option_list, index, kind=kind)
def _show_list_view(self, option_list: OptionList, index: MCPToolIndex) -> None:
self._viewing_server = None
self.query_one("#mcp-title", NoMarkupStatic).update("MCP Servers")
self._viewing_kind = None
has_connectors = connectors_enabled() and bool(self._connector_names)
title = "MCP Servers & Connectors" if has_connectors else "MCP Servers"
self.query_one("#mcp-title", NoMarkupStatic).update(title)
self.query_one("#mcp-help", NoMarkupStatic).update(
"↑↓ Navigate Enter Show tools Esc Close"
)
has_servers = bool(self._mcp_servers)
# ── Local MCP Servers ──
if has_servers:
max_name = max(len(srv.name) for srv in self._mcp_servers)
max_type = max(len(srv.transport) + 2 for srv in self._mcp_servers) # +[]
option_list.add_option(
Option(
Text("Local MCP Servers", style="bold", no_wrap=True), disabled=True
)
)
for srv in self._mcp_servers:
tools = index.server_tools.get(srv.name, [])
enabled = sum(1 for t, _ in tools if t in index.enabled_tools)
status = _server_status(enabled)
type_tag = f"[{srv.transport}]"
label = Text(no_wrap=True)
label.append(srv.name)
label.append(f" [{srv.transport}] {status}")
label.append(f" {srv.name:<{max_name}}")
label.append(f" {type_tag:<{max_type}}", style="dim")
label.append(f" {_tool_count_text(enabled)}", style="dim")
option_list.add_option(Option(label, id=f"server:{srv.name}"))
if self._mcp_servers:
option_list.highlighted = 0
return
# ── Workspace Connectors ──
if has_connectors:
max_name = max(len(n) for n in self._connector_names)
type_tag = "[connector]"
type_width = len(type_tag)
tool_texts = {
n: _tool_count_text(
sum(
1
for t, _ in index.connector_tools.get(n, [])
if t in index.enabled_tools
)
)
for n in self._connector_names
}
max_tools = max(len(t) for t in tool_texts.values())
if has_servers:
option_list.add_option(Option(Text("", no_wrap=True), disabled=True))
option_list.add_option(
Option(
Text("Workspace Connectors", style="bold", no_wrap=True),
disabled=True,
)
)
for cname in self._connector_names:
connected = (
self._connector_registry.is_connected(cname)
if self._connector_registry
else False
)
label = Text(no_wrap=True)
label.append(f" {cname:<{max_name}}")
label.append(f" {type_tag:<{type_width}}", style="dim")
label.append(f" {tool_texts[cname]:<{max_tools}}", style="dim")
if connected:
label.append(" ")
label.append("", style="green")
label.append(" connected", style="dim")
else:
label.append(" ")
label.append("", style="dim")
label.append(" not connected", style="dim")
option_list.add_option(Option(label, id=f"connector:{cname}"))
if not has_servers and not has_connectors:
empty_msg = (
"No MCP servers or connectors configured"
if connectors_enabled()
else "No MCP servers configured"
)
option_list.add_option(Option(empty_msg, disabled=True))
if has_servers or has_connectors:
# Skip disabled header options (e.g. section labels).
first_enabled = next(
(i for i, opt in enumerate(option_list._options) if not opt.disabled), 0
)
option_list.highlighted = first_enabled
# ── detail view ──────────────────────────────────────────────────
def _show_detail_view(
self,
server_name: str,
option_list: OptionList,
index: MCPToolIndex,
*,
kind: str = "server",
) -> None:
self._viewing_server = server_name
self._viewing_kind = kind
is_connector = kind == "connector"
title_prefix = "Connector" if is_connector else "MCP Server"
self.query_one("#mcp-title", NoMarkupStatic).update(
f"MCP Server: {server_name}"
f"{title_prefix}: {server_name}"
)
self.query_one("#mcp-help", NoMarkupStatic).update(
"↑↓ Navigate Backspace Back Esc Close"
)
enabled_tools = [
(tool_name, cls)
for tool_name, cls in sorted(
index.server_tools.get(server_name, []), key=lambda t: t[0]
)
if tool_name in index.enabled_tools
]
if not enabled_tools:
tools_source = index.connector_tools if is_connector else index.server_tools
all_tools = sorted(tools_source.get(server_name, []), key=lambda t: t[0])
visible_tools = [(n, c) for n, c in all_tools if n in index.enabled_tools]
if not visible_tools:
option_list.add_option(
Option("No enabled tools for this server", disabled=True)
)
return
for tool_name, cls in enabled_tools:
for tool_name, cls in visible_tools:
remote_name = cls.get_remote_name()
raw_desc = (
(cls.description or "").removeprefix(f"[{server_name}] ").split("\n")[0]
@ -151,12 +275,12 @@ class MCPApp(Container):
if raw_desc:
label.append(f" - {raw_desc}")
option_list.add_option(Option(label, id=f"tool:{tool_name}"))
if enabled_tools:
if visible_tools:
option_list.highlighted = 0
def _server_status(enabled: int) -> str:
if enabled == 0:
return "unavailable"
noun = "tool" if enabled == 1 else "tools"
return f"{enabled} {noun} enabled"
def _tool_count_text(count: int) -> str:
if count == 0:
return "no tools"
noun = "tool" if count == 1 else "tools"
return f"{count} {noun}"

View file

@ -149,9 +149,7 @@ class AssistantMessage(StreamingMessageBase):
self.add_class("assistant-message")
def compose(self) -> ComposeResult:
if self._content:
self._content_initialized = True
markdown = Markdown(self._content)
markdown = Markdown("")
self._markdown = markdown
yield markdown

View file

@ -326,7 +326,27 @@ class QuestionApp(Container):
def _switch_question(self, new_idx: int) -> None:
self.current_question_idx = new_idx
self.selected_option = 0
self.selected_option = self._restore_cursor(new_idx)
def _restore_cursor(self, question_idx: int) -> int:
"""Return the option index to restore the cursor to for a previously answered question."""
q = self.questions[question_idx]
if q.multi_select:
if selections := self.multi_selections.get(question_idx):
return min(selections)
return 0
if question_idx not in self.answers:
return 0
answer_text, is_other = self.answers[question_idx]
if is_other:
return len(q.options)
return next(
(i for i, opt in enumerate(q.options) if opt.label == answer_text), 0
)
def action_next_question(self) -> None:
if self._is_other_selected:
@ -427,7 +447,32 @@ class QuestionApp(Container):
elif not has_text and other_idx in selections:
selections.discard(other_idx)
def _handle_number_key(self, event: events.Key) -> bool:
"""Handle number key press to quickly select an option. Returns True if handled."""
if self.other_input and self.other_input.has_focus:
return False
if not event.character or not event.character.isdigit():
return False
option_idx = int(event.character) - 1
# Only handle valid option indices, excluding submit button
if option_idx < 0 or option_idx >= len(self._current_question.options) + (
1 if self._has_other else 0
):
return False
event.stop()
event.prevent_default()
self.selected_option = option_idx
if not (self._has_other and option_idx == self._other_option_idx):
self.action_select()
return True
def on_key(self, event: events.Key) -> None:
if self._handle_number_key(event):
return
if len(self.questions) <= 1:
return
if self.other_input and self.other_input.has_focus:

View file

@ -6,6 +6,7 @@ import contextlib
import copy
from enum import StrEnum, auto
from http import HTTPStatus
import os
from pathlib import Path
import threading
from threading import Thread
@ -60,6 +61,7 @@ from vibe.core.tools.base import (
ToolPermission,
ToolPermissionError,
)
from vibe.core.tools.connectors import ConnectorRegistry, connectors_enabled
from vibe.core.tools.manager import ToolManager
from vibe.core.tools.mcp import MCPRegistry
from vibe.core.tools.mcp_sampling import MCPSamplingHandler
@ -100,6 +102,7 @@ from vibe.core.utils import (
TOOL_ERROR_TAG,
VIBE_STOP_EVENT_TAG,
CancellationReason,
get_server_url_from_api_base,
get_user_agent,
get_user_cancellation_message,
is_user_cancellation_event,
@ -170,6 +173,7 @@ class AgentLoop:
self._init_error: Exception | None = None
self.mcp_registry = MCPRegistry()
self.connector_registry = self._create_connector_registry()
self.agent_manager = AgentManager(
lambda: self._base_config,
initial_agent=agent_name,
@ -178,6 +182,7 @@ class AgentLoop:
self.tool_manager = ToolManager(
lambda: self.config,
mcp_registry=self.mcp_registry,
connector_registry=self.connector_registry,
defer_mcp=defer_heavy_init,
)
self.skill_manager = SkillManager(lambda: self.config)
@ -266,13 +271,13 @@ class AgentLoop:
return self._init_complete.is_set()
def _complete_init(self) -> None:
"""Run deferred heavy I/O: MCP discovery and git status.
"""Run deferred heavy I/O: MCP discovery, connectors, and git status.
Intended to be called from a background thread when
``defer_heavy_init=True`` was passed to ``__init__``.
"""
try:
self.tool_manager.integrate_mcp(raise_on_failure=True)
self.tool_manager.integrate_all(raise_on_mcp_failure=True)
system_prompt = get_universal_system_prompt(
self.tool_manager, self.config, self.skill_manager, self.agent_manager
)
@ -403,6 +408,29 @@ class AgentLoop:
terminal_emulator=terminal_emulator,
)
def _create_connector_registry(self) -> ConnectorRegistry | None:
if not connectors_enabled():
return None
provider = self._base_config.get_mistral_provider()
if provider is None:
return None
api_key_env = provider.api_key_env_var or "MISTRAL_API_KEY"
api_key = os.getenv(api_key_env, "")
if not api_key:
return None
server_url = get_server_url_from_api_base(provider.api_base)
return ConnectorRegistry(api_key=api_key, server_url=server_url)
def refresh_system_prompt(self) -> None:
"""Rebuild and replace the system prompt with current tool/skill state."""
system_prompt = get_universal_system_prompt(
self.tool_manager, self.config, self.skill_manager, self.agent_manager
)
self.messages.update_system_prompt(system_prompt)
def _select_backend(self) -> BackendLike:
active_model = self.config.get_active_model()
provider = self.config.get_provider_for_model(active_model)
@ -582,6 +610,7 @@ class AgentLoop:
"call_type": (
"main_call" if self._is_user_prompt_call else "secondary_call"
),
"call_source": "vibe_code",
}
if self._current_user_message_id is not None:
metadata["message_id"] = self._current_user_message_id
@ -1342,7 +1371,9 @@ class AgentLoop:
self._max_price = max_price
self.tool_manager = ToolManager(
lambda: self.config, mcp_registry=self.mcp_registry
lambda: self.config,
mcp_registry=self.mcp_registry,
connector_registry=self.connector_registry,
)
self.skill_manager = SkillManager(lambda: self.config)

View file

@ -319,7 +319,7 @@ class ModelConfig(BaseModel):
temperature: float = 0.2
input_price: float = 0.0 # Price per million input tokens
output_price: float = 0.0 # Price per million output tokens
thinking: Literal["off", "low", "medium", "high"] = "off"
thinking: Literal["off", "low", "medium", "high", "max"] = "off"
auto_compact_threshold: int = 200_000
_default_alias_to_name = model_validator(mode="before")(_default_alias_to_name)

View file

@ -321,12 +321,14 @@ class AnthropicAdapter(APIAdapter):
BETA_FEATURES = (
"interleaved-thinking-2025-05-14,"
"fine-grained-tool-streaming-2025-05-14,"
"prompt-caching-2024-07-31"
"prompt-caching-2024-07-31,"
"context-1m-2025-08-07"
)
THINKING_BUDGETS: ClassVar[dict[str, int]] = {
"low": 1024,
"medium": 10_000,
"high": 32_000,
"max": 128_000,
}
DEFAULT_ADAPTIVE_MAX_TOKENS: ClassVar[int] = 32_768
DEFAULT_MAX_TOKENS = 8192

View file

@ -175,6 +175,7 @@ _THINKING_TO_REASONING_EFFORT: dict[str, ReasoningEffortValue] = {
"low": "none",
"medium": "high",
"high": "high",
"max": "high",
}
@ -283,9 +284,10 @@ class MistralBackend:
reasoning_effort=reasoning_effort,
)
message = response.choices[0].message
parsed = (
self._mapper.parse_content(response.choices[0].message.content)
if response.choices[0].message.content
self._mapper.parse_content(message.content)
if message and message.content
else ParsedContent(content="", reasoning_content=None)
)
return LLMChunk(
@ -293,10 +295,8 @@ class MistralBackend:
role=Role.assistant,
content=parsed.content,
reasoning_content=parsed.reasoning_content,
tool_calls=self._mapper.parse_tool_calls(
response.choices[0].message.tool_calls
)
if response.choices[0].message.tool_calls
tool_calls=self._mapper.parse_tool_calls(message.tool_calls)
if message and message.tool_calls
else None,
),
usage=LLMUsage(

View file

@ -106,4 +106,5 @@ Prioritize technical accuracy over validating beliefs. Disagree when necessary.
When uncertain, investigate before confirming.
Your output must contain zero emoji. This includes smiley faces, icons, flags, symbols like ✅❌💡, and all other Unicode emoji.
No over-the-top validation.
Stay focused on solving the problem regardless of user tone. Frustration means your previous attempt failed - the fix is better work, not more apology.
Stay focused on solving the problem regardless of user tone. Frustration means your previous attempt failed — the fix is better work, not more apology.
Other: requests unrelated to code → respond helpfully as a general assistant.

View file

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.logger import logger
from vibe.core.skills.models import SkillInfo, SkillMetadata
from vibe.core.skills.models import ParsedSkillCommand, SkillInfo, SkillMetadata
from vibe.core.skills.parser import SkillParseError, parse_frontmatter
from vibe.core.utils import name_matches
from vibe.core.utils.io import read_safe
@ -127,3 +127,32 @@ class SkillManager:
def get_skill(self, name: str) -> SkillInfo | None:
return self.available_skills.get(name)
def parse_skill_command(self, text_prompt: str) -> ParsedSkillCommand | None:
stripped = text_prompt.strip()
if not stripped.startswith("/"):
return None
parts = stripped[1:].split(None, 1)
if not parts:
return None
skill_name = parts[0].lower()
skill_info = self.get_skill(skill_name)
if skill_info is None:
return None
skill_content = read_safe(skill_info.skill_path).text
extra_instructions = parts[1] if len(parts) > 1 else None
return ParsedSkillCommand(
name=skill_name,
content=skill_content,
extra_instructions=extra_instructions,
)
@staticmethod
def build_skill_prompt(text_prompt: str, parsed: ParsedSkillCommand) -> str:
if parsed.extra_instructions is not None:
return f"{text_prompt}\n\n{parsed.content}"
return parsed.content

View file

@ -90,3 +90,9 @@ class SkillInfo(BaseModel):
user_invocable=meta.user_invocable,
skill_path=skill_path.resolve(),
)
class ParsedSkillCommand(BaseModel):
name: str
content: str
extra_instructions: str | None = None

View file

@ -1,5 +1,6 @@
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
import html
import os
from pathlib import Path
@ -37,74 +38,35 @@ class ProjectContextProvider:
_git_status_cache[self.root_path] = result
return result
def _fetch_git_status(self) -> str:
try:
timeout = min(self.config.timeout_seconds, 10.0)
num_commits = self.config.default_commit_count
current_branch = subprocess.run(
["git", "branch", "--show-current"],
def _run_git(
self, args: list[str], timeout: float
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
capture_output=True,
check=True,
cwd=self.root_path,
stdin=subprocess.DEVNULL if is_windows() else None,
text=True,
timeout=timeout,
).stdout.strip()
)
main_branch = "main"
try:
branches_output = subprocess.run(
["git", "branch", "-r"],
capture_output=True,
check=True,
cwd=self.root_path,
stdin=subprocess.DEVNULL if is_windows() else None,
text=True,
timeout=timeout,
).stdout
if "origin/master" in branches_output:
main_branch = "master"
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
pass
status_output = subprocess.run(
["git", "status", "--porcelain"],
capture_output=True,
check=True,
cwd=self.root_path,
stdin=subprocess.DEVNULL if is_windows() else None,
text=True,
timeout=timeout,
).stdout.strip()
if status_output:
@staticmethod
def _format_git_status(status_output: str) -> str:
if not status_output:
return "(clean)"
status_lines = status_output.splitlines()
MAX_GIT_STATUS_SIZE = 50
if len(status_lines) > MAX_GIT_STATUS_SIZE:
status = (
f"({len(status_lines)} changes - use 'git status' for details)"
)
else:
status = f"({len(status_lines)} changes)"
else:
status = "(clean)"
return f"({len(status_lines)} changes - use 'git status' for details)"
return f"({len(status_lines)} changes)"
log_output = subprocess.run(
["git", "log", "--oneline", f"-{num_commits}", "--decorate"],
capture_output=True,
check=True,
cwd=self.root_path,
stdin=subprocess.DEVNULL if is_windows() else None,
text=True,
timeout=timeout,
).stdout.strip()
recent_commits = []
@staticmethod
def _parse_git_log(log_output: str) -> list[str]:
recent_commits: list[str] = []
for line in log_output.split("\n"):
if not (line := line.strip()):
continue
if " " in line:
commit_hash, commit_msg = line.split(" ", 1)
if (
@ -116,6 +78,39 @@ class ProjectContextProvider:
recent_commits.append(f"{commit_hash} {commit_msg}")
else:
recent_commits.append(line)
return recent_commits
def _fetch_git_status(self) -> str:
try:
timeout = min(self.config.timeout_seconds, 10.0)
num_commits = self.config.default_commit_count
with ThreadPoolExecutor(max_workers=4) as pool:
branch_future = pool.submit(
self._run_git, ["branch", "--show-current"], timeout
)
remote_future = pool.submit(self._run_git, ["branch", "-r"], timeout)
status_future = pool.submit(
self._run_git, ["status", "--porcelain"], timeout
)
log_future = pool.submit(
self._run_git,
["log", "--oneline", f"-{num_commits}", "--decorate"],
timeout,
)
current_branch = branch_future.result().stdout.strip()
main_branch = "main"
try:
branches_output = remote_future.result().stdout
if "origin/master" in branches_output:
main_branch = "master"
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
pass
status = self._format_git_status(status_future.result().stdout.strip())
recent_commits = self._parse_git_log(log_future.result().stdout.strip())
git_info_parts = [
f"Current branch: {current_branch}",

View file

@ -16,7 +16,7 @@ from vibe.core.utils import get_server_url_from_api_base, get_user_agent
if TYPE_CHECKING:
from vibe.core.agent_loop import ToolDecision
_DEFAULT_TELEMETRY_BASE_URL = "https://codestral.mistral.ai"
_DEFAULT_TELEMETRY_BASE_URL = "https://api.mistral.ai"
_DATALAKE_EVENTS_PATH = "/v1/datalake/events"

View file

@ -0,0 +1,14 @@
from __future__ import annotations
import os
from vibe.core.tools.connectors.connector_registry import ConnectorRegistry
CONNECTORS_ENV_VAR = "EXPERIMENTAL_ENABLE_CONNECTORS"
def connectors_enabled() -> bool:
return os.getenv(CONNECTORS_ENV_VAR) == "1"
__all__ = ["CONNECTORS_ENV_VAR", "ConnectorRegistry", "connectors_enabled"]

View file

@ -0,0 +1,417 @@
from __future__ import annotations
import asyncio
from collections.abc import AsyncGenerator
import re
from typing import TYPE_CHECKING, Any, ClassVar
import httpx
from mistralai.client import Mistral
from mistralai.client.models.connectorsqueryfilters import (
ConnectorsQueryFiltersTypedDict,
)
from vibe.core.logger import logger
from vibe.core.tools.base import (
BaseTool,
BaseToolConfig,
BaseToolState,
InvokeContext,
ToolError,
)
from vibe.core.tools.mcp.tools import (
MCPTool,
MCPToolResult,
RemoteTool,
_OpenArgs,
call_tool_http,
)
from vibe.core.tools.ui import ToolResultDisplay
from vibe.core.types import ToolStreamEvent
from vibe.core.utils import run_sync
if TYPE_CHECKING:
from vibe.core.types import ToolResultEvent
_LIST_PAGE_SIZE = 100
_LIST_QUERY_FILTERS: ConnectorsQueryFiltersTypedDict = {"active": True}
_TOOL_FETCH_TIMEOUT = 5.0
def _normalize_name(name: str) -> str:
normalized = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
result = normalized.strip("_-")[:256]
return result or "unnamed"
def _connector_tool_to_remote(tool: dict[str, Any] | Any) -> RemoteTool | None:
"""Convert a ConnectorTool (SDK object or raw dict) to a RemoteTool."""
if isinstance(tool, dict):
name = tool.get("name")
description = tool.get("description")
schema = tool.get("jsonschema") or tool.get("json_schema")
else:
name = getattr(tool, "name", None)
description = getattr(tool, "description", None)
schema = getattr(tool, "jsonschema", None) or getattr(tool, "json_schema", None)
if not name:
return None
if schema is not None and not isinstance(schema, dict):
dump = getattr(schema, "model_dump", None)
schema = dump() if callable(dump) else None
return RemoteTool.model_validate({
"name": name,
"description": description,
"inputSchema": schema or {"type": "object", "properties": {}},
})
_DEFAULT_BASE_URL = "https://api.mistral.ai"
def _format_http_status_error(
exc: httpx.HTTPStatusError, connector_name: str, connector_id: str
) -> str:
"""Format an HTTP status error with response body for debugging."""
status = exc.response.status_code
connector_ref = f"'{connector_name}' (id: {connector_id})"
try:
body = exc.response.text[:500]
except Exception:
body = ""
match status:
case 401 | 403:
detail = (
f"Connector {connector_ref} authentication failed "
f"(HTTP {status}). Check your MISTRAL_API_KEY."
)
case 404:
detail = (
f"Connector {connector_ref} not found (HTTP 404). "
"It may have been deleted or is not accessible."
)
case _:
detail = f"Connector {connector_ref} request failed (HTTP {status})."
if body:
detail += f"\nServer response: {body}"
return detail
def _unwrap_http_status_error(exc: Exception) -> httpx.HTTPStatusError | None:
"""Extract an HTTPStatusError from an exception or ExceptionGroup."""
if isinstance(exc, httpx.HTTPStatusError):
return exc
if isinstance(exc, ExceptionGroup):
for inner in exc.exceptions:
if found := _unwrap_http_status_error(inner):
return found
if cause := exc.__cause__:
if isinstance(cause, httpx.HTTPStatusError):
return cause
return None
def _connector_error_message(
exc: Exception, connector_id: str, connector_name: str
) -> str:
"""Return an actionable error message for connector proxy failures."""
if http_err := _unwrap_http_status_error(exc):
return _format_http_status_error(http_err, connector_name, connector_id)
if isinstance(exc, httpx.TimeoutException):
return (
f"Connector '{connector_name}' timed out. "
"The remote service may be slow or unreachable."
)
if isinstance(exc, httpx.ConnectError):
return (
f"Cannot reach connector proxy for '{connector_name}'. "
"Check your network connection."
)
if isinstance(exc, ExceptionGroup):
messages = [str(e) for e in exc.exceptions]
return (
f"Connector '{connector_name}' call failed with multiple errors: "
+ "; ".join(messages)
)
return f"Connector '{connector_name}' call failed: {exc}"
def create_connector_proxy_tool_class(
*,
connector_name: str,
connector_alias: str,
connector_id: str,
remote: RemoteTool,
api_key: str,
server_url: str | None = None,
) -> type[BaseTool[_OpenArgs, MCPToolResult, BaseToolConfig, BaseToolState]]:
alias = connector_alias
published_name = f"connector_{alias}_{remote.name}"
base_url = server_url or _DEFAULT_BASE_URL
class ConnectorProxyTool(MCPTool):
description: ClassVar[str] = f"[{alias}] " + (
remote.description or f"Connector tool '{remote.name}'"
)
_server_name: ClassVar[str] = alias
_remote_name: ClassVar[str] = remote.name
_input_schema: ClassVar[dict[str, Any]] = remote.input_schema
_is_connector: ClassVar[bool] = True
_connector_id: ClassVar[str] = connector_id
_connector_name: ClassVar[str] = connector_name
_api_key: ClassVar[str] = api_key
_base_url: ClassVar[str] = base_url
@classmethod
def get_name(cls) -> str:
return published_name
@classmethod
def get_parameters(cls) -> dict[str, Any]:
return dict(cls._input_schema)
async def run(
self, args: _OpenArgs, ctx: InvokeContext | None = None
) -> AsyncGenerator[ToolStreamEvent | MCPToolResult, None]:
url = (
f"{self._base_url}/v1/experimental/connectors/{self._connector_id}/mcp"
)
headers = {"Authorization": f"Bearer {self._api_key}"}
payload = args.model_dump(exclude_none=True)
try:
yield await call_tool_http(
url, self._remote_name, payload, headers=headers
)
except Exception as exc:
msg = _connector_error_message(
exc, self._connector_id, self._connector_name
)
raise ToolError(msg) from exc
@classmethod
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
if not isinstance(event.result, MCPToolResult):
return ToolResultDisplay(
success=False,
message=event.error or event.skip_reason or "No result",
)
message = f"Connector tool {event.result.tool} completed"
return ToolResultDisplay(success=event.result.ok, message=message)
@classmethod
def get_status_text(cls) -> str:
return f"Calling connector tool {remote.name}"
ConnectorProxyTool.__name__ = f"Connector_{alias}__{remote.name}"
return ConnectorProxyTool
class ConnectorRegistry:
"""Discovers connector tools from the Mistral API.
Fetches all connectors and their tools on first call, then caches.
"""
def __init__(self, api_key: str, server_url: str | None = None) -> None:
self._api_key = api_key
self._server_url = server_url
self._cache: dict[str, dict[str, type[BaseTool]]] | None = None
self._connector_names: list[str] = []
self._connector_connected: dict[str, bool] = {}
self._discover_lock = asyncio.Lock()
def _get_client(self) -> Mistral:
return Mistral(api_key=self._api_key, server_url=self._server_url)
def get_tools(self) -> dict[str, type[BaseTool]]:
"""Return proxy tool classes for all connectors, using cache when possible."""
return run_sync(self.get_tools_async())
async def get_tools_async(self) -> dict[str, type[BaseTool]]:
"""Return proxy tool classes for all connectors, using cache when possible."""
if self._cache is not None:
result: dict[str, type[BaseTool]] = {}
for tools in self._cache.values():
result.update(tools)
return result
return await self._discover_all()
async def _discover_all(self) -> dict[str, type[BaseTool]]:
async with self._discover_lock:
# Re-check under lock — another coroutine may have finished
# discovery while we waited.
if self._cache is not None:
result: dict[str, type[BaseTool]] = {}
for tools in self._cache.values():
result.update(tools)
return result
async with self._get_client() as client:
connectors: list[Any] = []
try:
page = await client.beta.connectors.list_async(
page_size=_LIST_PAGE_SIZE, query_filters=_LIST_QUERY_FILTERS
)
connectors.extend(page.items or [])
while page.pagination and page.pagination.next_cursor:
page = await client.beta.connectors.list_async(
page_size=_LIST_PAGE_SIZE,
cursor=page.pagination.next_cursor,
query_filters=_LIST_QUERY_FILTERS,
)
connectors.extend(page.items or [])
except Exception as exc:
logger.warning(f"Failed to list connectors: {exc}")
self._cache = {}
self._connector_names = []
self._connector_connected = {}
return {}
# Build results locally to avoid publishing incomplete state.
cache: dict[str, dict[str, type[BaseTool]]] = {}
all_tools: dict[str, type[BaseTool]] = {}
connector_names: list[str] = []
connector_connected: dict[str, bool] = {}
# Deduplicate by normalized name, preserving order.
# When two connectors share the same alias, disambiguate
# with a numeric suffix rather than silently dropping.
seen_names: set[str] = set()
unique_connectors: list[tuple[str, str, Any]] = []
for connector in connectors:
connector_id = str(connector.id)
raw_name = connector.name or connector_id
alias = _normalize_name(raw_name)
if alias in seen_names:
original = alias
suffix = 2
while f"{alias}_{suffix}" in seen_names:
suffix += 1
alias = f"{alias}_{suffix}"
logger.warning(
f"Connector {raw_name!r} alias {original!r} collides, "
f"using {alias!r}"
)
seen_names.add(alias)
unique_connectors.append((connector_id, alias, connector))
# Fetch tools for all connectors in parallel.
tasks = [
self._fetch_connector_tools(client, c, alias)
for _, alias, c in unique_connectors
]
results = await asyncio.gather(*tasks)
for (connector_id, alias, _), tools_map in zip(
unique_connectors, results, strict=True
):
connector_names.append(alias)
if tools_map is None:
# Timeout or error — show in UI but don't cache so
# a refresh will retry discovery for this connector.
connector_connected[alias] = False
continue
cache[connector_id] = tools_map
all_tools.update(tools_map)
# TODO: replace with actual API field when available
connector_connected[alias] = bool(tools_map)
# Publish atomically — concurrent callers waiting on the
# lock will see the completed cache.
self._connector_names = connector_names
self._connector_connected = connector_connected
self._cache = cache
return all_tools
async def _fetch_connector_tools(
self, client: Mistral, connector: Any, connector_alias: str
) -> dict[str, type[BaseTool]] | None:
"""Fetch tools for a single connector via ``list_tools_async``.
The list endpoint does not always include tools inline, so we
call ``list_tools_async`` per connector to get the full set.
"""
connector_id = str(connector.id)
name = connector.name or connector_id
try:
response = await asyncio.wait_for(
client.beta.connectors.list_tools_async(
connector_id_or_name=connector_id
),
timeout=_TOOL_FETCH_TIMEOUT,
)
except TimeoutError:
logger.warning(
f"Timeout fetching tools for connector {name} (>{_TOOL_FETCH_TIMEOUT}s)"
)
return None
except Exception as exc:
logger.warning(f"Failed to list tools for connector {name}: {exc}")
return None
# The SDK may return a plain list, an iterable wrapper, or an
# object with a `data`/`items` attribute. Handle all cases.
if isinstance(response, list):
raw_tools: list[Any] = response
elif hasattr(response, "data"):
raw_tools = list(response.data or [])
elif hasattr(response, "items"):
raw_tools = list(response.items or [])
else:
try:
raw_tools = list(response)
except TypeError:
logger.warning(
f"Unexpected response type from list_tools_async for {name}: "
f"{type(response).__name__}"
)
raw_tools = []
result: dict[str, type[BaseTool]] = {}
for tool in raw_tools:
remote = _connector_tool_to_remote(tool)
if remote is None:
continue
try:
proxy_cls = create_connector_proxy_tool_class(
connector_name=name,
connector_alias=connector_alias,
connector_id=connector_id,
remote=remote,
api_key=self._api_key,
server_url=self._server_url,
)
result[proxy_cls.get_name()] = proxy_cls
except Exception as exc:
tool_name = (
tool.get("name")
if isinstance(tool, dict)
else getattr(tool, "name", "<unknown>")
)
logger.warning(
f"Failed to register connector tool {tool_name} for {name}: {exc}"
)
return result
@property
def connector_count(self) -> int:
if self._cache is None:
return 0
return len(self._connector_names)
def get_connector_names(self) -> list[str]:
return list(self._connector_names)
def is_connected(self, name: str) -> bool:
return self._connector_connected.get(name, False)
def clear(self) -> None:
self._cache = None
self._connector_names = []
self._connector_connected = {}

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Iterator
import hashlib
import importlib.util
@ -14,8 +15,10 @@ from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.logger import logger
from vibe.core.paths import DEFAULT_TOOL_DIR
from vibe.core.tools.base import BaseTool, BaseToolConfig
from vibe.core.tools.connectors import ConnectorRegistry
from vibe.core.tools.mcp import MCPRegistry
from vibe.core.utils import name_matches
from vibe.core.tools.mcp.tools import MCPTool
from vibe.core.utils import name_matches, run_sync
if TYPE_CHECKING:
from vibe.core.config import VibeConfig
@ -70,11 +73,13 @@ class ToolManager:
self,
config_getter: Callable[[], VibeConfig],
mcp_registry: MCPRegistry | None = None,
connector_registry: ConnectorRegistry | None = None,
*,
defer_mcp: bool = False,
) -> None:
self._config_getter = config_getter
self._mcp_registry = mcp_registry or MCPRegistry()
self._connector_registry = connector_registry
self._instances: dict[str, BaseTool] = {}
self._search_paths: list[Path] = self._compute_search_paths(self._config)
self._lock = threading.Lock()
@ -84,7 +89,7 @@ class ToolManager:
cls.get_name(): cls for cls in self._iter_tool_classes(self._search_paths)
}
if not defer_mcp:
self.integrate_mcp()
self.integrate_all()
@property
def _config(self) -> VibeConfig:
@ -207,18 +212,24 @@ class ToolManager:
return runtime_available
def integrate_mcp(self, *, raise_on_failure: bool = False) -> None:
"""Discover and register MCP tools.
"""Discover and register MCP tools (sync wrapper).
Idempotent: subsequent calls after a successful integration are
no-ops to avoid redundant MCP discovery.
"""
run_sync(self._integrate_mcp_async(raise_on_failure=raise_on_failure))
async def _integrate_mcp_async(self, *, raise_on_failure: bool = False) -> None:
"""Async MCP discovery — canonical implementation."""
if self._mcp_integrated:
return
if not self._config.mcp_servers:
return
try:
mcp_tools = self._mcp_registry.get_tools(self._config.mcp_servers)
mcp_tools = await self._mcp_registry.get_tools_async(
self._config.mcp_servers
)
except Exception as exc:
logger.warning("MCP integration failed: %s", exc)
if raise_on_failure:
@ -232,6 +243,71 @@ class ToolManager:
"MCP integration registered %d tools (via registry)", len(mcp_tools)
)
def _purge_connector_state(self) -> None:
"""Remove stale connector tool classes and cached instances."""
stale_keys = [
name
for name, cls in self._available.items()
if issubclass(cls, MCPTool) and cls.is_connector()
]
for key in stale_keys:
self._available.pop(key, None)
self._instances.pop(key, None)
def integrate_connectors(self) -> None:
"""Discover and register connector tools (sync wrapper)."""
run_sync(self.integrate_connectors_async())
async def integrate_connectors_async(self) -> None:
"""Discover and register connector tools — canonical implementation.
Thread-safe: can be called from the deferred-init background thread.
"""
if self._connector_registry is None:
return
try:
connector_tools = await self._connector_registry.get_tools_async()
except Exception as exc:
logger.warning(f"Connector integration failed: {exc}")
with self._lock:
self._purge_connector_state()
return
with self._lock:
self._purge_connector_state()
self._available.update(connector_tools)
logger.info(f"Connector integration registered {len(connector_tools)} tools")
def integrate_all(self, *, raise_on_mcp_failure: bool = False) -> None:
"""Discover MCP and connector tools in parallel.
Runs both async discovery paths concurrently via ``asyncio.gather``
inside a single ``run_sync`` call.
"""
run_sync(self._integrate_all_async(raise_on_mcp_failure=raise_on_mcp_failure))
async def _integrate_all_async(self, *, raise_on_mcp_failure: bool = False) -> None:
"""Run MCP and connector discovery concurrently.
Uses ``return_exceptions=True`` so that a failing MCP server does
not cancel in-flight connector discovery (or vice-versa).
"""
mcp_result, connector_result = await asyncio.gather(
self._integrate_mcp_async(raise_on_failure=raise_on_mcp_failure),
self.integrate_connectors_async(),
return_exceptions=True,
)
# Re-raise MCP errors when the caller asked for them.
if isinstance(mcp_result, BaseException):
if raise_on_mcp_failure:
raise mcp_result
logger.warning(f"MCP integration failed: {mcp_result}")
if isinstance(connector_result, BaseException):
logger.warning(f"Connector integration failed: {connector_result}")
def get_tool_config(self, tool_name: str) -> BaseToolConfig:
with self._lock:
tool_class = self._available.get(tool_name)

View file

@ -36,6 +36,12 @@ class MCPRegistry:
def get_tools(self, servers: list[MCPServer]) -> dict[str, type[BaseTool]]:
"""Return proxy tool classes for *servers*, using cache when possible."""
return run_sync(self.get_tools_async(servers))
async def get_tools_async(
self, servers: list[MCPServer]
) -> dict[str, type[BaseTool]]:
"""Async variant of :meth:`get_tools`."""
result: dict[str, type[BaseTool]] = {}
to_discover: list[tuple[str, MCPServer]] = []
@ -47,7 +53,7 @@ class MCPRegistry:
to_discover.append((key, srv))
if to_discover:
discovered = run_sync(self._discover_all(to_discover))
discovered = await self._discover_all(to_discover)
result.update(discovered)
return result

View file

@ -77,6 +77,7 @@ class MCPTool(
):
_server_name: ClassVar[str] = ""
_remote_name: ClassVar[str] = ""
_is_connector: ClassVar[bool] = False
@classmethod
def get_server_name(cls) -> str | None:
@ -86,6 +87,10 @@ class MCPTool(
def get_remote_name(cls) -> str:
return cls._remote_name or cls.get_name()
@classmethod
def is_connector(cls) -> bool:
return cls._is_connector
class RemoteTool(BaseModel):
model_config = ConfigDict(from_attributes=True)

View file

@ -14,6 +14,7 @@ from vibe.core.utils.concurrency import (
from vibe.core.utils.display import compact_reduction_display
from vibe.core.utils.http import get_server_url_from_api_base, get_user_agent
from vibe.core.utils.matching import name_matches
from vibe.core.utils.merge import MergeConflictError, MergeStrategy
from vibe.core.utils.paths import is_dangerous_directory
from vibe.core.utils.platform import is_windows
from vibe.core.utils.retry import async_generator_retry, async_retry
@ -39,6 +40,8 @@ __all__ = [
"AsyncExecutor",
"CancellationReason",
"ConversationLimitException",
"MergeConflictError",
"MergeStrategy",
"TaggedText",
"async_generator_retry",
"async_retry",

View file

@ -15,5 +15,5 @@ def get_user_agent(backend: Backend | None) -> str:
def get_server_url_from_api_base(api_base: str) -> str | None:
match = re.match(r"(https?://[^/]+)(/v.*)", api_base)
match = re.match(r"(https?://.+)(/v\d+.*)", api_base)
return match.group(1) if match else None

104
vibe/core/utils/merge.py Normal file
View file

@ -0,0 +1,104 @@
from __future__ import annotations
from collections.abc import Callable
from enum import StrEnum, auto
from typing import Any
class MergeConflictError(Exception):
"""Raised when CONFLICT strategy detects two layers both providing a value."""
def __init__(self, field_name: str = "") -> None:
msg = (
f"Merge conflict on field '{field_name}'"
if field_name
else "Merge conflict"
)
super().__init__(msg)
self.field_name = field_name
class MergeStrategy(StrEnum):
REPLACE = auto()
CONCAT = auto()
UNION = auto()
MERGE = auto()
CONFLICT = auto()
def apply(
self, base: Any, override: Any, *, key_fn: Callable[[Any], str] | None = None
) -> Any:
"""Combine base and override according to this strategy."""
if self is MergeStrategy.REPLACE:
return self._replace(base, override)
if self is MergeStrategy.CONCAT:
return self._concat(base, override)
if self is MergeStrategy.UNION:
return self._union(base, override, key_fn)
if self is MergeStrategy.MERGE:
return self._merge(base, override)
if self is MergeStrategy.CONFLICT:
return self._conflict(base, override)
raise NotImplementedError(f"Merge strategy {self!r} is not implemented")
def _coalesce(self, base: Any, override: Any) -> tuple[bool, Any]:
"""Return the non-None operand when at most one side is present.
Returns a (resolved, value) pair:
- (True, <value>) one or both sides are None; use value as-is.
- (False, None) both sides are present; caller must merge them.
"""
if base is None:
return True, override
if override is None:
return True, base
return False, None
def _replace(self, base: Any, override: Any) -> Any:
resolved, value = self._coalesce(base, override)
return value if resolved else override
def _concat(self, base: Any, override: Any) -> Any:
resolved, value = self._coalesce(base, override)
if resolved:
return value
if not isinstance(base, list) or not isinstance(override, list):
raise TypeError(
f"CONCAT requires list operands, got {type(base).__name__} and {type(override).__name__}"
)
return base + override
def _union(
self, base: Any, override: Any, key_fn: Callable[[Any], str] | None
) -> Any:
resolved, value = self._coalesce(base, override)
if resolved:
return value
if key_fn is None:
raise ValueError("UNION strategy requires key_fn")
if not isinstance(base, list) or not isinstance(override, list):
raise TypeError(
f"UNION requires list operands, got {type(base).__name__} and {type(override).__name__}"
)
merged: dict[str, Any] = {}
for item in base:
merged[key_fn(item)] = item
for item in override:
merged[key_fn(item)] = item
return list(merged.values())
def _merge(self, base: Any, override: Any) -> Any:
resolved, value = self._coalesce(base, override)
if resolved:
return value
if not isinstance(base, dict) or not isinstance(override, dict):
raise TypeError(
f"MERGE requires dict operands, got {type(base).__name__} and {type(override).__name__}"
)
return {**base, **override}
def _conflict(self, base: Any, override: Any) -> Any:
resolved, value = self._coalesce(base, override)
if resolved:
return value
raise MergeConflictError()

View file

@ -1,4 +1,3 @@
# What's new in v2.7.5
# What's new in v2.7.6
- **Trust dialog**: Display detected files and LLM risks in the trust folder dialog
- **Faster startup**: Deferred MCP and git I/O to a background thread for faster CLI startup
- **Streaming fix**: Fixed markdown fence context loss causing streaming rendering problems