v2.7.6 (#600)
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>
2
.vscode/launch.json
vendored
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.7.5",
|
||||
"version": "2.7.6",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ACP Server",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
25
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 == []
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="622.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</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 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 servers 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 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 servers 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 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  [stdio]  1 tool 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  [http]  1 tool 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 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 MCP 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)">  filesystem</text><text class="terminal-r8" x="183" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">  [stdio]</text><text class="terminal-r8" x="292.8" y="752" textLength="97.6" clip-path="url(#terminal-line-30)">  1 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)">  search    </text><text class="terminal-r9" x="183" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">  [http] </text><text class="terminal-r9" x="292.8" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">  1 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)">↑↓ Navigate  Enter Show tools  Esc 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="622.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="183" clip-path="url(#terminal-line-16)">devstral-latest</text><text class="terminal-r1" x="622.2" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</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 servers 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 servers 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 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  [stdio]  1 tool 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  [stdio]  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  [http]  1 tool 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 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 MCP 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)">  filesystem   </text><text class="terminal-r8" x="219.6" y="727.6" textLength="109.8" clip-path="url(#terminal-line-29)">  [stdio]</text><text class="terminal-r8" x="329.4" y="727.6" textLength="97.6" clip-path="url(#terminal-line-29)">  1 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)">  broken-server</text><text class="terminal-r9" x="219.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">  [stdio]</text><text class="terminal-r9" x="329.4" y="752" textLength="122" clip-path="url(#terminal-line-30)">  no 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)">  search       </text><text class="terminal-r9" x="219.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">  [http] </text><text class="terminal-r9" x="329.4" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">  1 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)">↑↓ Navigate  Enter Show tools  Esc 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="361.6" textLength="183" clip-path="url(#terminal-line-14)">devstral-latest</text><text class="terminal-r1" x="622.2" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="386" textLength="585.6" clip-path="url(#terminal-line-15)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type </text><text class="terminal-r3" x="231.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="292.8" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> for more information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</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 servers 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 Servers & 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 MCP 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)">  filesystem</text><text class="terminal-r8" x="183" y="678.8" textLength="109.8" clip-path="url(#terminal-line-27)">  [stdio]</text><text class="terminal-r8" x="292.8" y="678.8" textLength="97.6" clip-path="url(#terminal-line-27)">  1 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 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)">  gmail</text><text class="terminal-r9" x="122" y="752" textLength="158.6" clip-path="url(#terminal-line-30)">  [connector]</text><text class="terminal-r9" x="280.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">  3 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)"> 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)">  slack</text><text class="terminal-r9" x="122" y="776.4" textLength="158.6" clip-path="url(#terminal-line-31)">  [connector]</text><text class="terminal-r9" x="280.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">  2 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)"> 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)">↑↓ Navigate  Enter Show tools  Esc 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="622.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="597.8" clip-path="url(#terminal-line-18)">1 model · 2 connectors · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</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 servers 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 Servers & 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 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)">  gmail</text><text class="terminal-r8" x="122" y="752" textLength="158.6" clip-path="url(#terminal-line-30)">  [connector]</text><text class="terminal-r8" x="280.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">  3 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)"> 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)">  slack</text><text class="terminal-r10" x="122" y="776.4" textLength="158.6" clip-path="url(#terminal-line-31)">  [connector]</text><text class="terminal-r10" x="280.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">  2 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)"> 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)">↑↓ Navigate  Enter Show tools  Esc 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="585.6" clip-path="url(#terminal-line-19)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</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 servers 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: 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)">  -  Search Slack 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)">  -  Send a Slack 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)">↑↓ Navigate  Backspace Back  Esc 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="622.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</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 servers 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 servers 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 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  [stdio]  1 tool 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  [http]  1 tool 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 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 MCP 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)">  filesystem</text><text class="terminal-r8" x="183" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">  [stdio]</text><text class="terminal-r8" x="292.8" y="752" textLength="97.6" clip-path="url(#terminal-line-30)">  1 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)">  search    </text><text class="terminal-r9" x="183" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">  [http] </text><text class="terminal-r9" x="292.8" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">  1 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)">↑↓ Navigate  Enter Show tools  Esc 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">devstral-latest</text><text class="terminal-r1" x="622.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="434.8" textLength="183" clip-path="url(#terminal-line-17)">devstral-latest</text><text class="terminal-r1" x="622.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</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 servers 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 servers 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 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  [stdio]  1 tool 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  [http]  1 tool 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 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 MCP 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)">  filesystem</text><text class="terminal-r8" x="183" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">  [stdio]</text><text class="terminal-r8" x="292.8" y="752" textLength="97.6" clip-path="url(#terminal-line-30)">  1 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)">  search    </text><text class="terminal-r9" x="183" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">  [http] </text><text class="terminal-r9" x="292.8" y="776.4" textLength="97.6" clip-path="url(#terminal-line-31)">  1 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)">↑↓ Navigate  Enter Show tools  Esc 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="361.6" textLength="183" clip-path="url(#terminal-line-14)">devstral-latest</text><text class="terminal-r1" x="622.2" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="386" textLength="585.6" clip-path="url(#terminal-line-15)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type </text><text class="terminal-r3" x="231.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="292.8" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> for more information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</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 servers 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 Servers & 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 MCP 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)">  filesystem</text><text class="terminal-r8" x="183" y="678.8" textLength="109.8" clip-path="url(#terminal-line-27)">  [stdio]</text><text class="terminal-r8" x="292.8" y="678.8" textLength="97.6" clip-path="url(#terminal-line-27)">  1 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 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)">  gmail</text><text class="terminal-r9" x="122" y="752" textLength="158.6" clip-path="url(#terminal-line-30)">  [connector]</text><text class="terminal-r9" x="280.6" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">  3 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)"> 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)">  slack</text><text class="terminal-r9" x="122" y="776.4" textLength="158.6" clip-path="url(#terminal-line-31)">  [connector]</text><text class="terminal-r9" x="280.6" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">  2 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)"> 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)">↑↓ Navigate  Enter Show tools  Esc 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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="117.6" textLength="146.4" clip-path="url(#terminal-line-4)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="117.6" textLength="122" clip-path="url(#terminal-line-4)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="142" textLength="414.8" clip-path="url(#terminal-line-5)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="166.4" textLength="61" clip-path="url(#terminal-line-6)">Type </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)"> for more 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 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)">  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)">    |</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)">    |  POST /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)">    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)">| Gateway |</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)">    |</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)">    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)">| Service |</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)">    |</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)">    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)">|   DB    |</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)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</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)">></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% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
61
tests/snapshots/test_ui_snapshot_streaming_code_fence.py
Normal 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,
|
||||
)
|
||||
46
tests/stubs/fake_connector_registry.py
Normal 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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
401
tests/tools/test_connectors.py
Normal 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
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
AvailableCommand(
|
||||
name="proxy-setup",
|
||||
description="Configure proxy and SSL certificate settings",
|
||||
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()),
|
||||
),
|
||||
)
|
||||
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"],
|
||||
|
||||
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
|
||||
)
|
||||
await session.agent_loop.reload_with_initial_messages(
|
||||
base_config=new_config
|
||||
commands.append(
|
||||
AvailableCommand(
|
||||
name=cmd.name, description=cmd.description, input=input_spec
|
||||
)
|
||||
)
|
||||
message = (
|
||||
"Lean agent installed. Start a new session to switch to Lean mode."
|
||||
|
||||
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="instructions for the skill")
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
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:
|
||||
|
|
|
|||
5
vibe/acp/commands/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.acp.commands.registry import AcpCommandRegistry
|
||||
|
||||
__all__ = ["AcpCommandRegistry"]
|
||||
88
vibe/acp/commands/registry.py
Normal 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",
|
||||
),
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,11 +1037,14 @@ 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
|
||||
await messages_area.mount_all(widgets)
|
||||
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,9 +1370,52 @@ 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,
|
||||
)
|
||||
)
|
||||
|
||||
async def _show_status(self, **kwargs: Any) -> None:
|
||||
stats = self.agent_loop.stats
|
||||
status_text = f"""## Agent Statistics
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
server_tools.setdefault(server_name, []).append((tool_name, cls))
|
||||
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:
|
||||
self._viewing_server = None
|
||||
self.query_one("#mcp-title", NoMarkupStatic).update("MCP Servers")
|
||||
self.query_one("#mcp-help", NoMarkupStatic).update(
|
||||
"↑↓ Navigate Enter Show tools Esc Close"
|
||||
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._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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import html
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
|
@ -37,85 +38,79 @@ class ProjectContextProvider:
|
|||
_git_status_cache[self.root_path] = result
|
||||
return result
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@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:
|
||||
return f"({len(status_lines)} changes - use 'git status' for details)"
|
||||
return f"({len(status_lines)} changes)"
|
||||
|
||||
@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 (
|
||||
"(" in commit_msg
|
||||
and ")" in commit_msg
|
||||
and (paren_index := commit_msg.rfind("(")) > 0
|
||||
):
|
||||
commit_msg = commit_msg[:paren_index].strip()
|
||||
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
|
||||
|
||||
current_branch = subprocess.run(
|
||||
["git", "branch", "--show-current"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
cwd=self.root_path,
|
||||
stdin=subprocess.DEVNULL if is_windows() else None,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
).stdout.strip()
|
||||
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 = 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
|
||||
branches_output = remote_future.result().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:
|
||||
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)"
|
||||
|
||||
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 = []
|
||||
for line in log_output.split("\n"):
|
||||
if not (line := line.strip()):
|
||||
continue
|
||||
|
||||
if " " in line:
|
||||
commit_hash, commit_msg = line.split(" ", 1)
|
||||
if (
|
||||
"(" in commit_msg
|
||||
and ")" in commit_msg
|
||||
and (paren_index := commit_msg.rfind("(")) > 0
|
||||
):
|
||||
commit_msg = commit_msg[:paren_index].strip()
|
||||
recent_commits.append(f"{commit_hash} {commit_msg}")
|
||||
else:
|
||||
recent_commits.append(line)
|
||||
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}",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
|||
14
vibe/core/tools/connectors/__init__.py
Normal 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"]
|
||||
417
vibe/core/tools/connectors/connector_registry.py
Normal 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 = {}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||